@desolint/translation
v0.0.1
Published
Framework-agnostic i18next / react-i18next translation layer (client provider + useTranslation hook, server-side translator, locale helpers) for Desolint frontend projects.
Downloads
168
Maintainers
Readme
@desolint/translation
Framework-agnostic i18next / react-i18next translation layer for Desolint
frontend projects — a client provider, a boilerplate-compatible
useTranslation hook, a server-side translator, and locale helpers, with zero
hard dependency on next or any app code.
Requirements
- React 18 or newer, plus
i18next23+ andreact-i18next14+ (all peer dependencies) - npm 7 or newer — npm 7+ installs peer dependencies automatically
Install
npm install @desolint/translation i18next react-i18next i18next-resources-to-backendreact (>=18), i18next, react-i18next, and i18next-resources-to-backend
are peer dependencies — the package reuses the copies your app already has
and pulls in nothing else. No next, no nuqs, no UI library.
Neither installs peer dependencies automatically:
yarn add @desolint/translation react i18next react-i18next i18next-resources-to-backend
# or
pnpm add @desolint/translation react i18next react-i18next i18next-resources-to-backendWhy these are peers, not regular, dependencies: react-i18next keeps the
active i18next instance in React context and module internals — two copies of
react or i18next mean a component initialized against one and reading from
the other, which silently returns keys instead of translations. Declaring them
as peers makes npm reuse your app's copies.
What's in the box
Client entry — @desolint/translation
| Export | Purpose |
| ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| TranslationsProvider | Client provider — inits i18next for the active locale, hydrates from server resources, provides the instance + config to the tree. |
| useTranslation() | {t, i18n, ready, ct, Trans} — namespaces come from the provider, so call sites take no arguments. |
| ct({constant, t}) | Resolves a string \| (({t}) => …) constant. The hook / server translator return a t-bound ct({constant}). |
| initializeTranslations({locale, config, resources?, instance?}) | The shared init routine (used by the provider and the server entry). |
| setLocaleCookie, getCurrentLanguage, getLanguageOptions | Framework-agnostic locale helpers. |
| DEFAULT_LOCALE_COOKIE_NAME, DEFAULT_LOCALE_COOKIE_MAX_AGE_DAYS | Cookie defaults. |
Server entry — @desolint/translation/server
| Export | Purpose |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| getTranslations({locale, config, resources?}) | Server-render init → {i18n, resources, t}. Pass resources to <TranslationsProvider>. |
| getServerTranslator({config, locale?, resolveLocale?, resources?}) | {t, ct, i18n} for metadata / errors / anything outside the React tree. Replaces the boilerplate's translationUtilsValues(). |
| mockEmptyT | Sentinel t for reading a constant object's raw value instead of a label. |
The I18nConfig object
The package holds no config of its own — no configure() call, no module
singleton. Your app owns an I18nConfig (a stable module-level constant) and
passes it into the provider and the server helpers:
import type {I18nConfig} from '@desolint/translation';
export const i18nConfig: I18nConfig = {
locales: ['en', 'nl'],
defaultLocale: 'en',
namespaces: ['common', 'auth', 'dashboard'],
// Required only when you don't hand the provider pre-loaded `resources`.
// A template-string import lets the bundler code-split each namespace.
loadResource: (language, namespace) =>
import(`@/locales/${language}/${namespace}.json`),
};Examples
Self-contained snippets live in examples/:
i18n-config.ts— theI18nConfigconstanttranslations-provider.tsx— the'use client'wrapper that feeds the config to the providerprovider-setup.tsx— the server layout (server init + client hydration)use-translation.tsx—t/ctat a call sitetrans-html.tsx— translations containing markupserver-translator.tsx— page metadatalanguage-switcher.tsx— a switcher built from the locale helpers
Provider
TranslationsProvider is a Client Component, and i18nConfig carries a
loadResource function — so React can't pass the config across a
Server → Client boundary. Wire it in from a thin 'use client' wrapper that
imports the config directly (same shape as @desolint/react-query's provider
wrapper):
// app/providers/TranslationsProvider.tsx
'use client';
import {TranslationsProvider} from '@desolint/translation';
import {i18nConfig} from '@/i18nConfig';
export default function AppTranslationsProvider({children, locale, resources}) {
return (
<TranslationsProvider
config={i18nConfig}
locale={locale}
resources={resources}
fallback={<GlobalLoader />}
>
{children}
</TranslationsProvider>
);
}// app/[locale]/layout.tsx (Server Component)
import {getTranslations} from '@desolint/translation/server';
import {i18nConfig} from '@/i18nConfig';
import AppTranslationsProvider from '@/app/providers/TranslationsProvider';
export default async function LocaleLayout({children, params}) {
const {locale} = await params;
const {resources} = await getTranslations({locale, config: i18nConfig});
return (
<AppTranslationsProvider locale={locale} resources={resources}>
{children}
</AppTranslationsProvider>
);
}The server passes only serializable props (locale, resources). resources
is optional — omit it and the provider lazy-loads namespaces through
config.loadResource (client-only apps that render the provider directly can
skip the wrapper). fallback (default null) renders while init is in flight.
useTranslation
'use client';
import {useTranslation} from '@desolint/translation';
function ProfileCard({name}: {name: string}) {
const {t, ct, Trans} = useTranslation();
return (
<>
<h2>{t('common:profile.title')}</h2>
<p>{t('common:profile.greeting', {name})}</p>
<Trans i18nKey='dashboard:welcome' values={{name}} />
</>
);
}ct resolves a constant that may be a literal or a lazy translation — handy
for shared constant objects:
const NAV = {label: ({t}) => t('dashboard:nav.settings'), href: '/settings'};
// at the call site:
ct({constant: NAV.label});Server-side translation
import {getServerTranslator} from '@desolint/translation/server';
import {i18nConfig} from './i18nConfig';
export async function generateMetadata() {
const {t} = await getServerTranslator({
config: i18nConfig,
// You resolve the request locale — e.g. a cookie via next/headers.
resolveLocale: () => cookies().get('NEXTJS_LOCALE')?.value,
});
return {title: t('metadata:home.title')};
}API
Client (@desolint/translation)
TranslationsProvider— props{children, locale, config, resources?, fallback?}useTranslation()→{t, i18n, ready, ct, Trans}ct({constant, t})initializeTranslations({locale, config, resources?, instance?})→{i18n, resources, t}setLocaleCookie({language, cookieName?, maxAgeDays?, path?})getCurrentLanguage({currentLocale, languages})→LanguageOption | nullgetLanguageOptions({languages})→LanguageOption[]DEFAULT_LOCALE_COOKIE_NAME('NEXTJS_LOCALE'),DEFAULT_LOCALE_COOKIE_MAX_AGE_DAYS(30)TranslationConfigContext,useTranslationConfig()
Server (@desolint/translation/server)
getTranslations({locale, config, resources?})→{i18n, resources, t}getServerTranslator({config, locale?, resolveLocale?, resources?})→{t, ct, i18n}mockEmptyT()
Full type definitions ship in dist/index.d.ts and dist/server.d.ts.
What stays in your app
The Next.js- and UI-coupled pieces are deliberately not in the package:
I18nConfig/ namespace list — your config, injected.useLocaleRouter— a thin wrapper overnext/navigation+i18n.language; keep it app-side.- The language switcher UI — build it on
getLanguageOptions/setLocaleCookie/i18n.changeLanguage(see the example) with your own component library.
Development
npm install # install dependencies (Husky wires up git hooks)
npm run build # tsc + tsc-alias → dist/ (CJS + .d.ts)
npm run type-check # tsc --noEmit, source + examples/
npm test # vitest
npm run lint # eslintTranslationsProvider is the only 'use client' module; the build keeps the
directive on line 1 of dist/providers/TranslationsProvider.js (verified after
every build). A pre-commit hook runs lint-staged; the same
type-check / lint / test / build runs in CI on every push and PR.
Consumed locally via a file:../package-translation dependency until
published — run npm run build after changing source, then npm install in
the consuming app to refresh its copy.
License
MIT © Desolint — see LICENSE.
Free to use, modify and redistribute, commercially or otherwise. Provided "as is", without warranty or liability of any kind.
