Skip to content

feat(i18n): mount the provider and surface the language switcher (#159) - #360

Open
devrim-1283 wants to merge 1 commit into
AudioBitsStellar:mainfrom
devrim-1283:feat/wire-i18n-provider
Open

feat(i18n): mount the provider and surface the language switcher (#159)#360
devrim-1283 wants to merge 1 commit into
AudioBitsStellar:mainfrom
devrim-1283:feat/wire-i18n-provider

Conversation

@devrim-1283

Copy link
Copy Markdown

Closes #159.

The i18n work was all present and none of it reachable. I18nContext, the en/es locale tables and LanguageSwitcher existed — LanguageSwitcher even carries a closes #159 comment — but:

$ grep -rn "I18nProvider" src/ --include=*.tsx | grep -v context/I18nContext
(no matches)
$ grep -rn "LanguageSwitcher" src/ --include=*.tsx | grep -v components/LanguageSwitcher
(no matches)

Nothing mounted the provider and nothing rendered the switcher. No user could change language, and useI18n() would have thrown anywhere it was tried.

Wiring

  • I18nProvider wraps the tree in context/provider.tsx, inside QueryClientProvider and around StellarNetworkProvider.
  • LanguageSwitcher renders in TopHeader, next to the role badge.

Two defects found while wiring it

1. Prototype-chain lookup. readPersistedLocale used stored in locales, and in walks the prototype chain:

localStorage.setItem("audioblocks_locale", "constructor");
// "constructor" in locales  ->  true
// locales["constructor"]    ->  Object.prototype.constructor, a function

So t became a function instead of a translation table and every label in the UI rendered undefined. Now Object.hasOwn. setLocale gained the same validation, so an unsupported value cannot be written in the first place.

2. Unguarded storage access. Safari private mode and blocked site data both throw on localStorage, which would have taken the whole provider down on mount. Both calls are now wrapped.

Keeping existing tests working

23 existing tests render TopHeader without a provider. Rather than rewrite them, useOptionalI18n() was added for components that are i18n-aware rather than i18n-dependent. LanguageSwitcher uses it and renders null when there is no provider — it has nothing to switch. useI18n() still throws, so a component that genuinely needs translations fails loudly.

Tests

15 new cases: default locale, persistence and restore, an unsupported locale ignored, the four inherited-property values rejected, a localStorage that throws on read and on write, useI18n outside a provider, the switcher listing and changing locales, and a parity check that en and es define exactly the same keys with no empty strings.

Verified the tests catch the bug: restoring in turns 4 of them red.

No regressions

Tests
before 26 failed, 381 passed
after 26 failed, 396 passed

Same 26 failures, and the delta is the 15 new tests. tsc --noEmit reports the same 181 pre-existing errors. The one eslint error in I18nContext.tsx (react-hooks/set-state-in-effect) is pre-existing too — it is the SSR-safe mount effect, unchanged here.

Reproducing locally needs npm install --legacy-peer-deps.

Closes AudioBitsStellar#159.

The i18n work was all present and none of it reachable. I18nContext, the en/es
locale tables and LanguageSwitcher existed — LanguageSwitcher even carried a
"closes AudioBitsStellar#159" comment — but:

  $ grep -rn "I18nProvider" src/ --include=*.tsx | grep -v context/I18nContext
  (no matches)
  $ grep -rn "LanguageSwitcher" src/ --include=*.tsx | grep -v components/LanguageSwitcher
  (no matches)

Nothing mounted the provider and nothing rendered the switcher, so no user
could change language and useI18n() would have thrown anywhere it was tried.

- I18nProvider now wraps the tree in context/provider.tsx, inside
  QueryClientProvider and around StellarNetworkProvider.
- LanguageSwitcher renders in TopHeader, next to the role badge.

Two defects fixed while wiring it up:

readPersistedLocale used `stored in locales`, and `in` walks the prototype
chain. A stored value of "constructor", "toString", "valueOf" or
"hasOwnProperty" therefore passed the check and indexed `locales` to a function
instead of a translation table, so every label in the UI rendered undefined.
Now Object.hasOwn. setLocale gained the same validation, so an unsupported
locale cannot be written in the first place.

Both localStorage calls are wrapped: Safari private mode and blocked site data
throw on access, which would have taken the whole provider down on mount.

useOptionalI18n() was added for components that are i18n-aware rather than
i18n-dependent. LanguageSwitcher uses it and renders null with no provider —
it has nothing to switch — which keeps the 23 existing TopHeader tests that
render without one working. useI18n() still throws, so a component that truly
needs translations fails loudly.

Tests: 15 new cases — default locale, persistence and restore, an unsupported
locale ignored, the four inherited-property values rejected, a localStorage
that throws on read and on write, useI18n outside a provider, the switcher
listing and changing locales, and a parity check that en and es define exactly
the same keys with no empty strings.

Verified the tests catch the prototype bug: restoring `in` turns 4 of them red.

No regressions: 26 failing tests before and after (381 -> 396 passing, the 15
new ones). `tsc --noEmit` reports the same 181 pre-existing errors. The one
eslint error in I18nContext.tsx (react-hooks/set-state-in-effect) is also
pre-existing — it is the SSR-safe mount effect, unchanged by this PR.
@vercel

vercel Bot commented Aug 30, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the akatenvictor's projects Team on Vercel.

A member of the Team first needs to authorize it.

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.

Add internationalization (i18n) support for multi-language UI

1 participant