[Bug][Frontend][Backend] C# playground lessons don't switch to Spanish on language toggle — In-Browser (WASM) path is English-only by construction #608

Closed
opened 2026-07-16 03:25:26 +00:00 by spikerj · 3 comments
Owner

Symptom

On a C# playground lesson, flipping the language toggle EN→ES switches the page chrome (and lesson title/description from the catalog) to Spanish, but the lesson body (instructions / hints / starter) stays English. Reproduces while signed in and online. Worked at one point.

Root cause

Spanish lesson content is a server-only overlay applied by CreateAttempt(rng, locale) reading CurriculumTranslationStore → embedded es/*.json. The C# playground defaults to In-Browser Compile (activePath()==="wasm"), whose local attempt generator (SpikerSoft.Wasm.WasmCompilerEntry.GetAttempt) called the English-only CreateAttempt(rng) — because SpikerSoft.Wasm link-included the curriculum but excluded *.Localization.cs + the translation store (the old wasm-link-include rule: "WASM always serves English; locale overlay happens on the API path").

CSharpRunnerService.getAttempt (libraries/features/dev-tools-csharp-runner/src/lib/csharp-runner.service.ts) fetches the localized SERVER attempt when online, but silently falls back to the English WASM-local attempt whenever the server response lacks browserGradingState or the fetch is skipped/fails — so even a signed-in user can land on English. The WASM offline path had no localization at all.

Fix — centralize translations into the WASM path (one source of truth, offline-capable)

CurriculumTranslationStore loads from embedded manifest resources only (System.Text.Json + reflection — no Mongo/DI/ILogger/filesystem), so it's WASM-safe. The exclusion was a choice, not a technical limit.

Backend (spikersoft-backend):

  • SpikerSoft.Wasm.csproj: embed Translations/es/*.json as <EmbeddedResource> with LogicalName="SpikerSoft.Business.Domain.Lessons.Translations.es.%(Filename)%(Extension)"; compile in CurriculumTranslationStore.cs + LessonStrategyBase.Localization.cs; drop the Exclude on Curriculum/**/*.Localization.cs.
  • WasmCompilerEntry: GetAttempt(int lessonNumber, string? locale) normalizes the locale and calls CreateAttempt(rng, locale); CurriculumTranslationStore.Initialize() runs once at registry build.
  • Rewrote the wasm-link-include rule (.cursor + .claude) + AGENTS.md: translations are now compiled into WASM; everything link-included must stay WASM-safe.

Frontend (spikersoft-angular):

  • WasmRuntimeService.getAttempt sources the active content locale from Transloco and forwards it to the worker → JSExport GetAttempt(lessonNumber, locale) (stale bundles ignore the extra arg → graceful English until the wasm artifact republishes).
  • LessonOfflineCacheService cache entries are tagged with locale; a locale mismatch on read is a miss (no stale-language attempt). Additive IDB field — no keyPath change / version bump.

Verification

  • Backend: WASM build 0/0; SpikerSoft.UnitTests.slnf build clean; CurriculumLocaleTests 43/43; embedded resources confirmed under the exact manifest prefix (8/8 tracks).
  • Frontend: feature-dev-tools-csharp-runner 35/35 (new test asserts the active locale is forwarded), platform-progress-sync 27/27; both lint clean.
  • Remaining: in-browser smoke check (toggle EN→ES on a signed-in C# lesson, online + offline) after the wasm artifact republishes.

PRs: spikersoft-backend + spikersoft-angular (linked below).

## Symptom On a C# playground lesson, flipping the language toggle EN→ES switches the page chrome (and lesson title/description from the catalog) to Spanish, but the **lesson body (instructions / hints / starter)** stays English. Reproduces while **signed in and online**. Worked at one point. ## Root cause Spanish lesson content is a server-only overlay applied by `CreateAttempt(rng, locale)` reading `CurriculumTranslationStore` → embedded `es/*.json`. The C# playground defaults to **In-Browser Compile** (`activePath()==="wasm"`), whose local attempt generator (`SpikerSoft.Wasm.WasmCompilerEntry.GetAttempt`) called the **English-only** `CreateAttempt(rng)` — because `SpikerSoft.Wasm` link-included the curriculum but **excluded** `*.Localization.cs` + the translation store (the old `wasm-link-include` rule: "WASM always serves English; locale overlay happens on the API path"). `CSharpRunnerService.getAttempt` (`libraries/features/dev-tools-csharp-runner/src/lib/csharp-runner.service.ts`) fetches the localized SERVER attempt when online, but **silently falls back** to the English WASM-local attempt whenever the server response lacks `browserGradingState` or the fetch is skipped/fails — so even a signed-in user can land on English. The WASM offline path had **no** localization at all. ## Fix — centralize translations into the WASM path (one source of truth, offline-capable) `CurriculumTranslationStore` loads from **embedded manifest resources** only (System.Text.Json + reflection — no Mongo/DI/ILogger/filesystem), so it's WASM-safe. The exclusion was a choice, not a technical limit. **Backend (`spikersoft-backend`):** - `SpikerSoft.Wasm.csproj`: embed `Translations/es/*.json` as `<EmbeddedResource>` with `LogicalName="SpikerSoft.Business.Domain.Lessons.Translations.es.%(Filename)%(Extension)"`; compile in `CurriculumTranslationStore.cs` + `LessonStrategyBase.Localization.cs`; drop the `Exclude` on `Curriculum/**/*.Localization.cs`. - `WasmCompilerEntry`: `GetAttempt(int lessonNumber, string? locale)` normalizes the locale and calls `CreateAttempt(rng, locale)`; `CurriculumTranslationStore.Initialize()` runs once at registry build. - Rewrote the `wasm-link-include` rule (`.cursor` + `.claude`) + `AGENTS.md`: translations are now compiled into WASM; everything link-included must stay WASM-safe. **Frontend (`spikersoft-angular`):** - `WasmRuntimeService.getAttempt` sources the active content locale from Transloco and forwards it to the worker → JSExport `GetAttempt(lessonNumber, locale)` (stale bundles ignore the extra arg → graceful English until the wasm artifact republishes). - `LessonOfflineCacheService` cache entries are tagged with locale; a locale mismatch on read is a miss (no stale-language attempt). Additive IDB field — no keyPath change / version bump. ## Verification - Backend: WASM build 0/0; `SpikerSoft.UnitTests.slnf` build clean; `CurriculumLocaleTests` 43/43; embedded resources confirmed under the exact manifest prefix (8/8 tracks). - Frontend: `feature-dev-tools-csharp-runner` 35/35 (new test asserts the active locale is forwarded), `platform-progress-sync` 27/27; both lint clean. - Remaining: in-browser smoke check (toggle EN→ES on a signed-in C# lesson, online + offline) after the wasm artifact republishes. PRs: spikersoft-backend + spikersoft-angular (linked below).
Author
Owner

PRs opened (merge/deploy together):

  • Backend: spikersoft-backend #313 (feat/wasm-curriculum-i18n) — embeds es/*.json + the locale overlay into SpikerSoft.Wasm, GetAttempt(lessonNumber, locale), reverses the wasm-link-include rule.
  • Frontend: spikersoft-angular #193 (feat/wasm-curriculum-i18n) — WasmRuntimeService forwards the active Transloco locale to the WASM GetAttempt; locale-aware offline cache.

Leaving open until both merge and the in-browser smoke check (toggle EN→ES on a signed-in C# lesson, online + offline) is confirmed after the WASM artifact republishes.

PRs opened (merge/deploy together): - Backend: spikersoft-backend #313 (`feat/wasm-curriculum-i18n`) — embeds `es/*.json` + the locale overlay into `SpikerSoft.Wasm`, `GetAttempt(lessonNumber, locale)`, reverses the `wasm-link-include` rule. - Frontend: spikersoft-angular #193 (`feat/wasm-curriculum-i18n`) — `WasmRuntimeService` forwards the active Transloco locale to the WASM `GetAttempt`; locale-aware offline cache. Leaving open until both merge and the in-browser smoke check (toggle EN→ES on a signed-in C# lesson, online + offline) is confirmed after the WASM artifact republishes.
Author
Owner

Second root cause found — the online/signed-in symptom was NOT the WASM path

The merged WASM work (#313 / frontend) localizes the offline in-browser attempt starter. But the symptom actually reported — signed-in, online, C# lesson titles/descriptions AND tutorial bodies stay English despite the Spanish toggle — is a separate server-side bug, and this issue's original writeup mis-assumed the catalog title/description already switch (they don't).

Proven end-to-end (not inferred)

  • Live prod curl GET /api/Lessons?language=C%23&contentLocale=es"title":"Welcome to SpikerSoft", byte-identical to the English control. The frontend is sending contentLocale=es; the deployed server returns English. → not a render/frontend bug.
  • Deployed master ships the Spanish dataes/*.json EmbeddedResource (in the SpikerSoft.Business.CodeExecution slice) on master since 2026-05-20; DLL contains "Bienvenido", 8/8 tracks. → not a missing-resource bug.
  • spikersoft-infrastructure/spikersoft-backend/docker-stack.yml sets Seeding__RunOnStartup=false on the API service (seeding moved to a one-shot spikersoft-bootstrap stack, per #131).

Root cause

CurriculumTranslationStore.Initialize() — the in-memory load of the es/*.json overlay that BOTH the GetAllLessons/GetLessonById title overlay AND the CreateAttempt tutorial/attempt overlay read at request time — was only called inside StartupSeedRunner.RunAsync, which is gated by Seeding:RunOnStartup. With the gate off on API replicas, the serving process never populates the overlay, so every .For(Es) lookup silently falls back to English. Translation loading is not DB seeding and must run on every replica.

Fix — PR spikersoft-backend#316

  • CurriculumTranslationStore.Instance lazily self-initializes on first access (thread-safe, immutable once built) — empty overlay can never be observed.
  • Program.cs calls Initialize() unconditionally at startup, decoupled from Seeding:RunOnStartup.
  • StartupSeedRunner drops the coupled call; regression test reproduces the prod condition (no Initialize()) and asserts Spanish is still served.
  • Verified: WASM + SpikerSoft.UnitTests.slnf build clean; 4442 Lessons tests pass.

Prod resolution is pending deploy — the same curl returning "Bienvenido a SpikerSoft" after #316 merges + deploys is the proof. This issue should be closed once #316 is merged.

## Second root cause found — the online/signed-in symptom was NOT the WASM path The merged WASM work (#313 / frontend) localizes the **offline in-browser attempt starter**. But the symptom actually reported — signed-in, **online**, C# lesson **titles/descriptions AND tutorial bodies stay English** despite the Spanish toggle — is a **separate server-side bug**, and this issue's original writeup mis-assumed the catalog title/description already switch (they don't). ### Proven end-to-end (not inferred) - **Live prod curl** `GET /api/Lessons?language=C%23&contentLocale=es` → `"title":"Welcome to SpikerSoft"`, byte-identical to the English control. The frontend *is* sending `contentLocale=es`; the deployed **server** returns English. → not a render/frontend bug. - **Deployed master ships the Spanish data** — `es/*.json` `EmbeddedResource` (in the `SpikerSoft.Business.CodeExecution` slice) on master since 2026-05-20; DLL contains "Bienvenido", 8/8 tracks. → not a missing-resource bug. - **`spikersoft-infrastructure/spikersoft-backend/docker-stack.yml` sets `Seeding__RunOnStartup=false`** on the API service (seeding moved to a one-shot `spikersoft-bootstrap` stack, per #131). ### Root cause `CurriculumTranslationStore.Initialize()` — the in-memory load of the `es/*.json` overlay that BOTH the `GetAllLessons`/`GetLessonById` title overlay AND the `CreateAttempt` tutorial/attempt overlay read at request time — was only called **inside `StartupSeedRunner.RunAsync`**, which is gated by `Seeding:RunOnStartup`. With the gate off on API replicas, the serving process never populates the overlay, so every `.For(Es)` lookup silently falls back to English. Translation loading is not DB seeding and must run on every replica. ### Fix — PR spikersoft-backend#316 - `CurriculumTranslationStore.Instance` lazily self-initializes on first access (thread-safe, immutable once built) — empty overlay can never be observed. - `Program.cs` calls `Initialize()` unconditionally at startup, decoupled from `Seeding:RunOnStartup`. - `StartupSeedRunner` drops the coupled call; regression test reproduces the prod condition (no `Initialize()`) and asserts Spanish is still served. - Verified: WASM + `SpikerSoft.UnitTests.slnf` build clean; 4442 Lessons tests pass. **Prod resolution is pending deploy** — the same curl returning "Bienvenido a SpikerSoft" after #316 merges + deploys is the proof. This issue should be closed once #316 is merged.
Author
Owner

Resolved — verified in production

spikersoft-backend#316 merged (commit a30ea3b) and deployed. After the swarm rolling update landed (~8.5 min post-merge), the same prod curl that previously returned English now returns Spanish:

GET /api/Lessons?language=C%23&contentLocale=es
  1: Bienvenido a SpikerSoft | Tu punto de partida: cómo funcionan las lecciones...
  2: ¿Qué es C#? | Conoce C# — para qué se usa, cómo se ve el código...

GET /api/Lessons?language=C%23   (English control, unchanged)
  1: Welcome to SpikerSoft
  2: What is C#?

Titles + descriptions localize, and because the tutorial-panel/attempt overlay reads the same CurriculumTranslationStore.Instance, the full Spanish surface (tutorial bodies included) recovers with it.

Both root causes are now fixed and shipped:

  1. WASM/offline attempt starter — #313 (backend) + angular#193.
  2. Server overlay never initialized on non-seeding API replicas — #316 (this fix).

Closing. Follow-up (unrelated): NG0951 console spam filed as #609.

## Resolved — verified in production ✅ `spikersoft-backend`#316 merged (commit `a30ea3b`) and deployed. After the swarm rolling update landed (~8.5 min post-merge), the same prod curl that previously returned English now returns Spanish: ``` GET /api/Lessons?language=C%23&contentLocale=es 1: Bienvenido a SpikerSoft | Tu punto de partida: cómo funcionan las lecciones... 2: ¿Qué es C#? | Conoce C# — para qué se usa, cómo se ve el código... GET /api/Lessons?language=C%23 (English control, unchanged) 1: Welcome to SpikerSoft 2: What is C#? ``` Titles + descriptions localize, and because the tutorial-panel/attempt overlay reads the same `CurriculumTranslationStore.Instance`, the full Spanish surface (tutorial bodies included) recovers with it. Both root causes are now fixed and shipped: 1. WASM/offline attempt starter — #313 (backend) + angular#193. 2. Server overlay never initialized on non-seeding API replicas — #316 (this fix). Closing. Follow-up (unrelated): NG0951 console spam filed as #609.
Sign in to join this conversation.