// i18n configuration — Paraglide JS (inlang). Messages live in `messages/*.json` // and are compiled to `src/paraglide/` by the Vite plugin (gitignored). This // module is the locale-plumbing entry point; `@/i18n/messages` exposes the typed // message functions (`m`). // // Add a language: drop `messages/.json` next to `en.json`, add the code to // `project.inlang/settings.json` `locales`, and recompile. Content is reachable // via the `/` URL prefix (e.g. `/fr`); the base locale (`en`) needs none. import { useSyncExternalStore } from 'react' import { locales, baseLocale, overwriteGetLocale, getLocale } from '../paraglide/runtime.js' export const supportedLocales = locales export const defaultLocale = baseLocale export type Locale = (typeof locales)[number] // `getLocale()` reads the active locale (via the overwrite below) — used by // pages that format dates/values for the current language. export { getLocale } const LANGUAGE_STORAGE_KEY = 'app-locale' const RTL_LOCALES = new Set(['ar', 'he', 'fa', 'ur']) // Direction for a locale — RTL for Arabic/Hebrew/Farsi/Urdu, else LTR. Set this // on at the root so the browser (and Mantine) mirror the layout. export function localeDir(locale: string = defaultLocale): 'rtl' | 'ltr' { return RTL_LOCALES.has(locale.split('-')[0]) ? 'rtl' : 'ltr' } const normalizeLanguage = (value: string | null | undefined): Locale | undefined => { const short = value?.slice(0, 2).toLowerCase() return supportedLocales.includes(short as Locale) ? (short as Locale) : undefined } // Initial locale: persisted choice → browser → → base. (Ported from // the old i18next init; germantax has no in-app switcher, locale is read once.) export function detectInitialLocale(): Locale { if (typeof window === 'undefined') return defaultLocale return ( normalizeLanguage(window.localStorage.getItem(LANGUAGE_STORAGE_KEY)) ?? normalizeLanguage(window.navigator.language) ?? normalizeLanguage(window.document.documentElement.lang) ?? defaultLocale ) } // ── reactive locale store ──────────────────────────────────────────────────── // Paraglide's `getLocale()` is a module-global, decoupled from React. We bridge // it to a tiny external store: `getLocale` reads `activeLocale`; components // subscribe via `useLocale()` (useSyncExternalStore) so `m.*()` re-renders on // switch — no page reload, no Paraglide `setLocale`, no app-specific context. // The app's own setLocale (persist + ) calls `setActiveLocale`. let activeLocale: Locale = defaultLocale const listeners = new Set<() => void>() overwriteGetLocale(() => activeLocale) export function setActiveLocale(next: Locale): void { if (next === activeLocale) return activeLocale = next // Persist + reflect on (ported from the old i18next languageChanged // handler), so a reload restores the choice. if (typeof window !== 'undefined') { window.localStorage.setItem(LANGUAGE_STORAGE_KEY, next) window.document.documentElement.lang = next } for (const fn of listeners) fn() } function subscribe(fn: () => void): () => void { listeners.add(fn) return () => listeners.delete(fn) } // Resolve the initial locale once at startup (import side-effect, matching the // old i18n.ts init). Importing this module from the app entry runs it. activeLocale = detectInitialLocale() // Subscribe a component to locale changes so `m.*()` re-renders on switch. The // codemod injects a bare `useLocale()` wherever `const { t } = useTranslation()` // used to live; it also returns the active locale + direction for components // that need them (e.g. the language switcher). export function useLocale(): { locale: Locale; dir: 'ltr' | 'rtl'; setLocale: (l: Locale) => void } { const locale = useSyncExternalStore(subscribe, () => activeLocale, () => activeLocale) return { locale, dir: localeDir(locale), setLocale: setActiveLocale } } // ── i18n-debug masking ─────────────────────────────────────────────────────── // When enabled, every *translated* string is masked to block glyphs (█) so any // readable text left on screen is text that never went through a message (a // hardcoded/inlined string) — missing i18n at a glance. Toggle with `?i18n-debug` // in the URL or `localStorage['i18n-debug'] = '1'` (`I18N_DEBUG=1` for SSR). export function isI18nDebug(): boolean { if (typeof process !== 'undefined' && process.env?.I18N_DEBUG === '1') return true if (typeof window === 'undefined') return false const params = new URLSearchParams(window.location.search) if (params.has('i18n-debug')) return params.get('i18n-debug') !== '0' return window.localStorage?.getItem('i18n-debug') === '1' } /** Mask a rendered string when debug mode is on; otherwise pass it through. */ export function maskI18n(s: string): string { return isI18nDebug() ? s.replace(/\S/g, '█') : s }