Repository navigation
chore(deps): upgrade i18n stack (remix-i18next 7.5.0, i18next 26, react-i18next 17) - #404
Conversation
…ct-i18next 17) Bumps the three i18n packages together, because none of them can move alone: - remix-i18next 7.4.2 -> 7.5.0 - i18next 25.8.7 -> 26.4.2 - react-i18next 15.7.4 -> 17.0.13 remix-i18next@7.4.2 pins `i18next: ^24 || ^25` and `react-i18next: ^13..^16`, while react-i18next@17 requires `i18next >= 26.0.10`. 7.5.0 is a peer-range-only release that widens both (`i18next: ^24 || ^25 || ^26`, `react-i18next: ^13..^17`) and still requires only `react-router ^7`, so the upgrade lands without React Router 8. Deliberately stays on remix-i18next 7.x: 8.0.0 requires react-router ^8. No source changes needed. The documented breaking changes do not apply: - i18next 26 removes `initImmediate`, the legacy `interpolation.format` function, `showSupportNotice` and `simplifyPluralSuffix` — none are used anywhere in this repo. - react-i18next 16.0.0 was a peer bump only. - react-i18next 17.0.0's single breaking change is `Trans` auto-generated key serialization; this app has zero `Trans` usage. `i18next-browser-languagedetector` declares no peer dependencies and is unaffected, so it stays at ^8.0.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The client i18next instance was initialised with only the locale the
document had been server-rendered with, plus German as the fallback:
const locale = document.documentElement.lang || "de";
resources = { [locale]: …, de: … }
The language switcher navigates client-side, so useChangeLanguage moves
i18next to the new locale without a reload. With no resource bundle for
that locale, every t() call resolves through fallbackLng: "de" instead.
The result is German UI on an English page, while CMS content — which
comes from the route loader, not i18next — is correctly translated.
Reproduced on production: land on /de/ich-suche-redezeit, switch to EN,
open the coach search, and the filter chrome reads "Häufige Themen" /
"Weitere Filter" / "Sprachen" under `<html lang="en">`. A hard reload
fixes it, because hydrate() then sees lang="en" and loads that bundle.
Only breaks away from German: de -> en/ru/uk all fail, while en -> de
works because German is loaded anyway as the fallback.
Load all four bundles instead. They are ~1.2 kB gzipped each, so this
costs ~2.6 kB over the previous locale-plus-fallback pair — cheaper than
importing on languageChanged, which would flash German while the import
resolves.
Verified against the dev server across every direction away from German:
de -> en gives "Popular topics"/"More filters"/"listeners found",
de -> ru gives "Популярные темы"/"Дополнительные фильтры",
de -> uk gives "Популярні теми"/"Додаткові фільтри",
each with zero German leakage, and en -> de still renders German.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Added: fix for client-side language switchingPushed SymptomLand on a German page, switch to English via the language switcher, and the UI chrome stays German — Cause
const locale = document.documentElement.lang || "de";
resources = { [locale]: …, de: … }The switcher navigates client-side, so It only breaks away from German — FixLoad all four bundles at hydrate. They are ~1.2 kB gzipped each, so ~2.6 kB more than the previous locale-plus-fallback pair. Importing lazily on VerificationEvery direction away from German, via the real switcher with client-side navigation:
Zero German leakage in all three, Worth noting: this bug was invisible to the whole dependency-queue verification regime, which only ever navigated directly to 🤖 Generated with Claude Code |
Upgrades the three i18n packages together. None of them can move on its own.
remix-i18nexti18nextreact-i18nextWhy all three must move together
Dependabot's #379 tries to bump
react-i18nextalone to 17.0.7. That PR is uninstallable, because of two peer walls:remix-i18next@7.4.2capsreact-i18nextat^13 || ^14 || ^15 || ^16— soreact-i18next@17cannot be installed alongside it at all.react-i18next@17requiresi18next >= 26.0.10, butremix-i18next@7.4.2capsi18nextat^24 || ^25.remix-i18next@7.5.0is a peer-range-only release (#458) that widens both sides:Because it still requires only
react-router ^7, the whole i18n stack can move now without waiting on React Router 8.Deliberately staying on React Router 7
This does not bump
remix-i18nextto 8.0.0.remix-i18next@8requiresreact-router ^8, moves all exports to the package root (remix-i18next/middleware→remix-i18next), and changes thefindLocalecallback signature — none of which can land while the React Router 8 upgrade is blocked. 7.5.0 is the version that unblocks the i18n stack on RR7.Breaking changes checked
No source changes were needed. Every documented breaking change was checked against this repo:
i18next 26.0.0 removes:
initImmediate, the legacy monolithicinterpolation.formatfunction, theshowSupportNoticeconsole notice, andsimplifyPluralSuffix.grepacrossapp/,public/locales/,scripts/and the root config files finds zero uses of any of them.app/entry.client.tsxandapp/middleware/i18next.tsare the only places that callinit()/createI18nextMiddleware, and neither passes any removed option.react-i18next 16.0.0 was a peer-dependency bump only (no API change).
react-i18next 17.0.0 has exactly one potentially-breaking change:
transKeepBasicHtmlNodesFornow preserves HTML tag names in auto-generated<Trans>keys. This repo has zeroTrans/IcuTransusage — the whole surface in use isuseTranslation(11 files),I18nextProvider(app/entry.client.tsx,app/entry.server.tsx),initReactI18next(app/entry.client.tsx), anduseChangeLanguagefromremix-i18next/react(app/root.tsx). None of those changed in 16 or 17.i18next-browser-languagedetector(used inapp/entry.client.tsx) declares no peer dependencies at all, so nothing forces it to move. It stays at^8.0.0.The resulting lockfile change is minimal — 3 direct bumps plus
html-parse-stringify 3.0.1 → 4.0.1and@babel/runtime 7.28.4 → 7.29.7,use-sync-external-store 1.6.0added,void-elementsdropped.npm auditis unchanged at 10 findings, all in the pre-existingreact-router/express/vitechain, none i18n-related.Verification
npm installsucceeded with no--forceand no--legacy-peer-deps.npm cinpm run typechecknpm run buildnpx biome check app scripts types *.ts *.js *.jsonnpx vitest --run --dir appBuild artifacts were verified after deleting
build/and.netlify/v1/functions/first, since the Netlify tooling can fail silently while exiting 0:.netlify/v1/functions/react-router-server.mjs— emittedbuild/server/server.js— emittedBrowser smoke (dev server)
Since this is the i18n stack, the smoke test focused on translation resolution.
/de— German homepage, "Wir hören Dir zu!", fully translated./de/ich-suche-redezeit— coach search renders, 365 coaches,<html lang="de" dir="ltr">./en/blog— "The latest blog posts", English UI strings ("Continue reading...")./uk—lang="uk", Ukrainian content ("Ласкаво просимо…", "Перейти до вмісту")./ru—lang="ru", Russian content ("Добро пожаловать…", "Перейти к содержимому").No raw i18n keys leaked. Every
t()key used in the codebase (filter.gender,filter.language,filter.moreFilters,filter.popularTags,filter.showFilter,filter.submitCta,filter.tag,title.network,title.partner,title.media,machineMessage,skipToContent,ctaToBlogpost,noResult, …) was searched for as a literal string in the rendered body text of each locale — zero matches on all of/de,/de/ich-suche-redezeit,/ukand/ru.Coach language filter exercised, counts change correctly (confirmed both by clicking the radios in the hydrated client and by SSR response):
?lang=de?lang=en?lang=uk?lang=ru?gender=weiblichThe filter's own language chips (
Deutsch/English/Polskie/русский/Український) and the per-coach language labels render as translated text throughout.Server logs: no errors. Console: only the runbook's documented benign Vite dep-optimizer burst right after a fresh
npm ci(504 Outdated Optimize Dep+ one failed dynamic import ofentry.client.tsx). Confirmed benign per the documented procedure — the error list stayed frozen across three further navigations and a fresh network capture showed 200 OK on every request with a stable dep hash and zero 504s.Conflicts with the React Router 8 work
remix-i18next@8.0.0(and the import/findLocalemigration that comes with it). Both touchpackage.json/package-lock.jsonand theremix-i18nextmajor. Whichever merges second will need a rebase.Refs #379 — that PR stays open and is superseded by this one; it cannot be installed on its own.
🤖 Generated with Claude Code