Skip to content

Repository files navigation

@levigo/jadice-react-i18n-support

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.

Installation

npm install @levigo/jadice-react-i18n-support

The package is published to the internal registry https://artifacts.jadice.com/repository/npm-hosted/.

Peer dependencies

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

Quick start

1. Provide translation files

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.

2. Mount the provider

<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.

3. Translate

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>
        </>
    );
}

4. Switch language

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>
    );
}

Configuration

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.

Language resolution

On init() the language is picked in this order:

  1. localStorage["JADICE-I18N+LANGUAGE"] — whatever was last selected, not validated against supportedLanguages.
  2. The browser language (navigator.language, region stripped), if it is in supportedLanguages.
  3. 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 localStorage is 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.

API

Exported from the package root:

Components

  • I18NProvider — context provider. Props: config: ReactI18NConfig, fallback?: ReactNode, children.

Hooks

  • useTranslation(): I18NContextValue — returns {t, language, setLanguage, isLoading}.
  • useLanguage(): {language, setLanguage} — narrower alternative when no translation is needed.

Both throw if called outside an <I18NProvider>.

Types

  • TranslateFn — (key: string, params?: Record<string, any>) => string.
  • I18NContextValue — {t, language: string | null, setLanguage, isLoading: boolean}.
  • ReactI18NConfig, I18NProviderProps.

Service

  • ReactI18NService — the I18NProvider implementation backing the context. The React provider constructs and owns one; use it directly only outside React (tests, bootstrapping code).

    Implements translate(), translateDynamic(), translateOnce(), setLanguage() and getCurrentLanguage$() per the core interface, plus init(), destroy(), isLoading$, reload$ and currentLanguage.

Utilities

Exported for reuse; not needed for normal operation.

  • resolveKey(obj, "a.b.c") — dot-path lookup, undefined unless the result is a string.
  • interpolate(template, params) — replaces {{token}}; unmatched tokens stay verbatim.
  • deepmerge(target, source) — recursive merge used for layering source paths.

Context

  • I18NContext — the raw context. Prefer the hooks.

Interaction with the I18N singleton

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.

Reactivity model

  • 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 language or isLoading changes. 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 on language if 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 the children element reference and the memoized context value unchanged, so it does not by itself reach memoized consumers. In practice refreshes ride on the isLoading transition that accompanies each load. Reloads that do not change language or isLoading are not covered by the test suite.

Interpolation

{{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}}"

Missing translations

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.

Development

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 build

Tests 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.

License

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages