Skip to content

feat: centralize locale detection and add web↔docs cookie redirect#1690

Merged
yannickmonney merged 3 commits into
mainfrom
feat/centralize-locale-detection
May 8, 2026
Merged

feat: centralize locale detection and add web↔docs cookie redirect#1690
yannickmonney merged 3 commits into
mainfrom
feat/centralize-locale-detection

Conversation

@yannickmonney

@yannickmonney yannickmonney commented May 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • New framework-free @tale/i18n package owns locale primitives, cookie helpers, and the path-locale negotiation used by the web/docs Bun servers. New @tale/ui/i18n/* (moved here from @tale/webui so platform can use it) owns the React/i18next glue, including a single LocaleSync component now responsible for i18n.changeLanguage + <html lang>.
  • Web and docs gain server-side locale detection: a first-time visitor whose Accept-Language is de or fr (and who hits an unprefixed path) is 302-redirected to /de / /fr and a tale_locale cookie is set. Cookie domain is configurable via LOCALE_COOKIE_DOMAIN (set to .tale.dev in prod so tale.dev and docs.tale.dev share the preference). Locale-neutral paths (/api/*, /sitemap.xml, /robots.txt, /llms*.txt, *.md) skip negotiation.
  • The three services' lib/i18n/ folders are now identical thin re-export shims (client.tsx, config.ts, i18n-provider.tsx, i18n.ts, keys-dynamic.txt, locales.ts, messages.test.ts, messages-usage.test.ts, types.ts; web/docs additionally have use-current-locale.ts). The plop react-service template matches.

Behavior changes worth flagging

  • Marketing/docs visitors with German/French browsers now get redirected on first visit instead of seeing English silently. The tale_locale cookie persists the choice across subdomains.
  • Docs Dockerfile now bundles server.tsserver.js (mirroring web) so the @tale/i18n workspace import resolves at runtime.
  • services/web/lib/i18n/localized-paths.ts removed; localizedHreflocalizedPath from @tale/ui/i18n/locales; the route table moved inline into the only consumer (LocalizedLink).
  • Legacy unused notFound: { … } namespace removed from docs's en/de/fr bundles (the active keys are notFoundTitle etc. inside the docs namespace).

Things that stayed put

  • Platform's useLocale (localStorage + dayjs loader) is platform-specific business logic — kept where it is, but trimmed: it no longer calls i18n.changeLanguage, since LocaleSync owns that.
  • Agent-locale resolution (resolve-agent-locale, get-organization-default-locale) is platform-specific (selects locale-keyed agent metadata fields, not UI translation) — stays in platform.

Test plan

  • Ran bun run check (format, lint, typecheck, all tests) — 38/38 turbo tasks green: 45 i18n tests, 3025 platform tests, 26 docs tests.
  • Updated services/platform/messages/{en,de,fr}.json — N/A (no platform copy changes; platform i18n provider was rewired but message bundles unchanged).
  • Updated /docs/{en,de,fr}/ for every user-visible change — N/A (no doc-content changes; the redirect is purely server-side and behavior-driven).
  • Ran bun run --filter @tale/docs lint and bun run --filter @tale/docs test — green.
  • Updated README.md, README.de.md, README.fr.md — N/A (no top-level repo behavior change visible to a reader of those READMEs).

Manual smoke (web dev server, port 3781):

  • curl -i -H 'Accept-Language: de-DE,de;q=0.9' http://localhost:3781/302 Location: /de + Set-Cookie: tale_locale=de.
  • curl -i -H 'Cookie: tale_locale=fr' http://localhost:3781/pricing302 Location: /fr/pricing.
  • curl -i -H 'Accept-Language: en-US,en;q=0.9' http://localhost:3781/200 + Set-Cookie: tale_locale=en.
  • curl -i http://localhost:3781/de/pricing200 + Set-Cookie: tale_locale=de (cookie refresh).
  • curl -i http://localhost:3781/de with cookie already de200 + Vary only, no Set-Cookie (no rewrite).
  • curl -i http://localhost:3781/sitemap.xml and /api/health → no Set-Cookie, no Vary (skip).

Outstanding manual checks

  • Staging deploy with LOCALE_COOKIE_DOMAIN=.tale.dev baked into both Dockerfiles; visit https://tale.dev/de and confirm DevTools → Application → Cookies shows Domain=.tale.dev; then visit https://docs.tale.dev/ and confirm 302 → /de.
  • Platform: open the app, change language via the existing UI, confirm localStorage.user-locale updates and <html lang> flips (<html lang> is new for platform — previously not set).

Reviewer notes

  • CodeRabbit review run on the working tree; meaningful findings applied (URIError guard around decodeURIComponent in both servers; explicit strict: true in packages/i18n/tsconfig.json; case-insensitive .md skip; doc-comment fix in resolve-locale.ts). Skipped findings that would change semantics (?? for LOCALE_COOKIE_DOMAIN would break the empty-string env-var contract) or were YAGNI (cookie library swap, encodeURIComponent of constrained 'en'|'de'|'fr' values, REGIONAL_FALLBACKS table).
  • Two pre-existing typecheck errors on main (in services/docs/app/{entry-server,router}.tsx around import.meta.env.BASE_URL) and four pre-existing oxfmt drifts (services/docs/{app/entry-server.tsx,app/router.tsx,index.html} and services/web/app/components/layout/site-header.tsx) were fixed along the way to keep bun run check green.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added internationalization infrastructure with support for English, German, and French, plus regional language variants.
    • Implemented automatic locale detection based on browser language preferences and user-set language choices.
    • Added persistent language preference storage via cookies with automatic synchronization.
  • Refactoring

    • Consolidated locale and language utilities into a shared package for consistent behavior across all services.
    • Updated all services to use centralized language and locale management.
  • Chores

    • Updated Docker configurations to support locale cookie domain customization.
    • Added dependency declarations for the new shared i18n package across services.

Three services implemented i18n with overlapping but inconsistent logic:
web/docs used path-based locales but never redirected new visitors based
on Accept-Language and never set a cookie; platform parsed Accept-Language
with utilities only it could use; the React-side hook, provider, and
i18next bootstrap were duplicated across web/docs/platform.

This consolidates everything behind two shared packages:

- new packages/i18n/ — framework-free locale primitives used by every
  service and by the web/docs Bun servers: SUPPORTED_LOCALES,
  isUrlPrefixedLocale/isCookieLocale, parseAcceptLanguage, resolveLocale,
  isValidLocale, narrowBcp47, localizedPath, cookie helpers
  (LOCALE_COOKIE_NAME='tale_locale', serializeLocaleCookie,
  readLocaleCookie), and negotiatePathLocale (the redirect decision tree).

- @tale/ui/i18n/* — React/i18next glue used by all three services:
  defaultLocale, detectInitialLocale, resolveRegionalLocale, useT,
  useCurrentLocale, I18nProvider, initI18n, collectRegionalBundles, and
  LocaleSync (single owner of i18n.changeLanguage + <html lang>). The
  React i18n previously lived in @tale/webui/i18n/* but moved to
  @tale/ui/i18n/* so platform — which doesn't depend on @tale/webui —
  could use it.

Behavior changes for the marketing/docs sites:

- a German visitor hitting / now gets a 302 to /de and a tale_locale=de
  cookie (Domain=.tale.dev in prod via LOCALE_COOKIE_DOMAIN, unset in
  dev). On the next visit they land on /de directly without re-detection.
  Cross-subdomain cookie sharing means tale.dev and docs.tale.dev honor
  the same preference.
- locale-neutral paths (/api/*, /sitemap.xml, /robots.txt, /llms*.txt,
  *.md) skip negotiation entirely.
- docs Dockerfile now bundles server.ts → server.js so the @tale/i18n
  import resolves at runtime; mirrors what web already did.

Per-service lib/i18n/ folders are now identical (same file shape across
web, docs, platform, plus the plop react-service template). Platform
adopts the shared useT/I18nProvider/initI18n; useLocale stays
platform-specific (localStorage + dayjs) but no longer calls
i18n.changeLanguage directly — LocaleSync owns that.

Cleanup along the way:

- moved platform's parse-accept-language/resolve-locale/is-valid-locale
  into @tale/i18n; deleted the originals.
- moved narrow-bcp47 from platform into @tale/i18n.
- SUPPORTED_AGENT_LOCALES re-exports SUPPORTED_LOCALES.
- removed services/web/lib/i18n/localized-paths.ts: localizedHref → the
  shared localizedPath, and the route table moved inline into
  LocalizedLink (its only consumer).
- removed legacy unused notFound: { ... } namespace from docs's en/de/fr
  bundles.
- docs gained the shared defineMessagesParityTests +
  defineMessagesUsageTests (web and platform already had them).

Verification: bun run check — 38/38 turbo tasks green
(45 i18n tests, 3025 platform tests, 26 docs tests, all
linting/typechecking/format). Smoke-tested locally with curl on the web
server: DE Accept-Language → 302 /de + cookie; FR cookie + /pricing →
302 /fr/pricing; EN visitor → 200 + en cookie; /sitemap.xml and
/api/health skip negotiation.

CodeRabbit review applied: URIError guard around decodeURIComponent in
both servers; explicit strict: true in packages/i18n/tsconfig.json;
case-insensitive .md skip; resolve-locale doc comment matches
implementation.
The platform Dockerfile didn't COPY packages/i18n into its workspace
stage, so `bun install` in CI failed with "Workspace not found
packages/i18n" and broke Build platform / Smoke test / Validate images.
Web and docs were already updated; mirror the same change here.

Knip flagged four pure-shim files that no service code actually imports:

- services/web/lib/i18n/config.ts
- services/docs/lib/i18n/config.ts
- services/docs/lib/i18n/use-current-locale.ts
- services/platform/lib/i18n/locales.ts

These were added in the prior commit "for parity" with platform's
config.ts (which IS used by 3 consumers). Deleting them keeps each
service's lib/i18n/ honest about what it actually consumes — web/docs
still have locales.ts + use-current-locale.ts (URL-based), platform
still has config.ts (consumed by number formatter, agents config, org
queries). Plop template trimmed to the universal subset.

Trim re-exports in lib/i18n/locales.ts to drop unused symbols
(localizedPath/Locale/RegionalLocale/UrlPrefixedLocale in docs;
ALL_LOCALES/Locale/RegionalLocale/UrlPrefixedLocale in web).

Drop unused dependencies surfaced by knip:
- root: react-i18next (lives in @tale/ui now)
- platform: i18next-icu, intl-messageformat (only used by @tale/ui's
  initI18n)
@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR establishes a shared, standardized i18n infrastructure across the Tale monorepo by creating a new @tale/i18n package, extending @tale/ui with i18n modules, and migrating three services (web, docs, platform) to consume the shared implementation. The changes include: new locale type definitions and negotiation logic in @tale/i18n; client-side helpers, regional bundle collection, and a LocaleSync component in @tale/ui; removal of duplicated i18n exports from @tale/webui; refactoring of i18n provider wiring across all services to use declarative locale synchronization; server-side integration of locale negotiation and cookie handling in Bun servers; and updates to Dockerfiles, templates, and test utilities to support the new structure.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • tale-project/tale#1673: Both extract and standardize shared i18n surface (@tale/ui/i18n and @tale/ui/i18n-tests), with this PR adding the core @tale/i18n package and the retrieved PR adding i18n test utilities.
  • tale-project/tale#1496: Both modify i18n integration points across services including use-locale, I18nProvider/LocaleSync, and language-switch UI components.
  • tale-project/tale#943: Both update platform agent locale resolution and imports (e.g., narrowBcp47 from shared packages).

Suggested labels

refactor, i18n, monorepo

Suggested reviewers

  • larryro
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/centralize-locale-detection

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
package.json (1)

87-87: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove or justify unused react-i18next root dependency.

Knip reports react-i18next as unused at Line 87. If usage has moved fully behind @tale/ui/i18n, this should be removed to keep dependency hygiene clean.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 87, Knip flags the root package.json dependency
"react-i18next" as unused; either remove the "react-i18next": "17.0.2" entry
from package.json or add a short justification (e.g., comment in PR description
or move to devDependencies) if it’s intentionally kept for consumers; if usage
is now provided by the scoped package "@tale/ui/i18n", delete the dependency
line and run a reinstall and linting/knip check to confirm nothing breaks, or
document why the root-level dependency must remain.
services/platform/package.json (1)

99-100: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove now-unused i18n dependencies flagged by knip.

Knip flags i18next-icu (line 99) and intl-messageformat (line 100) as unused in services/platform. Both have no direct imports anywhere in platform source code; i18next-icu is pulled in transitively via @tale/ui, and intl-messageformat appears to be an orphaned dependency from prior code. Remove both to resolve the lint failure.

🔧 Cleanup
     "i18next": "26.0.4",
-    "i18next-icu": "2.4.3",
-    "intl-messageformat": "11.2.0",
     "jexl": "2.3.0",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/platform/package.json` around lines 99 - 100, Remove the now-unused
i18n dependencies by deleting the "i18next-icu" and "intl-messageformat" entries
from services/platform package.json dependencies, run the package manager to
update lockfile (npm/yarn/pnpm install), and re-run the knip/lint pipeline and
tests to confirm no imports break; note that "i18next-icu" is transitive via
`@tale/ui` so removing the direct entry is safe.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/i18n/src/locales.ts`:
- Around line 53-59: The function localizedPath currently returns an empty
string for localizedPath('en','') because the 'en' fast path runs before
normalizing root; move or add a root normalization check in localizedPath to
handle pathname === '' or pathname === '/' first: if root and locale === 'en'
return '/' otherwise return `/${locale}`; this ensures all locales including
'en' produce a valid root URL while preserving existing behavior for non-root
paths.

In `@packages/ui/package.json`:
- Around line 55-63: The package `@tale/ui` exposes React hooks/components that
rely on i18next, react-i18next, and i18next-icu at runtime, so move "i18next",
"react-i18next", and "i18next-icu" out of the dependencies block in
packages/ui/package.json and declare them under peerDependencies (keeping the
same version constraints) to require the host app to provide matching versions;
leave "@tale/i18n" as a normal dependency and optionally add those three
packages to devDependencies for local testing/builds if needed.

In `@packages/ui/src/i18n/locales.ts`:
- Around line 26-31: The function resolveRegionalLocale currently declares a
return type of string which loses type safety; change its signature to return
the precise locale union (e.g., Locale or SupportedLocale | RegionalLocale) and
import the appropriate type(s) into this module, update any related
constants/types such as REGIONAL_OVERRIDES if necessary so the candidate
(`${base}-${region}`) is recognized as a RegionalLocale, and ensure callers
still type-check with the new return type; reference the resolveRegionalLocale
function, the SupportedLocale/RegionalLocale/Locale types, and
REGIONAL_OVERRIDES when making these changes.

In `@packages/ui/src/i18n/use-current-locale.ts`:
- Around line 13-16: Remove the unsafe type assertion on the useParams call in
use-current-locale.ts and instead use TanStack Router's select option to pick
the lang param with proper typing; specifically, replace the cast around
useParams(...) as { lang?: string } by calling useParams({ strict: false,
select: p => ({ lang: p.lang }) }) (or select: p => p.lang depending on how you
consume it) so TypeScript infers lang as string | undefined without using `as`,
and update any code that reads `params.lang` to match the new selected shape.

In `@services/docs/app/entry-server.tsx`:
- Around line 19-20: Extract the duplicated expression that computes basepath
((import.meta.env.BASE_URL ?? '/').replace(/\/$/, '') || undefined) into a
single helper exported from a new module named basepath (e.g., export const
getBasepath or export const basepath) and replace the inline expression in both
router.tsx and entry-server.tsx with an import from that helper; ensure the
helper encapsulates the exact logic and preserves the undefined fallback so
existing callers (router and entry-server) behave identically.

In `@services/docs/lib/i18n/config.ts`:
- Line 1: Remove the unused shim file services/docs/lib/i18n/config.ts which
only re-exports defaultLocale from `@tale/ui/i18n/config`; delete the file and
update any imports if present to use `@tale/ui/i18n/config` directly (look for the
export "defaultLocale" in the file to locate the shim).

In `@services/docs/lib/i18n/keys-dynamic.txt`:
- Around line 10-14: The file comment is stale: update the explanatory comment
that references where `languageSwitcher` and `themeSwitcher` live; change the
path mention from "packages/webui/src/layout/" to the new location under
`@tale/ui/i18n` (or the appropriate `@tale/ui` package path that now contains
the React/i18next glue), so anyone auditing the allowlist knows
`languageSwitcher` and `themeSwitcher` are consumed from `@tale/ui/i18n` rather
than `@tale/webui`; keep the rest of the comment and listed keys
(`languageSwitcher`, `themeSwitcher`) unchanged.

In `@services/docs/lib/i18n/locales.ts`:
- Around line 5-13: The shim currently re-exports unused symbols localizedPath,
RegionalLocale, and UrlPrefixedLocale which breaks Knip; remove those three from
the re-export lists in this module so only consumed types/symbols remain (keep
resolveRegionalLocale, Locale, SupportedLocale if they are used). Update the two
export blocks (the named export list and the exported type list) to drop
localizedPath, RegionalLocale and UrlPrefixedLocale and run the Knip/lint to
confirm the pipeline passes.

In `@services/docs/lib/i18n/use-current-locale.ts`:
- Line 1: This re-export file only exposes useCurrentLocale (export {
useCurrentLocale } from '@tale/ui/i18n/use-current-locale') but isn't consumed
by the docs service; either delete this unused shim (remove
services/docs/lib/i18n/use-current-locale.ts) or wire it through by importing
useCurrentLocale in the docs app (e.g., add an import of useCurrentLocale in the
docs entry or a file under services/docs that uses i18n client functionality) so
knip sees it as used; update any barrel/index exports if needed to keep public
API consistent.

In `@services/docs/server.ts`:
- Around line 62-65: The catch block that swallows errors when decoding the
request path (the decodeURIComponent(pathname) call that assigns to rel) should
log the decoding error before returning the SPA fallback; update the catch to
capture the thrown error and emit a warning (e.g., console.warn or
console.error) including the pathname and the error, then return new
Response(file(join(DIST, 'index.html'))); keep the SPA fallback behavior intact
and reference the same variables/expressions (decodeURIComponent, pathname, rel,
Response(file(join(DIST, 'index.html')))) so the only change is adding the
warning log.

In `@services/platform/app/hooks/use-locale.ts`:
- Around line 40-53: The setLocale function can race with async dayjs locale
loading causing applyLocale/formatDate to use the old locale; change setLocale
(the function that currently calls setLocaleState(resolved) and
localStorage.setItem(STORAGE_KEY, resolved)) to first await
loadDayjsLocale(resolved) (handling errors) and only after the import succeeds
update setLocaleState(resolved) and persist to localStorage, or alternately gate
the locale state on a "localeLoaded" flag so components render only after
loadDayjsLocale completes; reference loadDayjsLocale, setLocale, setLocaleState,
applyLocale/formatDate, STORAGE_KEY, isValidLocale, and defaultLocale when
making this change.

In `@services/platform/lib/i18n/locales.ts`:
- Around line 1-13: This module exports and re-exports symbols (ALL_LOCALES,
detectInitialLocale, isUrlPrefixedLocale, localizedPath, resolveRegionalLocale
and types Locale, RegionalLocale, SupportedLocale, UrlPrefixedLocale) but is
unused; either remove this barrel file services/platform/lib/i18n/locales.ts to
eliminate dead code, or update callers to import from it — if you choose to
remove, delete the file and run the build/knip checks; if you choose to keep it,
add/modify imports in the consumers to reference this barrel (e.g., replace
direct imports from '@tale/ui/i18n/locales' with imports from
'services/platform/lib/i18n/locales.ts' where appropriate) so the exported
symbols are actually consumed.

In `@services/web/Dockerfile`:
- Around line 49-53: Remove the build-stage ARG/ENV declarations for
LOCALE_COOKIE_DOMAIN in the Dockerfile to avoid invalidating the build cache;
LOCALE_COOKIE_DOMAIN is only read at runtime by services/web/server.ts, so
delete the ARG LOCALE_COOKIE_DOMAIN and the ENV LOCALE_COOKIE_DOMAIN entries
from the builder stage (the block containing ARG LOCALE_COOKIE_DOMAIN and ENV
NODE_ENV... LOCALE_COOKIE_DOMAIN) and ensure the runner stage still defines
LOCALE_COOKIE_DOMAIN where it's actually consumed.

In `@services/web/lib/i18n/config.ts`:
- Line 1: The file contains an unused re-export shim: "export { defaultLocale }
from '@tale/ui/i18n/config'"; remove this shim if nothing imports this module,
or update the consumer(s) to import defaultLocale from this path instead of
importing directly from '@tale/ui/i18n/config'. Specifically, either delete the
re-export statement (cleanup) or ensure any intended consumers change their
imports to reference this re-exported symbol "defaultLocale" so the shim is
actually used.

In `@services/web/lib/i18n/locales.ts`:
- Line 14: Remove the unused re-exports flagged by knip: delete the export
entries for ALL_LOCALES and the type re-exports Locale, RegionalLocale, and
UrlPrefixedLocale from the locales barrel (the export list that currently
re-exports these symbols). Ensure you only remove the export statements (not the
original type or value definitions if they live elsewhere); run the build/tests
to confirm nothing breaks and add them back later when a concrete consumer
requires them.

In `@services/web/server.ts`:
- Around line 171-174: The bare catch around decodeURIComponent(pathname)
swallows malformed URL decode errors; update the catch to log the error and the
offending pathname instead of being empty so failures are visible. Specifically,
in the block that sets rel = decodeURIComponent(pathname).replace(/^\/+/, '');
catch the thrown error and call console.warn or console.error with a short
message including pathname and the error (or re-throw if desired), then return
the same fallback Response(file(join(DIST, 'index.html'))).
- Around line 211-236: The locale negotiation is incorrectly handling static
asset requests; update isLocaleNeutralPath (in packages/i18n/src/negotiate.ts)
so negotiatePathLocale() returns skip: true for asset directory patterns (e.g.,
paths under /assets/) and for common static file extensions (.js, .css, .png,
.jpg, .svg, .woff, .woff2, .ico, .map, etc.), ensuring negotiatePathLocale()
never sets redirectTo for those paths; verify both services calling
negotiatePathLocale (services/web where serveStatic and
applyLocaleResponseHeaders are used, and services/docs) will bypass
cookie/Accept-Language redirects for these requests.

In `@tools/plop/templates/react-service/lib/i18n/locales.ts`:
- Around line 1-13: Collapse the two named export blocks into wildcard
re-exports: replace the explicit exports of ALL_LOCALES, detectInitialLocale,
isUrlPrefixedLocale, localizedPath, resolveRegionalLocale and the explicit type
exports Locale, RegionalLocale, SupportedLocale, UrlPrefixedLocale with a single
export * from '@tale/ui/i18n/locales' and a single export type * from
'@tale/ui/i18n/locales' (so future additions from that module are automatically
exported).

---

Outside diff comments:
In `@package.json`:
- Line 87: Knip flags the root package.json dependency "react-i18next" as
unused; either remove the "react-i18next": "17.0.2" entry from package.json or
add a short justification (e.g., comment in PR description or move to
devDependencies) if it’s intentionally kept for consumers; if usage is now
provided by the scoped package "@tale/ui/i18n", delete the dependency line and
run a reinstall and linting/knip check to confirm nothing breaks, or document
why the root-level dependency must remain.

In `@services/platform/package.json`:
- Around line 99-100: Remove the now-unused i18n dependencies by deleting the
"i18next-icu" and "intl-messageformat" entries from services/platform
package.json dependencies, run the package manager to update lockfile
(npm/yarn/pnpm install), and re-run the knip/lint pipeline and tests to confirm
no imports break; note that "i18next-icu" is transitive via `@tale/ui` so removing
the direct entry is safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 50a35478-7bb9-4183-84b2-12a6efa62df6

📥 Commits

Reviewing files that changed from the base of the PR and between 4811527 and 3c837c3.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (87)
  • package.json
  • packages/i18n/package.json
  • packages/i18n/src/accept-language.test.ts
  • packages/i18n/src/accept-language.ts
  • packages/i18n/src/cookie.test.ts
  • packages/i18n/src/cookie.ts
  • packages/i18n/src/is-valid-locale.ts
  • packages/i18n/src/locales.ts
  • packages/i18n/src/narrow-bcp47.ts
  • packages/i18n/src/negotiate.test.ts
  • packages/i18n/src/negotiate.ts
  • packages/i18n/src/resolve-locale.test.ts
  • packages/i18n/src/resolve-locale.ts
  • packages/i18n/tsconfig.json
  • packages/i18n/vitest.config.ts
  • packages/ui/package.json
  • packages/ui/src/i18n/client.tsx
  • packages/ui/src/i18n/config.ts
  • packages/ui/src/i18n/init.ts
  • packages/ui/src/i18n/locales.ts
  • packages/ui/src/i18n/provider.tsx
  • packages/ui/src/i18n/regional-bundles.ts
  • packages/ui/src/i18n/sync.tsx
  • packages/ui/src/i18n/use-current-locale.ts
  • packages/webui/package.json
  • packages/webui/src/i18n/locales.ts
  • packages/webui/src/index.ts
  • packages/webui/src/layout/language-switcher.tsx
  • packages/webui/src/layout/theme-switcher.tsx
  • packages/webui/src/seo/build-sitemap.ts
  • packages/webui/src/seo/document-meta.ts
  • services/docs/Dockerfile
  • services/docs/app/entry-server.tsx
  • services/docs/app/router.tsx
  • services/docs/app/routes/__root.tsx
  • services/docs/docker-entrypoint.sh
  • services/docs/index.html
  • services/docs/lib/i18n/client.tsx
  • services/docs/lib/i18n/config.ts
  • services/docs/lib/i18n/i18n-provider.tsx
  • services/docs/lib/i18n/i18n.ts
  • services/docs/lib/i18n/keys-dynamic.txt
  • services/docs/lib/i18n/locales.ts
  • services/docs/lib/i18n/messages-usage.test.ts
  • services/docs/lib/i18n/messages.test.ts
  • services/docs/lib/i18n/use-current-locale.ts
  • services/docs/messages/de.json
  • services/docs/messages/en.json
  • services/docs/messages/fr.json
  • services/docs/package.json
  • services/docs/server.ts
  • services/docs/vitest.config.ts
  • services/platform/app/hooks/use-locale.ts
  • services/platform/convex/agent_tools/delegation/create_delegation_tool.ts
  • services/platform/lib/i18n/client.tsx
  • services/platform/lib/i18n/config.ts
  • services/platform/lib/i18n/i18n-provider.tsx
  • services/platform/lib/i18n/i18n.ts
  • services/platform/lib/i18n/locales.ts
  • services/platform/lib/shared/constants/agents.ts
  • services/platform/lib/shared/utils/resolve-agent-locale.ts
  • services/platform/package.json
  • services/web/Dockerfile
  • services/web/app/components/layout/localized-link.tsx
  • services/web/app/components/layout/site-footer.tsx
  • services/web/app/components/layout/site-header.tsx
  • services/web/app/pages/contact-page.tsx
  • services/web/app/pages/hardware-pricing-page.tsx
  • services/web/app/pages/home-page.tsx
  • services/web/app/pages/pricing-page.tsx
  • services/web/app/pages/request-demo-page.tsx
  • services/web/app/routes/__root.tsx
  • services/web/lib/i18n/client.tsx
  • services/web/lib/i18n/config.ts
  • services/web/lib/i18n/i18n-provider.tsx
  • services/web/lib/i18n/i18n.ts
  • services/web/lib/i18n/locales.ts
  • services/web/lib/i18n/localized-paths.ts
  • services/web/lib/i18n/use-current-locale.ts
  • services/web/package.json
  • services/web/server.ts
  • tools/plop/generators/react-service.ts
  • tools/plop/templates/react-service/lib/i18n/client.tsx
  • tools/plop/templates/react-service/lib/i18n/config.ts
  • tools/plop/templates/react-service/lib/i18n/i18n-provider.tsx
  • tools/plop/templates/react-service/lib/i18n/i18n.ts.hbs
  • tools/plop/templates/react-service/lib/i18n/locales.ts
💤 Files with no reviewable changes (6)
  • packages/webui/package.json
  • services/web/lib/i18n/localized-paths.ts
  • services/docs/messages/fr.json
  • packages/webui/src/i18n/locales.ts
  • services/docs/messages/de.json
  • services/docs/messages/en.json

Comment thread packages/i18n/src/locales.ts
Comment thread packages/ui/package.json
Comment thread packages/ui/src/i18n/locales.ts Outdated
Comment thread packages/ui/src/i18n/use-current-locale.ts Outdated
Comment thread services/docs/app/entry-server.tsx
Comment thread services/web/lib/i18n/config.ts Outdated
Comment thread services/web/lib/i18n/locales.ts Outdated
Comment thread services/web/server.ts
Comment thread services/web/server.ts
Comment thread tools/plop/templates/react-service/lib/i18n/locales.ts Outdated
…atic assets

Convex's deploy bundler doesn't resolve workspace-package subpath exports
through transitive re-exports. Smoke test failed with "Could not resolve
'@tale/i18n/narrow-bcp47'", "@tale/ui/i18n/config", "@tale/i18n/locales"
because platform's convex code reaches those workspace packages via
relative imports through `lib/i18n/config.ts`, `lib/shared/constants/
agents.ts`, and `lib/shared/utils/resolve-agent-locale.ts`.

Inline those values where convex can see them:

- services/platform/lib/i18n/config.ts: inline `defaultLocale = 'en'`
  (was a re-export from `@tale/ui/i18n/config`).
- services/platform/lib/shared/constants/agents.ts: inline
  `SUPPORTED_AGENT_LOCALES = ['en', 'de', 'fr']` (was a re-export of
  `SUPPORTED_LOCALES` from `@tale/i18n/locales`).
- services/platform/lib/shared/utils/narrow-bcp47.ts: restored as a
  platform-internal helper; dropped from `@tale/i18n` (it had only one
  consumer). Convex imports go through the relative path.

Both files carry a comment explaining why the re-export indirection is
intentional, in case a future cleanup rewires them through @tale/*.

Also fixes a real prod bug from CodeRabbit's review: the negotiator was
treating `/assets/index-Bn7FHL.js` as a candidate for redirect, so a
non-EN visitor's first request would 302 to `/de/assets/index-...js`,
load the SPA shell at that path, and break (script never loads).
`isLocaleNeutralPath` now skips `/assets/` and any path with a static
asset extension (.js, .css, .map, .png, .woff2, .ico, etc.).

Other CodeRabbit findings applied:

- localizedPath('en', '') returned '' (empty string); now returns '/'.
  Root normalization happens before the EN fast path.
- resolveRegionalLocale return type tightened to
  `SupportedLocale | RegionalLocale` (was `string`); REGIONAL_OVERRIDES
  is now typed `ReadonlySet<RegionalLocale>` and the lookup goes through
  a narrowed type guard.
- useCurrentLocale: replaced the `as { lang?: string }` cast with
  TanStack Router's `select` callback with a parameter annotation —
  no more `oxlint-disable` for that line.
- web/docs server.ts: `decodeURIComponent` catch now logs the offending
  pathname + error before falling back to the SPA shell (was silent).
- web/docs Dockerfiles: dropped the build-stage `LOCALE_COOKIE_DOMAIN`
  ARG/ENV. The value is read only at runtime by the Bun server, so it
  doesn't need to be in the builder layer (avoids cache-bust on env
  changes).

Skipped findings:

- Move i18next/react-i18next/i18next-icu to peerDependencies in
  @tale/ui: workspace-deduped via bun, regular deps work; not minimal.
- Extract a `basepath` helper for docs entry-server/router: pre-existing
  code, out of scope for this PR.
- keys-dynamic.txt comment update: the path it references
  (packages/webui/src/layout/) is still correct — the switcher
  *components* stayed in webui, only the i18n primitives moved to ui.
- Awaiting `loadDayjsLocale` before `setLocaleState`: pre-existing
  platform behavior, out of scope.
- Wildcard re-export from `@tale/ui/i18n/locales` in the plop template:
  the template doesn't include locales.ts anymore.
@yannickmonney yannickmonney merged commit 8421e5d into main May 8, 2026
23 checks passed
@yannickmonney yannickmonney deleted the feat/centralize-locale-detection branch May 8, 2026 16:37
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.

1 participant