From e14a4250efb828262347f5a4bbc4067cd71d885f Mon Sep 17 00:00:00 2001 From: grm Date: Mon, 14 Sep 2026 01:32:25 +0300 Subject: Add languages: English and Greek, chosen per blog on the Settings tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every blog and its dashboard were hard-wired to English. A blogger can now pick the language of their blog; it switches the whole dashboard and the blog's fixed text — post dates, archive months, the RSS link, the pager, the 404 page — while what the blogger wrote is left alone. The new internal/i18n package keys translations by the English string, so an untranslated key renders as English rather than blank, and TestGreekCatalogComplete scans the templates and handlers to fail when the Greek catalog misses a key or keeps a stale one. Templates are compiled once per language with t/tf/date/postdate/month closed over the language, so they need no data plumbing. The language lives in settings.language (blog migration 00002), not in the theme, so "Reset design" does not touch it. Public pages use the blog's language; management pages use the logged-in user's own blog's, so a superadmin editing someone else's blog keeps theirs; the login page follows Accept-Language. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A --- internal/web/templates/dashboard/confirm.html | 14 +- internal/web/templates/dashboard/design.html | 178 ++++++++++----------- internal/web/templates/dashboard/images.html | 14 +- internal/web/templates/dashboard/layout.html | 102 ++++++------ internal/web/templates/dashboard/module_form.html | 28 ++-- internal/web/templates/dashboard/overview.html | 24 +-- internal/web/templates/dashboard/page_form.html | 18 +-- internal/web/templates/dashboard/pages.html | 20 +-- internal/web/templates/dashboard/password.html | 12 +- internal/web/templates/dashboard/post_form.html | 20 +-- internal/web/templates/dashboard/posts.html | 20 +-- internal/web/templates/dashboard/section_form.html | 40 ++--- internal/web/templates/dashboard/sections.html | 26 +-- internal/web/templates/dashboard/settings.html | 15 +- 14 files changed, 267 insertions(+), 264 deletions(-) (limited to 'internal/web/templates/dashboard') diff --git a/internal/web/templates/dashboard/confirm.html b/internal/web/templates/dashboard/confirm.html index 8dd0b3a..62bf4a4 100644 --- a/internal/web/templates/dashboard/confirm.html +++ b/internal/web/templates/dashboard/confirm.html @@ -1,16 +1,16 @@ -{{define "title"}}Confirm · Blogspace{{end}} +{{define "title"}}{{t "Confirm"}} · Blogspace{{end}} {{define "content"}}
-

Are you sure?

+

{{t "Are you sure?"}}

{{if .Data.isHome}} -

This is the home page; it cannot be deleted. Make another page the home page first.

-

Back

+

{{t "This is the home page; it cannot be deleted. Make another page the home page first."}}

+

{{t "Back"}}

{{else}} -

You are about to permanently delete {{.Data.what}}. This cannot be undone.

+

{{tf "You are about to permanently delete %s. This cannot be undone." .Data.what}}

-

- Cancel

+

+ {{t "Cancel"}}

{{end}}
diff --git a/internal/web/templates/dashboard/design.html b/internal/web/templates/dashboard/design.html index 9c2a12f..e961388 100644 --- a/internal/web/templates/dashboard/design.html +++ b/internal/web/templates/dashboard/design.html @@ -1,21 +1,21 @@ -{{define "title"}}Design · {{.Blog.Title}}{{end}} +{{define "title"}}{{t "Design"}} · {{.Blog.Title}}{{end}} {{define "content"}} {{$t := .Data.theme}}
-

Design

-

Changes are live as soon as you save; refresh your blog ↗ to see them.

+

{{t "Design"}}

+

{{t "Changes are live as soon as you save; refresh"}} {{t "your blog"}} ↗ {{t "to see them."}}

-

SchemesBackgroundTextHeaderMenuPostsFooterLogo & icon

-

What appears where — header, columns, footer and their modules — is arranged on the Layout tab.

+

{{t "Schemes"}}{{t "Background"}}{{t "Text"}}{{t "Header"}}{{t "Menu"}}{{t "Posts"}}{{t "Footer"}}{{t "Logo & icon"}}

+

{{t "What appears where — header, columns, footer and their modules — is arranged on the"}} {{t "Layout"}} {{t "tab."}}

-

Colour schemes

-

A quick start: applying a scheme sets every colour below and saves immediately. Images, fonts and layout are kept.

+

{{t "Colour schemes"}}

+

{{t "A quick start: applying a scheme sets every colour below and saves immediately. Images, fonts and layout are kept."}}

- {{range .Data.presets}} {{end}}
@@ -25,158 +25,158 @@
-

Background

+

{{t "Background"}}

- - +
- {{template "imagepick" (dict "name" "bg_image" "current" $t.BgImage "sub" .Blog.Subdomain "images" .Data.images "label" "Background image")}} + {{template "imagepick" (dict "name" "bg_image" "current" $t.BgImage "sub" .Blog.Subdomain "images" .Data.images "label" (t "Background image"))}}
-

Text & content area

+

{{t "Text & content area"}}

- - - + + +
- - + +
- - - -
- -
-

The content spans the whole page; how much of it the posts get is set by the column widths on the Layout tab.

+

{{t "The content spans the whole page; how much of it the posts get is set by the column widths on the"}} {{t "Layout"}} {{t "tab."}}

-

Posts & dates

- +

{{t "Posts & dates"}}

+
-
- - View blog ↗ + + {{t "View blog"}} ↗
- Reset to defaults -

Puts every colour, font and layout option back to how a new blog starts and forgets the chosen images (they stay in your library).

+ {{t "Reset to defaults"}} +

{{t "Puts every colour, font and layout option back to how a new blog starts and forgets the chosen images (they stay in your library)."}}

- +
{{end}} diff --git a/internal/web/templates/dashboard/images.html b/internal/web/templates/dashboard/images.html index 2e39531..8b9faa3 100644 --- a/internal/web/templates/dashboard/images.html +++ b/internal/web/templates/dashboard/images.html @@ -1,14 +1,14 @@ -{{define "title"}}Images · {{.Blog.Title}}{{end}} +{{define "title"}}{{t "Images"}} · {{.Blog.Title}}{{end}} {{define "content"}}
-

Images

-

Images are stored with your blog. Put one in a post with Insert image in the editor, or copy its line from below.

+

{{t "Images"}}

+

{{t "Images are stored with your blog. Put one in a post with"}} {{t "Insert image"}} {{t "in the editor, or copy its line from below."}}

- -

+ +

-{{else}}

No images yet.

{{end}} +{{else}}

{{t "No images yet."}}

{{end}}
{{end}} diff --git a/internal/web/templates/dashboard/layout.html b/internal/web/templates/dashboard/layout.html index 229ca0e..48b6778 100644 --- a/internal/web/templates/dashboard/layout.html +++ b/internal/web/templates/dashboard/layout.html @@ -1,86 +1,86 @@ -{{define "title"}}Layout · {{.Blog.Title}}{{end}} +{{define "title"}}{{t "Layout"}} · {{.Blog.Title}}{{end}} {{define "content"}} {{$t := .Data.theme}} {{$u := print "/b/" .Blog.Subdomain "/layout"}}
-

Layout

-

Decide what goes where on your blog. Every change here is saved straight away; refresh your blog ↗ to see it. Colours and fonts are on the Design tab.

+

{{t "Layout"}}

+

{{t "Decide what goes where on your blog. Every change here is saved straight away; refresh"}} {{t "your blog"}} ↗ {{t "to see it."}} {{t "Colours and fonts are on the"}} {{t "Design"}} {{t "tab."}}

-

ColumnsHeaderLeft columnMain contentRight columnFooterMenu

+

{{t "Columns"}}{{t "Header"}}{{t "Left column"}}{{t "Main content"}}{{t "Right column"}}{{t "Footer"}}{{t "Menu"}}

-

Columns

+

{{t "Columns"}}

-
Header{{if not $t.HeaderOn}} (hidden){{end}}
+
{{t "Header"}}{{if not $t.HeaderOn}} ({{t "hidden"}}){{end}}
-
Left {{$t.LeftWidth}}%
-
Main content {{$t.MainWidth}}%
-
Right {{$t.RightWidth}}%
+
{{t "Left"}} {{$t.LeftWidth}}%
+
{{t "Main content"}} {{$t.MainWidth}}%
+
{{t "Right"}} {{$t.RightWidth}}%
-
Footer{{if not $t.FooterOn}} (hidden){{end}}
+
{{t "Footer"}}{{if not $t.FooterOn}} ({{t "hidden"}}){{end}}
- - + +
-

Between 15% and 40% each; the main content takes the rest. A column only appears when it has at least one module; without columns the posts take the whole width. On phones the columns stack: main content first, then the left and right columns, then the footer.

- -

+

{{t "Between 15% and 40% each; the main content takes the rest. A column only appears when it has at least one module; without columns the posts take the whole width. On phones the columns stack: main content first, then the left and right columns, then the footer."}}

+ +

-{{template "area" (dict "area" "header" "root" . "hint" "The top of every page. Stack the title, a logo and the menu in the order you like. A banner image can be chosen on the Design tab.")}} -{{template "area" (dict "area" "left" "root" . "hint" "A narrow column beside your posts. Good for the menu, an archive or recent posts.")}} +{{template "area" (dict "area" "header" "root" . "hint" (t "The top of every page. Stack the title, a logo and the menu in the order you like. A banner image can be chosen on the Design tab."))}} +{{template "area" (dict "area" "left" "root" . "hint" (t "A narrow column beside your posts. Good for the menu, an archive or recent posts."))}}
-

Main content

-

Your posts and pages always show here. You can add your own HTML before and after them — an intro box, a newsletter form, a badge…

+

{{t "Main content"}}

+

{{t "Your posts and pages always show here. You can add your own HTML before and after them — an intro box, a newsletter form, a badge…"}}

{{range $area := .Data.areaOrder}}{{$mods := index $.Data.areas $area}} - + {{if $mods}}{{$m := index $mods 0}} - - + + {{else}} - - + + {{end}} {{end}}
{{index $.Data.areaNames $area}}
{{t (index $.Data.areaNames $area)}}{{index $.Data.names $m.Kind}}{{with modsummary $m}} — {{.}}{{end}}edit remove{{t (index $.Data.names $m.Kind)}}{{with modsummary $m}} — {{.}}{{end}}{{t "edit"}} {{t "remove"}}Nothing extra.
{{t "Nothing extra."}}
-{{template "area" (dict "area" "right" "root" . "hint" "A narrow column on the other side of your posts. Same choices as the left one.")}} -{{template "area" (dict "area" "footer" "root" . "hint" "The bottom of every page: a copyright line, the RSS link, a list of your pages.")}} +{{template "area" (dict "area" "right" "root" . "hint" (t "A narrow column on the other side of your posts. Same choices as the left one."))}} +{{template "area" (dict "area" "footer" "root" . "hint" (t "The bottom of every page: a copyright line, the RSS link, a list of your pages."))}} @@ -90,24 +90,24 @@ {{define "area"}}{{$r := .root}}{{$area := .area}}{{$u := print "/b/" $r.Blog.Subdomain "/layout"}}{{$on := $r.Data.theme.AreaOn $area}}{{$mods := index $r.Data.areas $area}}
-

{{index $r.Data.areaNames $area}}{{if not $on}} hidden{{end}}

-
+

{{t (index $r.Data.areaNames $area)}}{{if not $on}} {{t "hidden"}}{{end}}

+

{{.hint}}

{{if $mods}} {{range $mods}} - - + + {{end}} -
-
-
+
+
{{index $r.Data.names .Kind}}{{with modsummary .}} — {{.}}{{end}} - {{if and (eq .Kind "logo") (not $r.Data.theme.Logo)}}no logo chosen pick one on the Design tab{{end}}{{if modsettings .Kind}}edit {{end}}remove{{t (index $r.Data.names .Kind)}}{{with modsummary .}} — {{.}}{{end}} + {{if and (eq .Kind "logo") (not $r.Data.theme.Logo)}}{{t "no logo chosen"}} {{t "pick one on the"}} {{t "Design"}} {{t "tab."}}{{end}}{{if modsettings .Kind}}{{t "edit"}} {{end}}{{t "remove"}}
{{else}}

Nothing here yet{{if ne $area "header"}} — this area is not shown until you add a module{{end}}.

{{end}} + {{else}}

{{if ne $area "header"}}{{t "Nothing here yet — this area is not shown until you add a module."}}{{else}}{{t "Nothing here yet."}}{{end}}

{{end}}
- - + +
{{end}} diff --git a/internal/web/templates/dashboard/module_form.html b/internal/web/templates/dashboard/module_form.html index b63b405..0cd27fa 100644 --- a/internal/web/templates/dashboard/module_form.html +++ b/internal/web/templates/dashboard/module_form.html @@ -2,32 +2,32 @@ {{define "content"}} {{$m := .Data.module}}
-

{{.Data.name}} in the {{lower .Data.area}}

+

{{.Data.name}} {{tf "in the %s" (lower .Data.area)}}

{{if or (eq $m.Kind "recent") (eq $m.Kind "archive") (eq $m.Kind "sitemap") (and (eq $m.Kind "html") (or (eq $m.Area "left") (eq $m.Area "right") (eq $m.Area "footer")))}} - + {{end}} {{if eq $m.Kind "recent"}} -
-

The newest published posts from every page of your blog, newest first.

+
+

{{t "The newest published posts from every page of your blog, newest first."}}

{{else if eq $m.Kind "archive"}} -

Every published post grouped by year and month, newest first. Years and months fold open and shut; the latest year starts open.

+

{{t "Every published post grouped by year and month, newest first. Years and months fold open and shut; the latest year starts open."}}

{{else if eq $m.Kind "sitemap"}} -

Links to every page of your blog.

+

{{t "Links to every page of your blog."}}

{{else if eq $m.Kind "html"}} - -

Put into your page exactly as written — embeds, badges and scripts all work. Anything broken here (an unclosed tag, say) can break the page around it, so check your blog after saving. 20 KB at most.

+ +

{{t "Put into your page exactly as written — embeds, badges and scripts all work. Anything broken here (an unclosed tag, say) can break the page around it, so check your blog after saving. 20 KB at most."}}

{{else if eq $m.Kind "text"}} - - -

Plain text; each line becomes its own paragraph.

+ + +

{{t "Plain text; each line becomes its own paragraph."}}

{{end}}

- - View blog ↗ - Back to layout + + {{t "View blog"}} ↗ + {{t "Back to layout"}}

diff --git a/internal/web/templates/dashboard/overview.html b/internal/web/templates/dashboard/overview.html index acd539d..e021495 100644 --- a/internal/web/templates/dashboard/overview.html +++ b/internal/web/templates/dashboard/overview.html @@ -1,35 +1,35 @@ {{define "title"}}{{.Blog.Title}} · Blogspace{{end}} {{define "content"}}

{{.Blog.Title}}

-

Your blog is live at {{.BlogURL}}. Every change you save here shows up there as soon as you refresh.

+

{{t "Your blog is live at"}} {{.BlogURL}}. {{t "Every change you save here shows up there as soon as you refresh."}}

-

Pages

+

{{t "Pages"}}

    - {{range .Data.pages}}
  • {{.Title}} ({{.PostCount}} posts{{if .IsHome}}, home{{end}}{{if not .ShowInNav}}, hidden from menu{{end}})
  • {{end}} + {{range .Data.pages}}
  • {{.Title}} ({{tf "%d posts" .PostCount}}{{if .IsHome}}, {{t "home"}}{{end}}{{if not .ShowInNav}}, {{t "hidden from menu"}}{{end}})
  • {{end}}
-

Latest posts

+

{{t "Latest posts"}}

{{if .Data.posts}} - + {{range .Data.posts}} - + - + {{end}}
TitlePageDate
{{t "Title"}}{{t "Page"}}{{t "Date"}}
{{.Title}}{{if not .Published}} hidden{{end}}{{.Title}}{{if not .Published}} {{t "hidden"}}{{end}} {{.PageTitle}} {{date .CreatedAt}}view ↗{{t "view"}} ↗
-

All posts

- {{else}}

No posts yet. Write the first one.

{{end}} +

{{t "All posts"}}

+ {{else}}

{{t "No posts yet."}} {{t "Write the first one."}}

{{end}}
{{end}} diff --git a/internal/web/templates/dashboard/page_form.html b/internal/web/templates/dashboard/page_form.html index 2692449..08c4a43 100644 --- a/internal/web/templates/dashboard/page_form.html +++ b/internal/web/templates/dashboard/page_form.html @@ -1,19 +1,19 @@ -{{define "title"}}{{if .Data.page.ID}}Edit page{{else}}New page{{end}} · {{.Blog.Title}}{{end}} +{{define "title"}}{{if .Data.page.ID}}{{t "Edit page"}}{{else}}{{t "New page"}}{{end}} · {{.Blog.Title}}{{end}} {{define "content"}}
-

{{if .Data.page.ID}}Edit page{{else}}New page{{end}}

+

{{if .Data.page.ID}}{{t "Edit page"}}{{else}}{{t "New page"}}{{end}}

- -
diff --git a/internal/web/templates/dashboard/pages.html b/internal/web/templates/dashboard/pages.html index eeb09c5..cb40eb4 100644 --- a/internal/web/templates/dashboard/pages.html +++ b/internal/web/templates/dashboard/pages.html @@ -1,25 +1,25 @@ -{{define "title"}}Pages · {{.Blog.Title}}{{end}} +{{define "title"}}{{t "Pages"}} · {{.Blog.Title}}{{end}} {{define "content"}}
-

Pages

- -

Each page holds its own list of posts. Which pages are in the menu, and in what order, is set on the Layout tab.

+

{{t "Pages"}}

+ +

{{t "Each page holds its own list of posts. Which pages are in the menu, and in what order, is set on the"}} {{t "Layout"}} {{t "tab."}}

- + {{range .Data.pages}} + {{if .IsHome}}{{t "home"}}{{end}}{{if not .ShowInNav}}{{t "not in menu"}}{{end}}{{end}}
OrderTitleAddressPosts
{{t "Order"}}{{t "Title"}}{{t "Address"}}{{t "Posts"}}
-
-
+
+
{{.Title}} - {{if .IsHome}}home{{end}}{{if not .ShowInNav}}not in menu{{end}} {{if .IsHome}}/{{else}}/{{.Slug}}{{end}} ↗ {{.PostCount}} - {{if not .IsHome}}
- delete{{end}} + {{if not .IsHome}}
+ {{t "delete"}}{{end}}
diff --git a/internal/web/templates/dashboard/password.html b/internal/web/templates/dashboard/password.html index 3e28744..2150a40 100644 --- a/internal/web/templates/dashboard/password.html +++ b/internal/web/templates/dashboard/password.html @@ -1,13 +1,13 @@ -{{define "title"}}Change password · Blogspace{{end}} +{{define "title"}}{{t "Change password"}} · Blogspace{{end}} {{define "content"}}
-

Change password

+

{{t "Change password"}}

- - - -

+ + + +

{{end}} diff --git a/internal/web/templates/dashboard/post_form.html b/internal/web/templates/dashboard/post_form.html index 8b4a593..af48215 100644 --- a/internal/web/templates/dashboard/post_form.html +++ b/internal/web/templates/dashboard/post_form.html @@ -1,24 +1,24 @@ -{{define "title"}}{{if .Data.post.ID}}Edit post{{else}}New post{{end}} · {{.Blog.Title}}{{end}} +{{define "title"}}{{if .Data.post.ID}}{{t "Edit post"}}{{else}}{{t "New post"}}{{end}} · {{.Blog.Title}}{{end}} {{define "content"}}
-

{{if .Data.post.ID}}Edit post{{else}}New post{{end}}

+

{{if .Data.post.ID}}{{t "Edit post"}}{{else}}{{t "New post"}}{{end}}

- +
-
- + {{template "editor" (dict "name" "body" "value" .Data.post.BodyMD "rows" 24 "tall" true "upload" (print "/b/" .Blog.Subdomain "/images/upload") "csrf" .CSRF)}} - +

- - {{if .Data.post.ID}}View post ↗{{end}} - Back to posts + + {{if .Data.post.ID}}{{t "View post"}} ↗{{end}} + {{t "Back to posts"}}

diff --git a/internal/web/templates/dashboard/posts.html b/internal/web/templates/dashboard/posts.html index c0b7112..3a99a31 100644 --- a/internal/web/templates/dashboard/posts.html +++ b/internal/web/templates/dashboard/posts.html @@ -1,28 +1,28 @@ -{{define "title"}}Posts · {{.Blog.Title}}{{end}} +{{define "title"}}{{t "Posts"}} · {{.Blog.Title}}{{end}} {{define "content"}}
-

Posts

- +

{{t "Posts"}}

+
-