React adapter for @levigo/jadice-i18n-support.
The core jadice-i18n-support package defines how translations are requested (the I18N singleton
and the I18NProvider interface) but deliberately does not implement where they come from. This
package supplies that implementation for React applications: it loads translation JSON files, detects
and persists the active language, registers itself with the I18N singleton so jadice core components
translate correctly, and exposes React context plus hooks for application code.
npm install @levigo/jadice-react-i18n-supportThe package is published to the internal registry https://artifacts.jadice.com/repository/npm-hosted/.
These must be present in the consuming application:
| Package | Range |
|---|---|
@levigo/jadice-i18n-support |
^2.0.162 |
@levigo/utility-types |
^2.0.80 |
react |
^18.0.0 || ^19.0.0 |
rxjs |
^7.4.0 |
Translation files are plain JSON, served as static assets, one file per language, named
{language}.json:
public/locales/en.json
public/locales/de.json
{
"toolbar": {
"save": "Save",
"close": "Close"
},
"greeting": "Hello {{name}}!"
}Keys are addressed in dot notation (toolbar.save). Nesting depth is arbitrary. Values must be
strings; a key resolving to an object or a missing key falls back to the key itself.
<I18NProvider> belongs near the root of the application. Only one instance may exist — it registers
itself with the global I18N singleton, and a second instance would overwrite the first.
import {I18NProvider} from "@levigo/jadice-react-i18n-support";
export function Root() {
return (
<I18NProvider
config={{
sourcePaths: ["/locales"],
defaultLanguage: "en",
supportedLanguages: ["de", "en", "fr", "it"],
}}
fallback={<div>Loading…</div>}
>
<App/>
</I18NProvider>
);
}Until the initial language is determined, the provider renders fallback (default: nothing) instead
of children. Note that the provider waits for the language, not for the translation files — the
first render can happen while translations are still in flight, in which case t() returns keys until
the load completes and a re-render is triggered.
import {useTranslation} from "@levigo/jadice-react-i18n-support";
export function Toolbar() {
const {t} = useTranslation();
return (
<>
<button>{t("toolbar.save")}</button>
<span>{t("greeting", {name: "World"})}</span>
</>
);
}import {useLanguage} from "@levigo/jadice-react-i18n-support";
export function LanguagePicker() {
const {language, setLanguage} = useLanguage();
return (
<select value={language ?? ""} onChange={e => setLanguage(e.target.value)}>
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>
);
}ReactI18NConfig:
| Option | Type | Default | Description |
|---|---|---|---|
sourcePaths |
string[] |
(required) | Base paths to load from. Each is fetched as {path}/{language}.json and deep-merged in order — later paths win on conflict. |
defaultLanguage |
string |
"en" |
Fallback when neither storage nor browser language yields a supported language. |
supportedLanguages |
string[] |
["de", "en", "fr", "it"] |
Whitelist used only for browser-language detection. |
fetchFn |
typeof fetch |
global fetch |
Custom fetch, e.g. to add auth headers or to stub in tests. |
Multiple source paths are the mechanism for layering translations — for example, shipping the jadice component defaults and letting the application override individual keys:
config={{sourcePaths: ["/jadice-locales", "/app-locales"]}}Files are fetched with cache: "no-store". A path that 404s, fails to parse, or throws is skipped
silently; the remaining paths still apply.
On init() the language is picked in this order:
localStorage["JADICE-I18N+LANGUAGE"]— whatever was last selected, not validated againstsupportedLanguages.- The browser language (
navigator.language, region stripped), if it is insupportedLanguages. defaultLanguage.
Every setLanguage() call writes back to that localStorage key. The key is intentionally identical to
the one used by the Angular adapter, so a language choice carries across frameworks in mixed
deployments.
Storing the language preference in
localStorageis functionally necessary for the requested feature (persisting a user's choice) and holds no personal data, but it is still terminal storage. Depending on the surrounding product's consent model, this may need to be covered in the privacy notice.
Exported from the package root:
I18NProvider— context provider. Props:config: ReactI18NConfig,fallback?: ReactNode,children.
useTranslation(): I18NContextValue— returns{t, language, setLanguage, isLoading}.useLanguage(): {language, setLanguage}— narrower alternative when no translation is needed.
Both throw if called outside an <I18NProvider>.
TranslateFn—(key: string, params?: Record<string, any>) => string.I18NContextValue—{t, language: string | null, setLanguage, isLoading: boolean}.ReactI18NConfig,I18NProviderProps.
-
ReactI18NService— theI18NProviderimplementation backing the context. The React provider constructs and owns one; use it directly only outside React (tests, bootstrapping code).Implements
translate(),translateDynamic(),translateOnce(),setLanguage()andgetCurrentLanguage$()per the core interface, plusinit(),destroy(),isLoading$,reload$andcurrentLanguage.
Exported for reuse; not needed for normal operation.
resolveKey(obj, "a.b.c")— dot-path lookup,undefinedunless the result is a string.interpolate(template, params)— replaces{{token}}; unmatched tokens stay verbatim.deepmerge(target, source)— recursive merge used for layering source paths.
I18NContext— the raw context. Prefer the hooks.
ReactI18NService.init() calls I18N.get().setProvider(this). From that point on, jadice core
components that translate via the I18N singleton — including non-React code — resolve against the
same translation data and language as the React tree. Application code should still go through the
hooks; the singleton exists for the core components.
t()is synchronous and reads the current translation table at call time — it is not a subscription.- Consumers re-render when the context value changes, which happens when
languageorisLoadingchanges. A language switch flips both, so translations refresh on switch. t()results must therefore not be memoized across renders on an unrelated dependency list; memoize onlanguageif you memoize at all.- Observable-based access (
translate(),translateDynamic()) is available on the service for interop with RxJS-based code, and updates on every reload.
Caveat: the provider also keeps an internal revision counter fed by the service's
reload$. A bump re-renders the provider but leaves both thechildrenelement reference and the memoized context value unchanged, so it does not by itself reach memoized consumers. In practice refreshes ride on theisLoadingtransition that accompanies each load. Reloads that do not changelanguageorisLoadingare not covered by the test suite.
{{name}} tokens are replaced from params. Whitespace inside the braces is tolerated
({{ name }}). Token names match \w+. A token with no corresponding param is left in the output
unchanged, which makes missing params visible rather than silently blank.
interpolate("Hello {{name}}!", {name: "World"}); // "Hello World!"
interpolate("{{a}} and {{b}}", {a: "X"}); // "X and {{b}}"There is no error channel: a missing key, an unloaded language, a failed fetch, and a non-string value all produce the key itself as the rendered string. This keeps the UI functional but means missing translations must be caught by inspection or by tests, not at runtime.
npm ci # install
npm run build # tsc -> dist/
npm run watch # tsc --watch
npm test # jest, with coverage
npm run lint # eslint over src/
npm run ci-setup # npm ci && npm run buildTests run under jest with ts-jest and the jsdom environment; component tests use
@testing-library/react. Sources live in src/, tests in test/ mirroring the source layout.
Build output is emitted to dist/ (ESM, target: ES2022) with declaration files; main and types
point there and only dist/, README.md and LICENSE are packed.
The license field in package.json is empty and no LICENSE file is present in the repository,
although package.json lists one under files. Licensing terms need to be clarified before external
distribution.