@unisense.io/react-translator
v1.0.0
Published
React SDK for Unisense Translation Management — fetch, cache, ICU format, hooks, Translate component.
Maintainers
Readme
@unisense.io/react-translator
React SDK for Unisense — the open-source Translation Management platform.
Manage all your i18n translations in one place, then consume them in your React app with a single hook.
Create your free workspace at unisense.io.
Which entry point do I need?
| My React version | Import from | Hook internals |
|---|---|---|
| 18+ | @unisense.io/react-translator | useSyncExternalStore — concurrent-safe |
| 16.8 – 17 | @unisense.io/react-translator/legacy | useState + useEffect |
Both entry points expose exactly the same API. Only the internal subscription mechanism differs.
npm install @unisense.io/react-translatorQuick start — React 18+
1. Wrap your app with <TranslatorProvider>
// main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { TranslatorProvider } from '@unisense.io/react-translator';
import { App } from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<TranslatorProvider
apiUrl="https://unisense.io/api"
projectId="clxxxxxxxxxxxxxxxx"
apiKey="your_api_key"
defaultLocale="en"
preloadLocales={['fr']}
fallback={<div>Loading translations…</div>}
>
<App />
</TranslatorProvider>
</StrictMode>
);You can get your
projectIdandapiKeyfrom your project's API Keys page on unisense.io.
2. Translate with useTranslate
import { useTranslate } from '@unisense.io/react-translator';
export function WelcomePage() {
const t = useTranslate();
return (
<div>
<h1>{t('homepage.title')}</h1>
<p>{t('greeting', { name: 'Alice' })}</p>
<p>{t('items.count', { count: 3 })}</p>
{/* With explicit fallback */}
<span>{t('missing.key', 'Default text')}</span>
</div>
);
}3. Switch language with useLanguage
import { useTranslate, useLanguage } from '@unisense.io/react-translator';
export function Header() {
const t = useTranslate();
const [language, changeLanguage] = useLanguage();
return (
<header>
<h1>{t('app.name')}</h1>
<div>
<button
onClick={() => changeLanguage('en')}
disabled={language === 'en'}
>
🇬🇧 English
</button>
<button
onClick={() => changeLanguage('fr')}
disabled={language === 'fr'}
>
🇫🇷 Français
</button>
</div>
</header>
);
}React 16.8 – 17 (legacy entry point)
The API is identical — only the import path changes:
import {
TranslatorProvider,
useTranslate,
useLanguage,
useIsLoading,
Translate,
} from '@unisense.io/react-translator/legacy';Everything else is the same.
Hooks reference
useTranslate()
Returns the t() translation function. The component re-renders automatically when the locale changes.
const t = useTranslate();
t('key') // → translated string or key if missing
t('key', 'Default text') // → translated string or fallback
t('key', { count: 3 }) // → ICU interpolation
t('key', 'Default {name}', { name }) // → fallback + interpolationuseLanguage()
const [language, changeLanguage] = useLanguage();
// language → 'en' | 'fr' | ...
// changeLanguage → (locale: string) => Promise<void>useIsLoading()
const loading = useIsLoading();
// true while the current locale is being fetcheduseTranslatorState()
Returns the full state snapshot. Useful when you need several values at once without multiple hook calls.
const { locale, loading, ready, translations } = useTranslatorState();useLanguages()
Returns a stable function to fetch the project's available languages from the API.
const getLanguages = useLanguages();
useEffect(() => {
getLanguages().then(langs => setLanguages(langs));
}, [getLanguages]);<Translate /> component
An inline component alternative to the useTranslate hook.
import { Translate } from '@unisense.io/react-translator';
export function ProductCard({ count }: { count: number }) {
return (
<div>
<h2><Translate id="product.title" /></h2>
<p><Translate id="product.description" defaultValue="No description available." /></p>
<span><Translate id="items.count" params={{ count }} /></span>
</div>
);
}| Prop | Type | Description |
|---|---|---|
| id | string | Translation key |
| params | Record<string, unknown> | ICU interpolation params |
| defaultValue | string | Fallback if key is missing |
Language switcher with flag list
import { useState, useEffect } from 'react';
import { useLanguage, useLanguages } from '@unisense.io/react-translator';
export function LanguageSwitcher() {
const [language, changeLanguage] = useLanguage();
const getLanguages = useLanguages();
const [langs, setLangs] = useState([]);
useEffect(() => {
getLanguages().then(setLangs);
}, [getLanguages]);
return (
<select value={language} onChange={e => changeLanguage(e.target.value)}>
{langs.map(l => (
<option key={l.code} value={l.code}>
{l.flagEmoji} {l.name}
</option>
))}
</select>
);
}ICU Message Format
The built-in parser supports the full subset needed for most apps — no external dependency required.
Simple variable
"Hello, {name}!"
→ t('key', { name: 'Alice' }) → "Hello, Alice!"Plural
"{count, plural, =0 {No items} one {# item} other {# items}}"
→ t('key', { count: 3 }) → "3 items"Supported cases: =0, =1, =N, zero, one, two, few, many, other.
Select
"{gender, select, male {his account} female {her account} other {their account}}"
→ t('key', { gender: 'female' }) → "her account"Nested ICU
"{count, plural, =0 {No {type}} one {One {type}} other {# {type}s}}"
→ t('key', { count: 2, type: 'project' }) → "2 projects"Configuration reference
All options for <TranslatorProvider>:
<TranslatorProvider
// Required
apiUrl="https://unisense.io/api"
projectId="clxxxxxxxxxxxxxxxx"
defaultLocale="en"
// Recommended
apiKey="your_api_key"
// Optional
preloadLocales={['fr', 'de']} // Preloaded in background after init
languageDetectors={[myDetector()]} // Ordered chain, first match wins
languageStorage={cookieLanguageStorage('lang')} // null = disabled
// UI
fallback={<Spinner />} // Shown while first locale loads
>TranslatorConfig interface
interface TranslatorConfig {
/** Base URL of the Unisense API — no trailing slash */
apiUrl: string;
/** Project ID from the Unisense dashboard */
projectId: string;
/** API Key from the project's API Keys page */
apiKey?: string;
/** Fallback locale when all detectors return undefined */
defaultLocale: string;
/** Extra locales to fetch in background after init */
preloadLocales?: string[];
/**
* Ordered chain of locale detectors.
* Default: [navigatorLanguageDetector()]
*/
languageDetectors?: LanguageDetectorMiddleware[];
/**
* Locale persistence layer.
* Default: cookieLanguageStorage('preferred_language')
* Set to null to disable.
*/
languageStorage?: LanguageStorageMiddleware | null;
}Locale resolution order
languageStorage.getLanguage()— stored cookie / localStorage valuelanguageDetectors[0..n]— first detector returning a non-undefined value winsconfig.defaultLocale— hard-coded fallback
Custom language detection & storage
URL param detector
import type { LanguageDetectorMiddleware } from '@unisense.io/react-translator';
const urlParamDetector: LanguageDetectorMiddleware = {
detect() {
return new URLSearchParams(window.location.search).get('lang') ?? undefined;
},
};localStorage storage
import type { LanguageStorageMiddleware } from '@unisense.io/react-translator';
const localStorageMiddleware: LanguageStorageMiddleware = {
getLanguage: () => localStorage.getItem('locale') ?? undefined,
setLanguage: (lang) => localStorage.setItem('locale', lang),
};Combining both
<TranslatorProvider
apiUrl="https://unisense.io/api"
projectId="clxxxxxxxxxxxxxxxx"
defaultLocale="en"
languageDetectors={[urlParamDetector, navigatorLanguageDetector()]}
languageStorage={localStorageMiddleware}
>SSR / Next.js
Disable browser-specific middleware on the server:
// app/layout.tsx (Next.js App Router)
import { TranslatorProvider, cookieLanguageStorage } from '@unisense.io/react-translator';
import { cookies } from 'next/headers';
export default function RootLayout({ children }) {
const locale = cookies().get('preferred_language')?.value ?? 'en';
return (
<html lang={locale}>
<body>
<TranslatorProvider
apiUrl={process.env.NEXT_PUBLIC_UNISENSE_API_URL!}
projectId={process.env.NEXT_PUBLIC_PROJECT_ID!}
apiKey={process.env.NEXT_PUBLIC_API_KEY!}
defaultLocale="en"
// Disable browser-only middleware on server
languageStorage={typeof window !== 'undefined' ? cookieLanguageStorage() : null}
languageDetectors={[]}
>
{children}
</TranslatorProvider>
</body>
</html>
);
}Advanced: using the core directly
TranslatorCore is the framework-agnostic engine underneath the hooks. You can use it directly for non-React contexts (Electron main process, Node.js scripts, etc.).
import { TranslatorCore } from '@unisense.io/react-translator';
const core = new TranslatorCore({
apiUrl: 'https://unisense.io/api',
projectId: 'clxxxxxxxxxxxxxxxx',
apiKey: 'your_api_key',
defaultLocale: 'en',
});
await core.init();
console.log(core.t('homepage.title'));
await core.changeLanguage('fr');
console.log(core.t('homepage.title'));Troubleshooting
Hook must be used inside <TranslatorProvider>
You called a hook (useTranslate, useLanguage, etc.) outside of a component tree wrapped by <TranslatorProvider>.
Translations not loading / 401 Unauthorized
- Verify your
apiKeyis set and valid. Generate one on unisense.io under Project → API Keys. - The SDK calls
GET {apiUrl}/public/{projectId}/translations/{locale}. - Ensure
apiUrlhas no trailing slash.
Component doesn't re-render after changeLanguage()
- React 18+:
useTranslateusesuseSyncExternalStore— re-renders are automatic. Check you're importing from@unisense.io/react-translator(not/legacy). - React 16.8-17: Import from
@unisense.io/react-translator/legacy. Re-renders useuseState + useEffectand are automatic too.
React 18 Strict Mode fires effects twice
This is expected and safe. The TranslatorCore deduplicates concurrent use() calls — the second call reuses the in-flight promise and never issues a duplicate HTTP request.
Related packages
| Package | Framework | Link |
|---|---|---|
| @unisense/ngx-translator | Angular 12 – 21+ | npm |
| @unisense.io/react-translator | React 16.8+ | this package |
License
MIT © Unisense
