diff options
| author | grm <grm@eyesin.space> | 2026-09-14 01:32:25 +0300 |
|---|---|---|
| committer | grm <grm@eyesin.space> | 2026-09-14 01:32:25 +0300 |
| commit | e14a4250efb828262347f5a4bbc4067cd71d885f (patch) | |
| tree | 3fcf6512f3b030b06c818ff4a224677a026d3b1b /AGENTS.md | |
| parent | 282d2ab3e0fb3a74f32160a231225773001acd14 (diff) | |
| download | blogspace-e14a4250efb828262347f5a4bbc4067cd71d885f.tar.gz blogspace-e14a4250efb828262347f5a4bbc4067cd71d885f.tar.bz2 blogspace-e14a4250efb828262347f5a4bbc4067cd71d885f.zip | |
Add languages: English and Greek, chosen per blog on the Settings tab
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'AGENTS.md')
| -rw-r--r-- | AGENTS.md | 33 |
1 files changed, 31 insertions, 2 deletions
@@ -52,6 +52,8 @@ internal/store/ Store = control DB (users, blog registry, create/delete bl BlogStore = one blog's DB (settings, pages, posts, images, sections, modules, menu) internal/auth/ bcrypt, JWT issue/parse, cookie, HMAC CSRF internal/markdown/ Render(md) → sanitized HTML +internal/i18n/ languages, T/Tf (English keys → catalog), FormatDate/Month, Accept-Language Match; + el.go is the Greek catalog, el_months.go the month tables internal/slug/ Make/Valid/WithSuffix (Greek transliteration included) internal/web/ server.go (host router, middleware, render helpers) routes.go (all routes), handlers_*.go (sections = announcements, layout = modules + menu) @@ -65,7 +67,7 @@ internal/web/ server.go (host router, middleware, render helpers) - **One database per blog**: the registry row (`blogs`: id, owner, subdomain, `db_name`) lives in the control DB; everything else — a one-row `settings` - table (title, tagline, theme) plus pages, posts, images, sections, modules, + table (title, tagline, language, theme) plus pages, posts, images, sections, modules, menu_items — lives in `blog_<sub>` (`db.DBName`: dashes → underscores, so subdomains are capped at 58 chars). No table in a blog DB carries a blog id; the database is the scope. `store.Store` (control) hands out a @@ -189,6 +191,33 @@ internal/web/ server.go (host router, middleware, render helpers) `notice notice-<style>` use translucent colours so they fit any theme. - **Dashboard nav**: `dashnav.html` marks the current tab with `hasPrefix .Path`; a new management section needs a tab there. +- **Languages** (`internal/i18n`, `settings.language`): every blog has a + language (`en` by default, `el` = Greek) chosen on the Settings tab. Public + pages use the blog's language; management pages use the **logged-in user's + own blog's** language (a user has one blog, so a superadmin editing someone + else's blog keeps theirs); anonymous pages (`/webadmin`) match + `Accept-Language`. `server.go`: `hostBlog`/`withBlog` put it in `ctxLang`, + `s.lang(r)` resolves it (lazily looking the user's blog up otherwise), the + `view` carries it as `.Lang` (also the `<html lang>`). + Keys are the English strings: templates write `{{t "Save"}}` / + `{{tf "Page %d of %d" .N .Last}}`, handlers `s.tr(r, "Post deleted.")` / + `s.trf(r, "Uploaded %s.", name)`; an unknown key renders as English. + Templates are compiled once **per language** (`templates.get(lang, name)`) + with `funcsFor(lang)`, whose `t`, `tf`, `date` (dashboard, long style), + `postdate` (`style, time` for the theme's `DateFormat`), `month` + (archive headings) and `modsummary` are closed over the language — so + templates need no data plumbing. A sentence with a link in the middle is + split into fragments around the `<a>`. `moduleNames`/`areaNames`/preset + names stay English maps and are translated where used (`{{t (index …)}}`, + `s.tr(r, moduleNames[k])`). Content the blogger typed is never translated; + default module headings are created in the blog's language. + `TestGreekCatalogComplete` scans the templates and handlers for keys and + fails when the Greek catalog misses one or keeps a stale one — reword an + English string and the catalog entry must follow. Keys reached through a + variable (`{{t .Role}}`) are listed in that test by hand. + Adding a language: an entry in `i18n.languages`, a catalog file, a month + table in `monthTables` (nominative, in-date and short forms), and the test + gets a copy per language if its rules differ. - **Slugs**: auto-generated from the title; on collision generated slugs get `-2`, `-3`…, user-typed slugs return a 409 with a message. - **Errors**: `store.ErrNotFound` / `store.ErrConflict` are the sentinels @@ -217,7 +246,7 @@ superadmin password to `admin`. Production refuses both. (runs in every blog database — it must not reference `users`/`blogs`) and `internal/db/migrations/control/` for users and the registry; goose `-- +goose Up/Down` sections; they run automatically at startup. Never - edit an applied migration. Both chains were re-baselined at 00001 after + edit an applied migration. Blog chain so far: `00001_init`, `00002_language`. Both chains were re-baselined at 00001 after the move to per-blog databases; deployments from before it have `goose_db_version` rows 2–7 in the control DB that must be deleted once (README "Upgrading from a single database") or the next control migration |
