[Bug][Frontend] es→en language toggle leaves server-localized content in Spanish (lesson names et al.) — fetch-once surfaces never re-pull on locale change #611

Closed
opened 2026-07-16 06:13:25 +00:00 by spikerj · 1 comment
Owner

Symptom

After #608 shipped, toggling English→Spanish worked flawlessly, but toggling back left parts of the interface in Spanish — notably lesson names.

Root cause (traced end to end)

  • Backend exonerated: live prod probes with interleaved en/es/en/es/en catalog requests all localize correctly and identically — no shared-state mutation in the translation overlay.
  • Service worker exonerated for this symptom: the api-lessons-catalog dataGroup is freshness (network-first) and entries are keyed by full URL including contentLocale, so an en URL can never serve a Spanish body.
  • Playground runner already correct (#275's refetch effect) — verified with a Playwright toggle walk.
  • The real bug class: fetch-once surfaces. Server-localized content keeps its FETCH-TIME language on screen unless the surface re-pulls on toggle. Confirmed stale: My Journey (the lesson-name trail — zero refetches fired on toggle, reproduced empirically), the playground shell's Reading chapter (fetch keyed on category only), and the geography explorer (countries/facts loaded once at init). The reactive surfaces each hand-rolled the same langChanges$ edge-tracker idiom (4 copies); nothing enforced it for new surfaces, which is why this class keeps recurring (#275 → this).

Fix

spikersoft-angular PR #196:

  • New @spikersoft/platform-content-locale: ContentLocaleService.locale signal + onContentLocaleChange() edge-triggered hook — the one blessed invalidation idiom.
  • Fixed the three fetch-once surfaces; migrated the four hand-rolled copies (language-runner, sql-runner, x86, reg-ex) onto the shared helper.
  • Last-started-wins guards so a slow pre-toggle response can never restore stale-language content.
  • Unit specs at every layer (incl. previously-untested #275 runner behavior) + a permanent E2E round-trip spec (anonymous-locale-toggle.spec.ts) driving the real nav toggle both directions on the C# playground and My Journey. Full app suite 1873/1873.
  • Policy documented in the i18n workflow rule (both .cursor/.claude mirrors) and the lib README.

Close after PR #196 merges.

## Symptom After #608 shipped, toggling English→Spanish worked flawlessly, but toggling back left parts of the interface in Spanish — notably lesson names. ## Root cause (traced end to end) - **Backend exonerated:** live prod probes with interleaved `en/es/en/es/en` catalog requests all localize correctly and identically — no shared-state mutation in the translation overlay. - **Service worker exonerated for this symptom:** the `api-lessons-catalog` dataGroup is `freshness` (network-first) and entries are keyed by full URL including `contentLocale`, so an `en` URL can never serve a Spanish body. - **Playground runner already correct** (#275's refetch effect) — verified with a Playwright toggle walk. - **The real bug class: fetch-once surfaces.** Server-localized content keeps its FETCH-TIME language on screen unless the surface re-pulls on toggle. Confirmed stale: **My Journey** (the lesson-name trail — zero refetches fired on toggle, reproduced empirically), the playground shell's **Reading chapter** (fetch keyed on category only), and the **geography explorer** (countries/facts loaded once at init). The reactive surfaces each hand-rolled the same `langChanges$` edge-tracker idiom (4 copies); nothing enforced it for new surfaces, which is why this class keeps recurring (#275 → this). ## Fix spikersoft-angular PR #196: - New `@spikersoft/platform-content-locale`: `ContentLocaleService.locale` signal + `onContentLocaleChange()` edge-triggered hook — the one blessed invalidation idiom. - Fixed the three fetch-once surfaces; migrated the four hand-rolled copies (language-runner, sql-runner, x86, reg-ex) onto the shared helper. - Last-started-wins guards so a slow pre-toggle response can never restore stale-language content. - Unit specs at every layer (incl. previously-untested #275 runner behavior) + a permanent E2E round-trip spec (`anonymous-locale-toggle.spec.ts`) driving the real nav toggle both directions on the C# playground and My Journey. Full app suite 1873/1873. - Policy documented in the i18n workflow rule (both `.cursor`/`.claude` mirrors) and the lib README. Close after PR #196 merges.
Author
Owner

Resolved in spikersoft-angular PR #196 (merged to master). New @spikersoft/platform-content-locale primitive (ContentLocaleService signal + onContentLocaleChange edge hook); the three fetch-once surfaces (My Journey trail, playground Reading chapter, geography explorer) now re-pull on every language toggle; the four hand-rolled langChanges$ copies (language-runner, sql-runner, x86, reg-ex) migrated onto the shared helper; last-started-wins guards prevent a slow pre-toggle response from restoring stale-language content. Unit specs at every layer plus the permanent anonymous-locale-toggle.spec.ts e2e round-trip (failed before the fix, green after). Full app suite 1873/1873. Closing.

Resolved in spikersoft-angular PR #196 (merged to `master`). New `@spikersoft/platform-content-locale` primitive (`ContentLocaleService` signal + `onContentLocaleChange` edge hook); the three fetch-once surfaces (My Journey trail, playground Reading chapter, geography explorer) now re-pull on every language toggle; the four hand-rolled `langChanges$` copies (language-runner, sql-runner, x86, reg-ex) migrated onto the shared helper; last-started-wins guards prevent a slow pre-toggle response from restoring stale-language content. Unit specs at every layer plus the permanent `anonymous-locale-toggle.spec.ts` e2e round-trip (failed before the fix, green after). Full app suite 1873/1873. Closing.
Sign in to join this conversation.