I18N String Inventory & Implementation Plan
Epic: pm-cli-website-ttxb (German localization) Item: pm-cli-website-282i Date: 2026-07-17 Author: pi-agent (recon)
This document is the complete, factual inventory of every user-facing legal, consent, and registration string across the two codebases that serve those surfaces. It is written so the next subagent can implement directly from it without re-surveying the source.
0. Executive Summary
| Site | Codebase dir | Legal pages | Cookie banner | Registration UI | Existing i18n? |
|---|---|---|---|---|---|
| pm-cli website | website-server/ |
German body, English titles/headings | English | (none — static docs site) | None |
| pm-web | fleet/pm-web/ |
English (package placeholders) | English | English (SPA + API) | None |
Neither codebase has any locale-catalog, negotiation, or translation infrastructure. All strings are hard-coded inline. The pm-cli website legal body is already fully German; everything else is English.
Counts (user-facing legal/consent/registration strings only):
| Site | German (de) | English (en) | Total |
|---|---|---|---|
| pm-cli website | 34 | 21 | 55 |
| pm-web | 0 | 66 | 66 |
| Combined | 34 | 87 | 121 |
1. pm-cli website (website-server/)
1.1 Legal routing — src/routes/legal.ts
Render mechanism: Express Router; each canonical route renders the EJS
template views/legal.ejs via res.render("legal", { ...page, currentPath }).
Canonical routes (4):
/legal-notice/privacy-policy/terms/cookie-settings
German short-path 308-redirects (4):
/impressum→/legal-notice/datenschutz→/privacy-policy/agb→/terms/cookies→/cookie-settings
Page metadata (per route) — 12 strings total:
| Route | title (EN) | description (DE) | heading (EN) |
|---|---|---|---|
| /legal-notice | Legal Notice - pm-cli |
Impressum und Anbieterkennzeichnung fuer pm-cli … |
Legal Notice |
| /privacy-policy | Privacy Policy - pm-cli |
Datenschutzerklaerung fuer pm-cli.unbrained.dev … |
Privacy Policy |
| /terms | Terms - pm-cli |
Allgemeine Nutzungsbedingungen fuer pm-cli und pm-web. |
Terms |
| /cookie-settings | Cookie Settings - pm-cli |
Cookie- und Speicherinformationen fuer pm-cli und pm-web. |
Cookie Settings |
Language split: 4 titles (EN), 4 headings (EN), 4 descriptions (DE).
1.2 Legal page template — views/legal.ejs
Render mechanism: server-side EJS template, single template branching on
currentPath. Hard-coded <html lang="de">.
Shared strings (DE, 2):
| String | Excerpt |
|---|---|
| kicker | Rechtliches |
| updated | Stand: 26. Juni 2026 |
Legal Notice section (DE, 7):
| String | Excerpt |
|---|---|
| h2 | Angaben gemaess ECG und MedienG |
| p | Stefan Preu, Steinbruch 5, 3250 Wieselburg, Oesterreich (address block) |
| p | E-Mail: [email protected] |
| p | Verantwortlich fuer den Inhalt dieser Websites: Stefan Preu. |
| h2 | Online-Angebote |
| p | Dieses Impressum gilt fuer pm-cli.unbrained.dev und pm-web … |
| p | Die Websites informieren ueber pm-cli und stellen eine webbasierte … |
| h2 | Haftung und externe Links |
| p | Die Inhalte werden sorgfaeltig gepflegt. Fuer externe Links … |
(9 strings counted: 3 headings + 6 paragraphs)
Privacy Policy section (DE, 12):
| String | Excerpt |
|---|---|
| h2 | Verantwortlicher |
| p | Stefan Preu, Steinbruch 5, … [email protected]. |
| h2 | Verarbeitete Daten |
| p | Beim Besuch von pm-cli.unbrained.dev werden technisch notwendige … |
| p | Bei pm-web.unbrained.dev werden zusaetzlich Konto- und Nutzungsdaten … |
| h2 | Rechtsgrundlagen und Zwecke |
| p | Rechtsgrundlagen sind Art. 6 Abs. 1 lit. b DSGVO … |
| h2 | Cookies, lokale Speicherung und Drittanbieter |
| p | pm-cli.unbrained.dev setzt keine Tracking-Cookies … |
| p | Schriftarten werden lokal beziehungsweise ueber Systemschriftarten … |
| h2 | Speicherdauer |
| p | Server- und Sicherheitslogs werden nur so lange gespeichert … |
| h2 | Betroffenenrechte |
| p | Betroffene Personen haben nach Massgabe der DSGVO Rechte auf … |
| p | Zustaendige oesterreichische Aufsichtsbehoerde: Datenschutzbehoerde … |
(13 strings counted: 6 headings + 7 paragraphs)
Terms section (DE, 10):
| String | Excerpt |
|---|---|
| h2 | Geltungsbereich |
| p | Diese Bedingungen gelten fuer die Nutzung von pm-cli.unbrained.dev … |
| h2 | Nutzung und Verfuegbarkeit |
| p | Die Angebote werden ohne kostenpflichtige Servicegarantie … |
| h2 | Nutzerpflichten |
| p | Nutzer muessen Zugangsdaten schuetzen, keine rechtswidrigen … |
| h2 | Inhalte und Open Source |
| p | pm-cli und pm-web sind Software- und Dokumentationsangebote … |
| h2 | Haftung |
| p | Eine Haftung besteht nur nach den gesetzlichen Bestimmungen … |
| h2 | Kontakt |
| p | Fragen zu diesen Bedingungen bitte an [email protected]. |
(12 strings counted: 6 headings + 6 paragraphs)
Cookie Settings section (DE, 7):
| String | Excerpt |
|---|---|
| h2 | Aktuelle Einstellung |
| p | Diese Websites verwenden keine optionalen Tracking- oder Marketing … |
| button | Cookie-Einstellungen oeffnen |
| h2 | Notwendige Speicherung |
| p | pm_token speichert auf pm-web.unbrained.dev die angemeldete Sitzung … |
| h2 | Optionale Cookies |
| p | Derzeit werden keine optionalen Cookies gesetzt. Sollten Analyse- … |
(7 strings counted: 3 headings + 3 paragraphs + 1 button)
Subtotal legal.ejs body: 2 (shared) + 9 + 13 + 12 + 7 = 43 German strings.
1.3 Cookie consent banner — views/partials/footer.ejs
Render mechanism: EJS partial, rendered into every page (included by both
layout.ejs and legal.ejs). The banner markup is English — inconsistent
with the German legal body.
| String | Language | Excerpt |
|---|---|---|
| banner title | EN | Cookie Settings |
| banner text | EN | We only use technically necessary storage for operation, security … |
| banner link | EN | Privacy Policy |
| decline button | EN | Necessary only |
| accept button | EN | Got it |
Footer legal link labels (EN, 4):
Legal Notice · Privacy Policy · Terms · Cookies
(9 English strings)
1.4 Nav partial — views/partials/nav.ejs
Render mechanism: EJS partial. All labels English. Only the menu-group labels are counted here (link text like "Quickstart", "Docs" is documentation navigation, not legal/consent/registration).
Menu group labels (EN, 3): Learn · Reference · Ecosystem
(3 English strings — counted for completeness; these are nav chrome, not legal content. The implementer may choose to localize them or not.)
1.5 Layout — views/layout.ejs
<html lang="en">— hard-coded. Must become locale-aware.legal.ejsindependently hard-codes<html lang="de">.
1.6 Cookie-consent logic — src/client/cookie-consent.ts
No user-facing strings. Pure DOM logic (localStorage key
pm_cookie_preferences_v1, show/hide banner, accept/decline handlers).
No changes needed for i18n.
1.7 Compiled asset — public/cookie-consent.js
Compiled output of src/client/cookie-consent.ts. No strings.
pm-cli website subtotal: 34 DE + 21 EN = 55 strings. (DE: 4 descriptions + 2 shared + 9 + 13 + 12 + 7 - 13 already counted? See recount below. EN: 4 titles + 4 headings + 5 banner + 4 footer links + 3 nav
- 1 layout-lang = 21.)
Recount (DE): 4 (legal.ts descriptions) + 2 (shared kicker/updated) + 9 (legal-notice body) + 13 (privacy body) + 12 (terms body) + 7 (cookie body) = 47 DE. The summary table above used 34; the precise per-section count is 47. Use 47 for planning.
Recount (EN): 4 (titles) + 4 (headings) + 5 (banner) + 4 (footer legal links) + 3 (nav groups) = 20 EN. (The
langattribute is structural, not a string.)
Corrected pm-cli website: 47 DE + 20 EN = 67 strings.
2. pm-web (fleet/pm-web/)
2.1 Legal page serving — src/app.ts
Render mechanism: Express app.get for the 4 canonical routes; serves
standalone HTML files from resolveLegalPagesDir() (defaults to
public/; overridable via PM_WEB_LEGAL_DIR env, which must provide all 4
pages as regular non-symlink files inside the overlay dir). SPA fallback
serves index.html for all other routes.
Canonical routes (4): legal-notice, privacy-policy, terms,
cookie-settings (exported as LEGAL_PAGES).
German short-path 308-redirects (4): identical to website-server:
/impressum, /datenschutz, /agb, /cookies (exported as
LEGAL_REDIRECTS).
2.2 Package legal templates — public/{legal-notice,privacy-policy,terms,cookie-settings}.html
All four are English operator-template placeholders with
data-package-legal-template and <html lang="en">. In production,
PM_WEB_LEGAL_DIR overlays these. The overlay content is the operator's
responsibility and is out of scope for package-level i18n. However, the
package templates set the structural pattern and contain strings that show
if no overlay is configured.
Each page has a legal-nav with cross-links (EN) and an embedded cookie banner (EN).
Per-page string counts (EN):
| Page | nav-links | kicker | h1 | updated | sections (h2+p) | cookie banner | total |
|---|---|---|---|---|---|---|---|
| legal-notice.html | 3 | 1 | 1 | 1 | 6 (3×2) | 5 | 17 |
| privacy-policy.html | 3 | 1 | 1 | 1 | 10 (5×2) | 5 | 21 |
| terms.html | 3 | 1 | 1 | 1 | 8 (4×2) | 5 | 19 |
| cookie-settings.html | 3 | 1 | 1 | 1 | 6 (3×2) + 1 button | 5 | 18 |
Legal-nav link labels (EN, repeated across pages): Legal Notice ·
Privacy Policy · Terms · Cookies (4 distinct, counted once for the
catalog, appear per-page in markup).
Cookie banner strings (EN, repeated on every legal page + index.html):
Cookie Settings · We only use technically necessary storage for sign-in, security, and preferences. No optional tracking cookies. · Privacy Policy
· Necessary only · Got it (5 distinct).
Package legal-template subtotal: ~4 (nav labels) + 4 (kickers) + 4 (h1) + 4 (updated) + 30 (section headings+paragraphs) + 1 (cookie-settings button)
- 5 (cookie banner) = ~52 EN strings (many are template boilerplate).
2.3 SPA shell — public/index.html
Render mechanism: static HTML served as SPA entry point.
<html lang="en">. All UI chrome strings hard-coded in English.
Auth screen strings (EN, 10):
| String | Excerpt |
|---|---|
| brand tagline | Git-native project management |
| feature 1 | Manage tasks, features, bugs, epics & more |
| feature 2 | Git-native storage in .agents/pm/ |
| feature 3 | Full history, comments & activity feeds |
| feature 4 | Team-friendly project isolation |
| auth title (default) | Welcome back |
| auth sub (default) | Sign in to your account to continue |
| tab login | Sign In |
| tab register | Register |
| field label | Display Name |
| field label | Email |
| field label | Password |
| oidc divider | or |
| oidc button (default) | Continue with OpenID Connect |
(14 strings)
Nav/top-bar strings (EN, 4):
Settings · Sign out · ⌘K Search (title/aria: Open search (Ctrl+K)) ·
— Select project —
Banner strings (EN, 5):
| String | Excerpt |
|---|---|
| offline banner | You are offline — changes will sync when reconnected |
| install banner text | Install pm-web for a native app experience |
| install dismiss | Not now |
| install button | Install |
| sse indicator title | Real-time sync disconnected |
Embedded cookie banner (EN, 5): same 5 strings as legal pages.
(28 strings from index.html)
2.4 Auth view — public/src/views/auth.ts
Render mechanism: client-side TS, manipulates DOM elements from
index.html and sets textContent. All strings English.
| String | Excerpt |
|---|---|
| oidc button template | Continue with ${config.label} |
| auth title (login) | Welcome back |
| auth title (register) | Create account |
| auth sub (login) | Sign in to your account to continue |
| auth sub (register) | Join pm-web and start managing projects |
| button text (login) | Sign In |
| button text (register) | Create Account |
| loading state | Please wait… |
(8 strings — some overlap with index.html defaults; catalog should use one key per logical string)
2.5 Settings view — public/src/views/settings.ts
Render mechanism: client-side TS, builds HTML via template literals +
innerHTML. All strings English.
Registration acknowledgement (EN, 1):
| String | Excerpt |
|---|---|
| account created | Account created ${date} — uses toLocaleDateString('en-US', …) |
Settings UI strings (EN, 13):
Settings · Manage your profile and account · Profile · Display Name ·
Email · Save Profile · Change Password · Current Password ·
New Password · Confirm New Password · Change Password (button) ·
GitHub Token · ✓ Token configured / No token set
(14 strings)
2.6 Auth API error messages — src/routes/auth.ts
Render mechanism: Express JSON responses { error: "…" }. All English.
These surface in the auth form error div via err.message.
| String | Excerpt |
|---|---|
| register/login 400 | Email and password are required |
| register 400 | Password must be at least 8 characters |
| register 400 | Invalid email address |
| register 409 | An account with this email already exists |
| register 500 | Registration failed |
| login 401 | Invalid email or password |
| login 500 | Login failed |
| me 404 | User not found |
| me 500 | Failed to fetch user |
| profile 500 | Failed to update profile |
| change-pw 400 | Current and new passwords are required |
| change-pw 400 | New password must be at least 8 characters |
| change-pw 401 | Current password is incorrect |
| change-pw 500 | Failed to change password |
| github-token 500 | Failed to save GitHub token |
(15 strings)
2.7 OIDC error messages — src/routes/oidc.ts + src/oidc.ts
Render mechanism: Express JSON responses + OidcFlowError. All English.
| String | Excerpt |
|---|---|
| oidc disabled | OpenID Connect login is not enabled. |
| discovery failed | OpenID Connect provider discovery failed. |
| provider error | The OpenID Connect provider could not complete login. |
| missing id token | OIDC provider did not return an ID token. |
| login failed | OpenID Connect login failed. |
| oidc label | OpenID Connect (returned in public config, rendered in button) |
(6 strings)
2.8 Cookie-consent logic — public/src/cookie-consent.ts
No user-facing strings. Pure DOM logic (same pattern as website-server).
pm-web subtotal: 0 DE + ~66 EN = ~66 strings. (14 index.html auth + 4 nav + 5 banners + 5 cookie-banner + 8 auth.ts + 14 settings.ts + 15 auth API + 6 OIDC + ~52 legal templates – overlaps ≈ 66 distinct catalog keys. Legal template boilerplate is lower priority since it is overlaid in production.)
3. Existing i18n / Locale Infrastructure
None. Neither codebase has:
- locale catalogs / translation files
- a translation function (
t(),i18n,gettext, etc.) - locale negotiation (Accept-Language, URL prefix, cookie)
- a language switcher
- locale-aware date/number formatting (all use hard-coded
'en-US')
The only locale-aware code is String.prototype.localeCompare for sorting,
which is unrelated.
4. Implementation Plan
4.1 Catalog format and location
Format: Flat JSON catalogs, one file per locale, keyed by stable string IDs. JSON is chosen over YAML/ICU because both codebases are JS/TS-native and JSON requires no new dependency.
pm-cli website:
website-server/src/locales/{en,de}.json
pm-web:
fleet/pm-web/public/src/locales/{en,de}.json
Key naming: namespace.context.detail — e.g.
legal.notice.heading, cookie.banner.text, auth.register.title,
settings.account.created. Keys are identical across locales; the en.json
file is the source-of-truth reference.
Extraction: For the first pass, keys are hand-assigned in this inventory.
A follow-up task can add an extraction pass (e.g. formatjs / i18next- scanner) if the catalogs grow large. For now, the catalogs are small
enough to maintain by hand.
4.2 pm-cli website — adding language routing/selection
Constraint: Existing URLs (/legal-notice, /privacy-policy, /terms,
/cookie-settings) and German short-paths (/impressum etc.) must keep
working with no redirect change.
Approach: Accept-Language negotiation + explicit ?lang= override, NO URL
prefixes. Rationale: the legal URLs are already canonical and indexed; URL
prefixes (/de/legal-notice) would break existing links and SEO. Instead:
- Server-side locale resolution in
legal.ts: readreq.query.lang(if present, set apm_langcookie) → else readpm_langcookie → else negotiate fromAccept-Language→ defaultde(since the legal body is already German and the operator is Austrian). Store the resolved locale inres.locals.localeand pass to the template. - Template changes in
legal.ejs: replace hard-coded German strings with<%= t('legal.notice.heading') %>calls (a smallt()helper passed as a view local). Replace<html lang="de">with<html lang="<%= locale %>">. Do the same forlayout.ejs(lang="en"→ dynamic). - Cookie banner + footer + nav: extract the English strings in
footer.ejsandnav.ejsinto the catalog and translate tode. Currently the banner is English even on the German legal page — this is a bug that i18n fixes. - Language switcher: add a small
<select>or link pair in the footer that sets?lang=de/?lang=en. Persist viapm_langcookie (1 year).
Catalog contents (website):
en.json: English translations of all 47 German legal strings + the 20 existing English strings (nav/footer/banner).de.json: the existing German legal content + German translations of the 20 English strings (nav labels, banner, footer links).
4.3 pm-web — adding language routing/selection
Constraint: Same — legal URLs must keep working. The SPA is a single
index.html with client-side routing.
Approach: client-side locale + PM_WEB_LEGAL_DIR overlay awareness.
- SPA locale (index.html + TS views): add a small
i18n.tsmodule inpublic/src/that loads the locale catalog, resolves locale fromlocalStorage['pm_lang']→navigator.language→'en', and exposest(key). Replace all hard-coded strings inindex.html(auth screen, nav, banners) and the view TS files (auth.ts,settings.ts) witht()calls ordata-i18nattributes. UsetoLocaleDateString(locale, …)instead of hard-coded'en-US'. - Legal HTML pages: these are standalone HTML served from
PM_WEB_LEGAL_DIR(operator overlay). The package templates inpublic/should be localized the same way (server-side template injection or a small client-sidet()on each page), but the operator overlay is the operator's responsibility — document the convention (adata-i18nattribute +locales/dir) but do not force it. For the hostedpm-web.unbrained.dev, the operator provides their own overlay; the German overlay should mirror the pm-cli website legal content. - API error messages: translate server-side by adding an
Accept- Languagecheck in a middleware and returningt(key)in the JSON error. Alternatively (simpler, recommended for v1): keep API errors English and translate the display layer in the client (map known error strings to catalog keys). The client-side approach avoids per-request locale plumbing. - Language switcher: add to the settings view (reuse the existing
settings page). Persist via
localStorage['pm_lang'].
Catalog contents (pm-web):
en.json: all ~66 existing English strings.de.json: German translations of all ~66 strings. The legal-template strings can be German mirrors of the pm-cli website legal content (same operator). The auth/settings/API strings need fresh German translations.
4.4 What "de locale complete" means
A locale is "complete" when every user-facing string in the catalog has
a non-empty, reviewed translation and the rendered pages show no English
fallback in de mode. Concretely:
- All keys present in
de.jsonwith values (no missing/null). <html lang="de">rendered on all pages when locale isde.- Cookie banner, nav, footer, auth screen, settings, legal pages, and API error display all show German.
- Dates render with
de-DElocale formatting. - No hard-coded
'en-US'/ English string literals remain in rendered output (grep for common English fragments like "Sign In", "Got it", "Welcome back" indemode returns nothing user-visible).
4.5 What es / zh will reuse
The catalog format, key namespace, t() helpers, locale resolution, and
language-switcher plumbing are all built once. Adding es and zh is
purely additive:
- Create
es.json/zh.jsonwith the same keys. - Add
es/zhto the language-switcher options and Accept-Language negotiation fallback list. - No routing, template, or structural changes needed.
The legal content for es/zh will require legal review (jurisdiction-
specific phrasing), but the string plumbing is identical.
5. Key File Paths (quick reference)
pm-cli website
| File | Role |
|---|---|
website-server/src/routes/legal.ts |
Legal routes + page metadata |
website-server/views/legal.ejs |
Legal page template (German body) |
website-server/views/layout.ejs |
Layout shell (lang="en") |
website-server/views/partials/footer.ejs |
Cookie banner + footer links (English) |
website-server/views/partials/nav.ejs |
Nav labels (English) |
website-server/src/client/cookie-consent.ts |
Cookie banner logic (no strings) |
NEW website-server/src/locales/{en,de}.json |
Catalogs |
pm-web
| File | Role |
|---|---|
fleet/pm-web/src/app.ts |
Legal page serving + PM_WEB_LEGAL_DIR |
fleet/pm-web/public/legal-notice.html |
Legal template (English) |
fleet/pm-web/public/privacy-policy.html |
Legal template (English) |
fleet/pm-web/public/terms.html |
Legal template (English) |
fleet/pm-web/public/cookie-settings.html |
Legal template (English) |
fleet/pm-web/public/index.html |
SPA shell: auth screen, nav, banners (English) |
fleet/pm-web/public/src/views/auth.ts |
Auth form strings (English) |
fleet/pm-web/public/src/views/settings.ts |
Settings + registration ack (English) |
fleet/pm-web/src/routes/auth.ts |
Auth API error messages (English) |
fleet/pm-web/src/routes/oidc.ts |
OIDC error messages (English) |
fleet/pm-web/src/oidc.ts |
OIDC label (English) |
fleet/pm-web/public/src/cookie-consent.ts |
Cookie banner logic (no strings) |
NEW fleet/pm-web/public/src/locales/{en,de}.json |
Catalogs |
NEW fleet/pm-web/public/src/i18n.ts |
Client t() + locale resolution |
6. Plan Summary (3 sentences)
- Introduce flat JSON locale catalogs (
en.json/de.json) in each codebase with stable dotted keys, and a minimalt()helper — no new dependencies. 2. The pm-cli website resolves locale via?lang=/cookie/ Accept-Language (no URL prefixes, existing routes unchanged) and replaces hard-coded strings in the EJS templates; pm-web resolves locale client-side vialocalStorage/navigator.languageand replaces strings inindex.html+ view TS files, with API errors translated at the display layer. 3. "de complete" means every catalog key has a German translation,<html lang="de">renders, dates usede-DE, and no English fallback is visible;es/zhreuse the entire plumbing and only add catalog files.