- Status: Accepted
- Deciders: Team
- Date: 19.02.2026
A1: A scoped context internal to src/app-components/ exposes a minimal translation function and a translation React Component. The wider app wires them up once via a bridge component placed inside GlobalFormDataReadersProvider.
src/app-components/ is intended to contain self-contained, "dumb" UI components with no dependencies on the wider app (see 2024-10-17-app-component-library.md). However, some components need to display translated strings — for example, an aria-label on a loading spinner or a page number label in a pagination control.
The naive solution of importing useLanguage from src/features/language/ directly violates the self-containment requirement and re-introduces the tight coupling ADR 001 set out to remove.
- B1: Components in
src/app-components/must not import from outsidesrc/app-components/. - B2: Language switching must be reactive — components must re-render when the language changes.
- B3: The solution must be simple to use inside components and cheap to wire up in the app.
- B4: The solution must be easy to test in isolation (Storybook, unit tests).
- A1: Scoped context inside
src/app-components/accepting a minimal translation function. - A2: Pre-translated strings passed as props by the caller.
- A3: Translation function passed as a prop by the caller.
- A4: Module-level singleton — a mutable global translation function.
- A5: Dedicated i18n library (e.g.
react-i18next) scoped toapp-components.
- Good, because it satisfies B1: components only import from within
src/app-components/. - Good, because it satisfies B2: React context is reactive; language changes propagate automatically.
- Good, because it satisfies B3: wire-up cost is paid once at the app level; components just call a hook.
- Good, because it satisfies B4: tests pass
t={(key) => key}or a fixture function — no mocking ofuseLanguage's dependency tree. - Good, because the interface is narrow:
(key: string, params?: (string | number | undefined)[]) => string. Only the minimal surface the components actually need is exposed. - Bad, because components are not 100% standalone — they require the provider to be present. Mitigated by a clear error message when the provider is missing.
- Good, because components are fully standalone with zero i18n knowledge.
- Good, because it satisfies B1 and B4.
- Bad, because it does not scale: as more components need more strings, each gets its own set of string props, leading to inconsistent APIs and prop proliferation.
- Bad, because callers must know and supply every text a component needs, spreading that responsibility across the codebase.
- Good, because components remain unaware of the i18n implementation.
- Bad, because components still need to know the translation keys they use, creating hidden coupling.
- Bad, because every call site must thread the function through, which is repetitive and unusual for a UI component API.
- Bad, because it does not scale across many components for the same reasons as A2.
- Good, because no context or provider is needed — components just import and call
t(). - Good, because it satisfies B1.
- Bad, because it does not satisfy B2: the singleton is not reactive. Changing the language updates the function reference but does not trigger re-renders in components that have already rendered.
- Bad, because global mutable state is an anti-pattern in React and makes the data flow opaque.
- Bad, because it is harder to test: the global state leaks between tests unless explicitly reset.
- Good, because the library is genuinely self-contained;
app-componentsdepends only on the library, not on the app. - Good, because it satisfies B1 and B2.
- Bad, because it introduces an external dependency and significant setup overhead for a problem that is fully solved by a single function interface.
- Bad, because translation resources would need to be maintained separately from the app's own text resources, creating duplication.
The context lives at src/app-components/AppComponentsProvider.tsx and exposes AppComponentsProvider and useTranslation. The app side contains a small bridge component that calls useLanguage() and passes langAsString to the provider.