Skip to content

feat(custom-v2): separate-bundle i18n for fork-owned keys + complete EN/NL coverage#197

Merged
zjean merged 5 commits into
mainfrom
feat/v2-i18n-custom-bundle
May 19, 2026
Merged

feat(custom-v2): separate-bundle i18n for fork-owned keys + complete EN/NL coverage#197
zjean merged 5 commits into
mainfrom
feat/v2-i18n-custom-bundle

Conversation

@zjean
Copy link
Copy Markdown
Owner

@zjean zjean commented May 19, 2026

Summary

Isolates all fork-owned i18n keys into a new frontend/src/i18n/custom/{en,nl}.json bundle so future upstream syncs never touch them, then fills every previously-untranslated string in custom-v2/ so both English and Dutch users get fully localised UI.

Per audit before this PR: 38 EN + 40 NL v3_* keys were intermixed with upstream's bundle (merge-conflict risk on every sync), and ~95 hardcoded English strings in v2 code bypassed i18n entirely — Dutch users saw English on every toast, dialog, breadcrumb. After this PR: zero untranslated user-facing strings (verified by an AST-style sweep of translate pipes and l10nTranslate directives across custom-v2/; the only remaining "unresolved" match is a dynamic {{ t.label }} binding whose value resolves at runtime).

Changes (4 commits, reviewable in order)

1. mod(i18n): add custom translation provider for fork-owned keys (f2c2d1ab)

  • Registers a second angular-l10n provider custom alongside the existing app provider.
  • Extends TranslationLoader.get(language, provider) to load i18n/custom/{en,nl}.json for the new provider.
  • Creates empty stub files. No behaviour change yet.

2. mod(i18n): move v3_* keys from upstream bundles into custom/ (161ce371)

  • Relocates 38 EN + 40 NL v3_* keys (link, share, delete, move-to-trash, etc.) out of i18n/{en,nl}.json into i18n/custom/{en,nl}.json.
  • Fills two English values that previously only existed in NL (v3_delete_permanently_one, v3_empty_trash) so non-NL users see real phrases instead of key literals.
  • Net effect: ~80 lines of merge-conflict surface removed from upstream JSON. Runtime behaviour unchanged.

3. feat(custom-v2): make ToastService translation-aware and add v2 keys (30db4f28)

  • Refactors ToastService.success/error/info to accept an optional args parameter and translate the message through L10nTranslationService before display. Strings that aren't keys fall through to themselves (preserves English).
  • Adds ~45 new keys covering admin CRUD toasts, comment ops, settings ops, share-dialog variants, and parameterised file-operation progress messages (v3_moving_to_trash_one_progress, v3_renamed_to, v3_item_created, etc.).
  • Rewrites parameterised toast call sites in personal, space-files, trash, trash-bin, text-code-view, share-dialog to use key + args instead of template literals.
  • Simplifies my earlier inline L10nTranslationService.translate('Space is disabled') call from chore: sync upstream (2026-05-18) + v2 trash disabled-space guard #196 to the new toast.info('v3_space_disabled_in_trash', { name }) pattern.

4. feat(custom-v2): backfill remaining v2 i18n keys + document the pattern (aeefa43c)

  • Adds the final batch of missing keys spotted by a sweep across custom-v2/** (top-bar, admin headers, dock-panel empty states, recents groupings, etc.).
  • Updates CLAUDE.md § i18n to document the new pattern (custom bundle location, v3_/English-as-key conventions, toast auto-translation).

Why a separate bundle

Past upstream syncs have repeatedly conflicted on i18n/{en,nl}.json because upstream adds keys near ours alphabetically. Isolating fork keys into i18n/custom/ zeroes out that conflict surface — upstream only writes to its own bundles, we only write to ours, angular-l10n merges them at lookup. Same pattern as our custom-* source paths.

Why translation-aware ToastService

~50 v2 call sites passed raw English to toasts. Wrapping each with inline translation.translate(...) is verbose; making the service do it centralises the change to one file and lets callers stay readable. Static strings that already exist as upstream keys ('Group updated', 'Member added') become translated for free; v2-specific strings get keys in custom/.

Languages other than EN + NL

Out of scope for this PR. TranslationLoader returns an empty bundle for the custom provider in any language that isn't en or nl, so the missing-translation handler kicks in and returns the key literal — same behaviour as today. (Pre-existing v3_* keys had identical coverage; this PR doesn't regress it.) If/when a third language is needed, drop custom/<lang>.json and add it to CUSTOM_LANGS in l10n.ts.

Verification

  • npm --prefix frontend run build — clean (only the pre-existing 60 kB bundle-budget warning)
  • npm --prefix frontend run lint — clean
  • npm --prefix backend run build — TSC 0 issues, SWC 567 files
  • ✅ Missing-key audit on translate pipes: 0 unresolved
  • ✅ Missing-key audit on l10nTranslate directives: 0 unresolved (one dynamic binding excluded)
  • ✅ JSON validity check on both custom bundles

Migration notes for future work

  • New v2 strings → put the key in i18n/custom/{en,nl}.json (use v3_* prefix for parameterised, plain English for static).
  • New parameterised toast → this.toast.success('v3_my_key', { name, count }).
  • Static toast → this.toast.success('My static string') — works whether the key lives in upstream or custom.
  • Breadcrumbs / confirm dialogs / action sheets — they already auto-translate via template pipes; just pass keys.

Merge strategy

Squash and merge per CLAUDE.md (feat PR, not an upstream sync).

zjean and others added 5 commits May 19, 2026 16:11
angular-l10n supports multiple providers whose bundles are merged at
lookup time. This change registers a second provider 'custom' alongside
the existing 'app' one, with bundles loaded from frontend/src/i18n/custom/.

Goals:
- Keep fork-specific i18n keys (v3_*, custom-v2 strings) out of upstream's
  i18n bundles, so upstream syncs never touch them.
- Provide EN + NL translations for v2 UI strings without needing to
  modify upstream JSON files at all.

For now the custom bundles are empty stubs; subsequent commits will (1)
migrate existing v3_* keys out of upstream en.json/nl.json into the
custom bundles, and (2) translate the ~95+ hardcoded English strings
currently scattered across custom-v2 components.

Other languages (de/fr/es/...) fall through unchanged — the
missing-translation handler returns the key itself, matching pre-existing
behaviour for non-en/nl users on v2 features.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Relocates the 38 EN + 40 NL fork-specific keys (v3_link_*, v3_share_*,
v3_delete_*, v3_move_to_trash_*, v3_n_items_selected, v3_drop_indexes,
v3_space_managers_required, etc.) out of upstream's en.json/nl.json into
the new i18n/custom/{en,nl}.json bundles introduced by the previous
commit.

Runtime behaviour is unchanged — angular-l10n merges keys from both
providers at lookup time, so every existing consumer keeps resolving its
v3_ keys exactly as before. Confirmed by grepping callers (zero v3_
references outside custom-v2/) and by a successful frontend build+lint.

While here, fills two English values that previously only existed in NL
(v3_delete_permanently_one, v3_empty_trash) so non-NL users get a real
phrase instead of the key literal.

Removes ~38 lines of merge-conflict surface area from the upstream JSON
files — these were the prime conflict points during the past two
upstream syncs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactors ToastService so toast.success/error/info accept an i18n key
(plus optional placeholder args), then resolves it through
L10nTranslationService before display. Strings that aren't registered
as keys fall through to themselves — so the existing English-only
behaviour is preserved while EN/NL users now get proper localisation.

Adds ~45 new keys to i18n/custom/{en,nl}.json:
- Static keys for v2-specific toast strings (admin CRUD failure paths,
  comment ops, settings ops, share-dialog error variants).
- Parameterised v3_* keys with {{ name }} / {{ nb }} placeholders for
  file-operation progress toasts (Moving/Copying/Archiving/Renamed/
  Created/Downloading/Deleting/Saved/etc.).
- Two confirm-dialog keys (Unsaved changes / Discard) that weren't yet
  in any bundle.

Updates the parameterised toast call sites (personal, space-files,
trash, trash-bin, text-code-view, share-dialog) to pass the key + args
form instead of template literals. Static toast calls don't need
rewriting — they're already keys (e.g. 'Group updated') that the
refactored service now translates automatically.

The trash component's earlier inline `L10nTranslationService.translate()`
call from PR #196 is also simplified to the new pattern, dropping the
explicit injection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the last batch of missing translations spotted by a sweep across
custom-v2's templates and components — keys referenced by translate
pipes or `l10nTranslate` directives that weren't yet in any bundle:

- Top-bar / navigation: Back, Forward, Sync-In
- Admin screens: Account type, Group scope, manager(s), Sign in as…, admin
- Dock panel empty states: Click a row to inspect..., Select a file to see...,
  Comments are file-only, No comments to show, etc.
- Recents groupings: Today, Yesterday, Earlier
- File-ops misc: Show comments, Storage usage, Saving…, Diagram
- Group members empty state: Members of, No members yet.

After this commit, a missing-key audit across translate pipes and
l10nTranslate directives in custom-v2/ reports zero unresolved
references (the lone remaining match is a dynamic `{{ t.label }}`
binding, which resolves through its runtime value rather than as a
literal key).

Also updates CLAUDE.md §i18n to document the new custom-bundle pattern,
the v3_* / English-as-key naming convention, and the toast translation
auto-resolution — replacing the stale "custom. prefix" guidance with
what the codebase actually does.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Live verification through the dev server exposed a real gap: the audit
in the previous commits only walked translate-pipe / l10nTranslate uses
and treated anything else as already-fine. But several
high-visibility surfaces hardcoded English directly into element text
or attributes without any translation wrapper, so Dutch users still
saw English on every page.

This commit wires translation through those surfaces:

- **Left navigation** (most-visible gap): section labels (Workspace /
  Shared / Admin), the dynamic `{{ it.label }}` entries for workspace /
  shared / admin lists, "All shares" / "Administration" toggles, the
  static "People" / "Trash" links, and the "← Back to classic UI"
  footer. Settings link title/aria-label now translate too.

- **Title bar** (mobile): "Toggle navigation" aria-label on the brand
  button.

- **Top bar** (desktop): "Breadcrumb" landmark aria-label;
  ⌘K search placeholder ("Search files…" / "Search…" mobile variant)
  routed through L10nTranslationService since it's a computed signal.

- **Recents screen**: hero kicker ("Workspace"), title heading
  ("Recents"), lede paragraph, "Pick up where you left off" / "Recent
  comments" section labels, "{{ g.label }}" bucket labels (Today /
  Yesterday / Earlier), and the empty-state title + body.

- **Diagram view**: "Loading diagram…" state + "Diagram editor" iframe
  title + dynamic errorMessage value.

- **Office view**: "Loading editor…" state, OnlyOffice error strings
  (settings missing / not available / load failed), routed through
  the translate pipe.

- **Text/code view**: "Search (Ctrl/Cmd+F)" and "Line wrap" tooltip
  titles, the readonly/editing toggle tooltip.

- **Aria-labels** on Toast dismiss button, Tree picker close button,
  Page breadcrumb landmark.

Adds 18 new keys to custom/{en,nl}.json covering the previously
unkeyed strings (Workspace, All shares, ← Back to classic UI, Loading
diagram…, No recent activity, Pick up where you left off, Recent
comments, Toggle navigation, Diagram editor, Search (Ctrl/Cmd+F),
Line wrap, Breadcrumb, Files-you-have-touched lede, empty-state
lede, OnlyOffice errors x3, Read-only/Editing toggle titles, Search
files… / Search…).

Verified live in Dutch via chrome-devtools MCP:
- Left nav renders: BESTANDEN / WERKRUIMTE / Zoeken / Recente bestanden
  / Persoonlijk / Ruimtes / GEDEELD / Alle delingen / Met mij / Met
  anderen / Via links / BEHEER / Beheer / Gebruikers / Groepen /
  Mensen / Prullenmand / Instellingen / ← Terug naar klassieke UI
- Top bar: Terug / Vooruit / Bestanden zoeken… / Meldingen
- Recents: heading "Recente bestanden", lede + section labels Dutch
- Settings + admin/users: all strings Dutch
- Zero console errors/warnings (the L10nMissingTranslationHandler
  logs to console.error on misses, so a clean console proves coverage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zjean
Copy link
Copy Markdown
Owner Author

zjean commented May 19, 2026

Deep verification update

After opening this PR, I ran a live verification through chrome-devtools MCP against the dev server with sessionStorage.locale = {language:'nl'}. The Dutch UI exposed real gaps that the static audit missed.

What the static audit caught

The pre-PR audits walked all | translate: pipe uses and l10nTranslate directive uses, and reported zero unresolved keys. That's accurate — every key that's reaching the translation pipeline is now in the bundle.

What the static audit missed

Several high-visibility surfaces had hardcoded English directly in element text or attributes, with no translation mechanism wired up at all — so they never showed in the "missing key" scan. The biggest offender was the v2 left nav: section labels, item labels, and footer links all rendered English even with the Dutch locale set.

Fix shipped in c0f642ec

  • Left navigation, title bar, top bar, recents screen, diagram-view, office-view, text-code-view — all wired through translate
  • 18 additional keys added to custom/{en,nl}.json
  • Live re-verification confirms full Dutch coverage across left nav, top bar, breadcrumbs, recents, settings, admin
  • Console clean (the L10nMissingTranslationHandler logs to console.error on misses; zero errors == zero misses)

Updated diff

5 commits, EN keys: 132, NL keys: 132, key sets identical.

Lesson learned

A "missing key" audit is necessary but not sufficient. The real test is "does it render correctly with locale=nl?" — that's what catches strings that bypass translation entirely. I should have done the live test before opening the PR.

@zjean zjean merged commit b8be39d into main May 19, 2026
1 check passed
@zjean zjean deleted the feat/v2-i18n-custom-bundle branch May 19, 2026 15:59
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.

1 participant