Skip to content

feat/admin UI french i18n - #217

Open
edimitchel wants to merge 4 commits into
Calnode:mainfrom
edimitchel:feat/admin-ui-french-i18n
Open

edimitchel wants to merge 4 commits into
Calnode:mainfrom
edimitchel:feat/admin-ui-french-i18n

Conversation

@edimitchel

@edimitchel edimitchel commented Nov 3, 2026 •

Copy link
Copy Markdown

Porting french translation into the application for the back-office interface

First step toward translating the whole admin interface: adds the admin i18n catalog and a complete French translation. Related to #216.

Closes #218

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA. ✅
Posted by the CLA Assistant Lite bot.

@edimitchel

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

@edimitchel

Copy link
Copy Markdown
Author

recheck

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The port is thorough — catalog parity, placeholder checks, svelte-check and a production build all pass — but it removes every clickable setup link from the settings pages and regresses pluralization that already worked. Please restore those before merging.

Reviewed changes

  • i18n layer — new store-free catalog (catalog.ts) plus a Svelte locale store and $t/tr helpers (index.ts), with en.json as source of truth and a complete fr.json; locale resolved from localStorage/navigator.languages and selected in Settings → Profile.
  • Admin routes and components — hardcoded English replaced throughout; label arrays (TABS, LOCATION_TYPES, REASON_LABELS, nav lists) made $derived so they update on locale change; calendar/date formatting switched to the admin locale.
  • Tests and tooling — Vitest catalog test (CI) plus a duplicate node:test variant (manual command), allowImportingTsExtensions, and a Vitest exclude.
  • Docs — CHANGELOG, CONTRIBUTING and ARCHITECTURE updated for the admin catalog.

⚠️ Setup guidance lost all its clickable links

The translation replaced sentences that embedded anchors with plain catalog strings, so operator-facing setup pages now name destinations without linking them (Google Cloud console, Stripe dashboard, R2/B2/S3, LiveKit/Deepgram consoles, Zoom marketplace, docs/VIDEO.md). The stale settingsEmailLink / stripeLink keys suggest a link-splitting approach was started and abandoned. This is unrelated to translation and should be restored.

Technical details
# Setup links replaced by plain text

## Affected sites
- `frontend/src/routes/settings/google/+page.svelte:80` — `settings.google.setup.step1` lost the console.cloud.google.com link; `step3Testing` lost "Google's guide".
- `frontend/src/routes/settings/payments/+page.svelte:68` — Stripe Dashboard → API keys link gone.
- `frontend/src/routes/settings/storage/+page.svelte:63` — Cloudflare R2 / Backblaze B2 / AWS S3 links gone.
- `frontend/src/routes/settings/video/+page.svelte:105` — cloud.livekit.io and self-hosted docs links gone; the webhook step lost the LiveKit docs link; the Deepgram console link is gone.
- `frontend/src/routes/settings/zoom/+page.svelte:55` — Zoom App Marketplace, `docs/VIDEO.md` and `docs/ZOOM.md` links gone.
- `frontend/src/routes/event-types/[slug]/+page.svelte:799,836` — the invite hint lost its `Settings → Email` link and the price hint lost its `Stripe` link.
- Unused leftovers: `eventTypeEditor.invite.settingsEmailLink`, `eventTypeEditor.notifications.smtpNotConfiguredLink`, `eventTypeEditor.price.stripeLink`.

## Required outcome
- External and internal anchors are clickable again, with their visible text still translated.

## Suggested approach
- Split each sentence into prefix / link-text / suffix keys and render `<a href="…">{$t(linkKey)}</a>` between them. This keeps markup out of the catalogs and avoids `{@html}` for authored strings.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash | 𝕏

Comment thread frontend/src/lib/i18n/en.json Resolved
Comment thread frontend/src/lib/i18n/en.json Resolved
Comment thread frontend/src/lib/i18n/fr.json Resolved

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The plural, prefix, and date fixes all verify correctly. One item from the previous review is still open: the operator setup pages remain unlinked.

Reviewed changes

  • Restored plural handling — translate now selects .one/.other via Intl.PluralRules when a numeric {count} is passed, with translatePlural/translatePluralPair helpers; the five affected catalog entries became plural objects.
  • Restored the configured recordings prefix — settings.storage.recordings.subtitle takes a {prefix} placeholder wired from recordings_prefix.
  • Made date formatting locale-aware — api-keys, connections, members, and webhooks now format through localeTag() instead of the OS locale.
  • Translated the French Google redirect heading — settings.google.redirectUris.title is now URI de redirection autorisées.
  • Extended the tests — plural-selection and "no baked (s)" checks were added to both the Vitest and node:test suites (17 passing).

⚠️ Setup pages still have no clickable links

The original commit replaced sentences that embedded anchors with plain catalog strings, and the new commits did not touch those pages: operators reading Google, Stripe, storage, LiveKit, Deepgram, or Zoom setup guidance get destinations named but not linked. This is the outstanding half of the previous review and is unrelated to translation, so it should be restored before merging.

Technical details
# Operator setup links are still plain text

## Affected sites
- `frontend/src/routes/settings/google/+page.svelte` — console.cloud.google.com and Google's guide links
- `frontend/src/routes/settings/payments/+page.svelte` — Stripe Dashboard → API keys
- `frontend/src/routes/settings/storage/+page.svelte` — Cloudflare R2 / Backblaze B2 / AWS S3
- `frontend/src/routes/settings/video/+page.svelte` — cloud.livekit.io, self-hosted docs, LiveKit webhook docs, Deepgram console
- `frontend/src/routes/settings/zoom/+page.svelte` — Zoom App Marketplace, `docs/VIDEO.md`, `docs/ZOOM.md`
- `frontend/src/routes/event-types/[slug]/+page.svelte` — `Settings → Email` and `Stripe` inline links

## Required outcome
- External and internal anchors are clickable again, with their visible text still translated.

## Suggested approach
- Split each sentence into prefix / link-text / suffix catalog keys and render `<a href="…">{$t(linkKey)}</a>` between them; remove the now-unused `settingsEmailLink` / `smtpNotConfiguredLink` / `stripeLink` keys or wire them up.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This run reviewed the delta since the prior Pullfrog review (0409035), i.e. commit b21d28a.

  • Restored operator setup links — the guidance sentences on the calendar, Google, payments, storage, video, Zoom, and event-type invite/price/notification surfaces render real <a> tags again instead of plain text.
  • Added a safe link-splitting layer — splitLinked plus the LinkedText component split a catalog string on named {placeholder} slots, keeping translations plain text and never interpreting catalog content as HTML.
  • Wired the previously-stale link-text keys — settingsEmailLink, stripeLink and smtpNotConfiguredLink are now used by the restored links.
  • Extended the tests — en/fr placeholder parity for linked sentences, plain-text link keys, a no-HTML-anchors guard, and splitLinked unit tests, in both the Vitest and node suites.
  • Documented the convention — CONTRIBUTING now explains that link slots are {…} placeholders rendered by LinkedText.

Verified on b21d28a: pnpm run check reports 0 errors/warnings, pnpm run build succeeds, and the catalog node suite passes 22/22.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash | 𝕏

@shockalotti

Copy link
Copy Markdown
Contributor

The CLA check is failing because the commits are authored by cursoragent <cursoragent@cursor.com>, not by your GitHub account.

You signed as @edimitchel, and that signature is fine. But the action works by mapping each commit's author email to a GitHub user, then checking whether that user has signed. cursoragent@cursor.com does not map to you, so it cannot tell that your signature covers these commits.

To fix it, re-author the commits with the email tied to your GitHub account:

git config user.name "Your Name"
git config user.email "you@example.com"   # the email on your GitHub account
git rebase --exec 'git commit --amend --no-edit --reset-author' origin/main
git push --force-with-lease

If you use GitHub's privacy address, <your-id>+<username>@users.noreply.github.com is linked to your account and works too.

This is not a rule aimed at Cursor. It is the same thing we do for our own AI-assisted commits (Claude, OpenCode): the commit author is the human, so the CLA applies to them. Point Cursor at your identity and it will behave the same way.

@edimitchel
edimitchel force-pushed the feat/admin-ui-french-i18n branch from b21d28a to 266da5d Compare November 4, 2026 00:44
github-actions Bot added a commit that referenced this pull request Nov 4, 2026
@edimitchel

Copy link
Copy Markdown
Author

Done @shockalotti !

@shockalotti shockalotti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved the pending CI runs (this fork PR was sitting in `action_required`, so build/check/validate had never executed). They are now green: build ×2, check, validate, arm64-smoke, cla, pullfrog, SBOM. Worth saying plainly since the PR looked reviewed-but-unverified before.

The design holds up on re-read: separate client-side catalog, English as source of truth with per-key fallback, named `{placeholders}` via LinkedText rather than `{@html}`, and adding a locale is copy-the-JSON plus two registrations.

One gap before merge: `CLAUDE.md` still tells future agents the opposite of what this PR does. It is not in the diff, so merging leaves the repo's own guidance contradicting the code — that file is what agents read first.

Current text (CLAUDE.md:137):

```markdown
title/description) are translated. NOT translated: the admin SPA, the LiveKit room
(`livekit-room.html`/`.js`, ~45 hardcoded strings - it has its own asset pipeline and shares
no string plumbing with the Go templates), and admin-authored content (event names,
descriptions, questions, custom email copy).
```

Suggested replacement:

```markdown
title/description) are translated. The admin SPA has its own separate catalog
(`frontend/src/lib/i18n/`, client-side, `en` + `fr`, English fallback per key) — a
different system from the booker locales here; see ARCHITECTURE §23. NOT translated:
the LiveKit room
(`livekit-room.html`/`.js`, ~45 hardcoded strings - it has its own
asset pipeline and shares no string plumbing with the Go templates), and admin-authored
content (event names, descriptions, questions, custom email copy).
```

If you'd rather not touch it in this PR, say so and I'll do it as a follow-up immediately after merge — but it does need to land with this change, not later, or it will rot.

Also worth noting for the record: the `/book` Polish locale landed separately in #220. As you say in #218, that gives Polish bookers nothing in the admin — a `pl.json` catalog here would be the follow-up.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This run re-reviewed the delta since the prior review (266da5d): commit 36c77ef, a documentation-only change touching one file (+6/−4).

  • CLAUDE.md now describes the admin catalog — the i18n section no longer tells readers the admin SPA is untranslated; it points at frontend/src/lib/i18n/ (client-side, en + fr, per-key English fallback) as a distinct system from the booker locales, and defers to ARCHITECTURE §23, whose locale table and prose already document it.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash | 𝕏

@shockalotti

Copy link
Copy Markdown
Contributor

Thank you, this is genuinely strong work. I had it reviewed in depth rather than skimmed, and the verdict is good work, no blocker.

One thing I need from you: please rebase onto current main. Your last upstream merge is v0.13.0; since then main merged a docs fix (#222) that edits the same CONTRIBUTING.md locale section, so the merge now conflicts there. Nothing else is stale. A rebase is the only thing blocking this.

What the review verified independently, so you know it was actually read:

  • 1,324 keys in each catalogue, exact match, no non-string leaves
  • 1,272 $t/tr/trp/trp2 call sites, zero pointing at a missing key
  • placeholder and trailing-punctuation parity exact across all 1,324 keys
  • the 103 byte-identical strings are brand names, URLs and protocol tokens, not untranslated UI text hiding
  • build passes, node suite 22/22, vitest 22/22, visual smoke 5/5
  • the modified shared components have no importers outside admin routes, so the booker page is insulated
  • the French reads native rather than machine: consistent terminology, « » quotes, correct register for each screen

Do not act on the following. I am taking these myself after merge so you are not asked for another round:

  1. {@html} on catalogue content (+layout.svelte:306) contradicts the "never {@html} catalog content" note at catalog.ts:8, and catalog.test.ts:125-131 only rejects <a and href=. Safe today because the data is in-repo, but a future community locale file could carry <img src=x onerror=…> and pass that test. I will split the sentence or widen the guard.
  2. Both catalogues ship in one 143 KB chunk, about 15% of client JS, downloaded by every admin including on /admin/login. I will lazy-load everything except English.
  3. Nothing proves the 1,272 call sites match the catalogue. A typo would silently render settings.foo.bar and CI would stay green. I will add that scan.
  4. Strict key parity will block a partially translated German or Polish catalogue. I will relax it to "every key exists in en" plus a coverage floor.

French corrections I will also apply: eventTypeEditor.embed.inline.label = "En ligne" reads as online, not inline (suggest "Intégré à la page"); directoryOrderHint "pages publiques de personne et d'équipe" is word-for-word English; and the archived/active empty state has wrong agreement ("archivés" against masculine singular "type de rendez-vous") because a translated label is lowercased and spliced into another sentence. Everything else passed: this is not machine French.

Provenance: as with the existing booker locales, we will record in the CHANGELOG and CONTRIBUTING that this is a machine-assisted draft with no native review yet. If you happen to know a French speaker who would read the admin end to end, that would be worth more than any of the fixes above.

On the architecture question, I had suggested folding the admin catalogue into the public locale files. The review made a better, quantified case against it and I am persuaded: 169 shared keys across 11 languages against 1,324 admin keys in 2 languages means unification saves roughly 11% of translator effort while forcing a schema migration on both sides and putting admin strings in the binary that is served to the public. We will keep the files separate, standardise terminology with a glossary, and revisit only when a second admin locale lands.

Rebase and I will take it from there.

The SvelteKit admin is no longer English-only. A small client-side
catalog (English source of truth, French by a native speaker) drives
chrome, list pages, the event-type editor, settings, and auth screens.

Locale is chosen in Settings → Profile (localStorage) with a
navigator.languages fallback; missing keys fall back to English.
Dates and times follow the admin locale. The LiveKit room and
booker-facing catalogs are unchanged.
Use Intl.PluralRules (.one/.other) instead of baked "(s)" strings,
pass the configured recordings prefix into storage copy, translate
the Google redirect-URI heading, and format admin dates with localeTag().
Split catalog sentences on {placeholders} and render real <a> tags via
LinkedText so Google, Stripe, storage, LiveKit, Deepgram, Zoom, and
in-app Settings links work again in both locales without {@html}.
@edimitchel

Copy link
Copy Markdown
Author

Rebased onto current main, conflicts in CHANGELOG.md and CONTRIBUTING.md resolved — should be good to go. Thanks for the review!

I'll be using Calnode personally with the French admin UI, so I'll fix any translation mistakes I run into. Once the catalog has been properly reviewed by a human, we can drop the machine-draft note for French.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Translate the admin interface (i18n for the back-office)

2 participants