Loaded via TranslocoHttpLoader (assets/i18n/${lang}.json). Every visitor downloads the entire ~600 KB blob for their language up front, and each release invalidates the hash so it re-downloads. The file only grows as features/languages are added — this is the frontend's clearest O(strings × languages) payload.
Global config in projects/spikersoft/src/main.ts: single loader, availableLangs, reRenderOnLangChange: true, provideTranslocoMissingHandler(LoggingMissingHandler).
Routes are already lazy (loadComponent) in projects/spikersoft/src/routes.ts — but translations are not split to match.
Service worker caches /assets/** lazily, so repeat visits are cheap; the cost is first-load + per-release re-fetch of the whole blob.
Proposal
Adopt Transloco scopes (provideTranslocoScope) so each lazy feature loads only its own translation chunk, aligning i18n payload with the existing route code-splitting:
Keep a small global/common file (nav, shared buttons, errors) loaded at startup.
Extract per-feature scopes (e.g. playground, profile, blog, games, contact, …) into assets/i18n/<scope>/<lang>.json, provided at the lazy route/component level.
Migrate keys incrementally — a scope can be carved out one feature at a time while the global file continues to serve everything not yet migrated.
Why this is non-breaking
Scoped translations are additive; unmigrated keys keep resolving from the global file, so the app never regresses mid-migration.
The existing LoggingMissingHandler + the test that asserts every key exists in every language give an immediate signal if a key is dropped during extraction.
No change to how components call transloco.translate / the transloco pipe for global keys; scoped keys just gain a scope prefix.
Acceptance criteria
Common/global i18n file reduced to shared strings only; measured initial i18n payload materially smaller than the current ~600 KB.
At least the playground scope (largest surface) split out and loaded with its lazy route.
Missing-key test updated to cover per-scope files; CI green.
No visible regression in language switching (reRenderOnLangChange still works across scopes).
Docs note the scope convention so new features add scoped keys by default.
Priority
High among the frontend-scaling items — biggest, safest payoff. Do before the i18n files cross ~1 MB.
(Filed from an architecture review of content/translation delivery. Local-hosting only — no CDN.)
## Problem
UI strings ship as **one monolithic Transloco file per language**, HTTP-fetched at runtime:
- `projects/spikersoft/src/assets/i18n/en.json` — **568,871 bytes**
- `projects/spikersoft/src/assets/i18n/es.json` — **603,890 bytes**
Loaded via `TranslocoHttpLoader` (`assets/i18n/${lang}.json`). Every visitor downloads the entire ~600 KB blob for their language up front, and each release invalidates the hash so it re-downloads. The file only grows as features/languages are added — this is the frontend's clearest O(strings × languages) payload.
## Current state (measured)
- Transloco `@jsverse/transloco` **8.4.0** (supports scopes / lazy scoped translations natively).
- Global config in `projects/spikersoft/src/main.ts`: single loader, `availableLangs`, `reRenderOnLangChange: true`, `provideTranslocoMissingHandler(LoggingMissingHandler)`.
- Routes are already lazy (`loadComponent`) in `projects/spikersoft/src/routes.ts` — but translations are not split to match.
- Service worker caches `/assets/**` lazily, so repeat visits are cheap; the cost is first-load + per-release re-fetch of the whole blob.
## Proposal
Adopt Transloco **scopes** (`provideTranslocoScope`) so each lazy feature loads only its own translation chunk, aligning i18n payload with the existing route code-splitting:
1. Keep a small **global/common** file (nav, shared buttons, errors) loaded at startup.
2. Extract per-feature scopes (e.g. `playground`, `profile`, `blog`, `games`, `contact`, …) into `assets/i18n/<scope>/<lang>.json`, provided at the lazy route/component level.
3. Migrate keys **incrementally** — a scope can be carved out one feature at a time while the global file continues to serve everything not yet migrated.
## Why this is non-breaking
- Scoped translations are additive; unmigrated keys keep resolving from the global file, so the app never regresses mid-migration.
- The existing `LoggingMissingHandler` + the test that asserts every key exists in every language give an immediate signal if a key is dropped during extraction.
- No change to how components call `transloco.translate` / the `transloco` pipe for global keys; scoped keys just gain a scope prefix.
## Acceptance criteria
- [ ] Common/global i18n file reduced to shared strings only; measured initial i18n payload materially smaller than the current ~600 KB.
- [ ] At least the `playground` scope (largest surface) split out and loaded with its lazy route.
- [ ] Missing-key test updated to cover per-scope files; CI green.
- [ ] No visible regression in language switching (`reRenderOnLangChange` still works across scopes).
- [ ] Docs note the scope convention so new features add scoped keys by default.
## Priority
High among the frontend-scaling items — biggest, safest payoff. Do before the i18n files cross ~1 MB.
_(Filed from an architecture review of content/translation delivery. Local-hosting only — no CDN.)_
Global bundle materially smaller: en.json 568,871 → 390,160 bytes (−31%), es.json 603,890 → 424,833 (−30%). Scopes carved out: devTools (172/202 KB) + keycloakAdmin (35/39 KB) — together ~34% of the old monolith.
Largest surface split with its lazy route: devTools (all dev-tools playgrounds) provided via provideTranslocoScope("devTools") on /tools (inherited by every tool child) and /clue-for-sql; keycloakAdmin on /keycloak-admin.
Missing-key testing covers per-scope files: testing/transloco-test-setup.ts merges scoped files back under their namespaces, and PR #218 adds i18n-key-parity.spec.ts — CI-enforced en↔es key parity for the global bundle and every scope, with a documented allowlist for the Spanish-overlay namespaces (employment.*, gamesClueForSql.mysteries.*) whose English source is backend seed data / in-code catalogs. It also swept the 6 genuinely stale es-only keys it flagged. CI green (test-and-lint, build, Sonar, e2e-smoke, e2e-anonymous).
No language-switch regression across scopes: e2e/playwright/anonymous-locale-toggle.spec.ts toggles en→es→en on the C# playground, which lives on a devTools-scoped route.
Docs: assets/i18n/README.md has a full "global bundle vs. lazily-loaded scopes" section — decision rubric, new-scope checklist (incl. registering scopes in the parity spec + test harness), and the explicit rationale for why games/admin/profile deliberately stay global.
Remaining namespaces in the global bundle are a documented, deliberate stopping point (cross-route sprawl / eager-consumer reuse) — see the README before re-litigating.
Done — closing after a full verification pass against the acceptance criteria.
**Delivered** (spikersoft-angular PRs #88, #89, #218):
- [x] **Global bundle materially smaller**: `en.json` 568,871 → 390,160 bytes (−31%), `es.json` 603,890 → 424,833 (−30%). Scopes carved out: `devTools` (172/202 KB) + `keycloakAdmin` (35/39 KB) — together ~34% of the old monolith.
- [x] **Largest surface split with its lazy route**: `devTools` (all dev-tools playgrounds) provided via `provideTranslocoScope("devTools")` on `/tools` (inherited by every tool child) and `/clue-for-sql`; `keycloakAdmin` on `/keycloak-admin`.
- [x] **Missing-key testing covers per-scope files**: `testing/transloco-test-setup.ts` merges scoped files back under their namespaces, and PR #218 adds `i18n-key-parity.spec.ts` — CI-enforced en↔es key parity for the global bundle and every scope, with a documented allowlist for the Spanish-overlay namespaces (`employment.*`, `gamesClueForSql.mysteries.*`) whose English source is backend seed data / in-code catalogs. It also swept the 6 genuinely stale es-only keys it flagged. CI green (test-and-lint, build, Sonar, e2e-smoke, e2e-anonymous).
- [x] **No language-switch regression across scopes**: `e2e/playwright/anonymous-locale-toggle.spec.ts` toggles en→es→en on the C# playground, which lives on a `devTools`-scoped route.
- [x] **Docs**: `assets/i18n/README.md` has a full "global bundle vs. lazily-loaded scopes" section — decision rubric, new-scope checklist (incl. registering scopes in the parity spec + test harness), and the explicit rationale for why `games`/`admin`/`profile` deliberately stay global.
Remaining namespaces in the global bundle are a documented, deliberate stopping point (cross-route sprawl / eager-consumer reuse) — see the README before re-litigating.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
UI strings ship as one monolithic Transloco file per language, HTTP-fetched at runtime:
projects/spikersoft/src/assets/i18n/en.json— 568,871 bytesprojects/spikersoft/src/assets/i18n/es.json— 603,890 bytesLoaded via
TranslocoHttpLoader(assets/i18n/${lang}.json). Every visitor downloads the entire ~600 KB blob for their language up front, and each release invalidates the hash so it re-downloads. The file only grows as features/languages are added — this is the frontend's clearest O(strings × languages) payload.Current state (measured)
@jsverse/transloco8.4.0 (supports scopes / lazy scoped translations natively).projects/spikersoft/src/main.ts: single loader,availableLangs,reRenderOnLangChange: true,provideTranslocoMissingHandler(LoggingMissingHandler).loadComponent) inprojects/spikersoft/src/routes.ts— but translations are not split to match./assets/**lazily, so repeat visits are cheap; the cost is first-load + per-release re-fetch of the whole blob.Proposal
Adopt Transloco scopes (
provideTranslocoScope) so each lazy feature loads only its own translation chunk, aligning i18n payload with the existing route code-splitting:playground,profile,blog,games,contact, …) intoassets/i18n/<scope>/<lang>.json, provided at the lazy route/component level.Why this is non-breaking
LoggingMissingHandler+ the test that asserts every key exists in every language give an immediate signal if a key is dropped during extraction.transloco.translate/ thetranslocopipe for global keys; scoped keys just gain a scope prefix.Acceptance criteria
playgroundscope (largest surface) split out and loaded with its lazy route.reRenderOnLangChangestill works across scopes).Priority
High among the frontend-scaling items — biggest, safest payoff. Do before the i18n files cross ~1 MB.
(Filed from an architecture review of content/translation delivery. Local-hosting only — no CDN.)
Done — closing after a full verification pass against the acceptance criteria.
Delivered (spikersoft-angular PRs #88, #89, #218):
en.json568,871 → 390,160 bytes (−31%),es.json603,890 → 424,833 (−30%). Scopes carved out:devTools(172/202 KB) +keycloakAdmin(35/39 KB) — together ~34% of the old monolith.devTools(all dev-tools playgrounds) provided viaprovideTranslocoScope("devTools")on/tools(inherited by every tool child) and/clue-for-sql;keycloakAdminon/keycloak-admin.testing/transloco-test-setup.tsmerges scoped files back under their namespaces, and PR #218 addsi18n-key-parity.spec.ts— CI-enforced en↔es key parity for the global bundle and every scope, with a documented allowlist for the Spanish-overlay namespaces (employment.*,gamesClueForSql.mysteries.*) whose English source is backend seed data / in-code catalogs. It also swept the 6 genuinely stale es-only keys it flagged. CI green (test-and-lint, build, Sonar, e2e-smoke, e2e-anonymous).e2e/playwright/anonymous-locale-toggle.spec.tstoggles en→es→en on the C# playground, which lives on adevTools-scoped route.assets/i18n/README.mdhas a full "global bundle vs. lazily-loaded scopes" section — decision rubric, new-scope checklist (incl. registering scopes in the parity spec + test harness), and the explicit rationale for whygames/admin/profiledeliberately stay global.Remaining namespaces in the global bundle are a documented, deliberate stopping point (cross-route sprawl / eager-consumer reuse) — see the README before re-litigating.