Rendering translations in your app
Nothing on npm called @multilocale/react or @multilocale/multilocale-react is
maintained or installable — the first was never published, the second was
abandoned in 2023. If a search result, an old tutorial or a model tells you to
npm install one of them, it is wrong.
The maintained package is multilocale,
and it is a CLI. It moves files. Your framework's own i18n runtime renders
them.
The shape of it
multilocale.com ← where copy is written and machine-translated
│
│ npx multilocale download (reads multilocale.json)
▼
translations/<lang>.json ← committed to git, one dictionary per language
│
│ react-i18next / next-intl / Lingui / Android / iOS
▼
your components ← t('welcome_message')
Two properties fall out of this that a runtime fetch cannot give you:
- The site builds statically and works offline. Translations are files in the repository, so an outage at multilocale.com cannot take your app down, and a page renders in Japanese without a network round trip.
- Translations are reviewable. A translation change is a diff in a pull request, tied to the commit that shipped it.
The trade is that publishing new copy needs a download and a deploy rather
than being live instantly. If you want that, fetch the dictionary yourself from
GET /api/phrases — but for almost every app, files are the
right answer.
1. Get the files
npx multilocale signup --email you@example.com --json # or: npx multilocale login
npx multilocale projects create my-app --locales en,es,fr --default-locale en
npx multilocale projects update my-app --paths "translations/%lang%.json"
npx multilocale import # push the strings you already have (once)
npx multilocale localize all # machine-translate into every project locale
npx multilocale download # write translations/<lang>.json
See the CLI guide for the whole command set, and
Configuration for paths and output formats. Commit the
downloaded files.
2. Render them
React with react-i18next
npm install i18next react-i18next
// i18n.js
import { createInstance } from 'i18next'
import { initReactI18next } from 'react-i18next'
import en from './translations/en.json'
import es from './translations/es.json'
const i18n = createInstance()
i18n.use(initReactI18next).init({
lng: 'en',
resources: {
en: { translation: en },
es: { translation: es },
},
interpolation: { escapeValue: false },
})
export default i18n
import { I18nextProvider, useTranslation } from 'react-i18next'
import i18n from './i18n.js'
function WelcomeBanner() {
const { t } = useTranslation()
return <h1>{t('welcome_message')}</h1>
}
export default function App() {
return (
<I18nextProvider i18n={i18n}>
<WelcomeBanner />
</I18nextProvider>
)
}
Using the source string as the key is a good habit — an untranslated key then
renders readable English instead of welcome_message. But English sentences
contain . and :, which i18next reads as its nested-key and namespace
separators, so t('Read more.') silently renders nothing. Turn both off:
i18n.use(initReactI18next).init({
keySeparator: false,
nsSeparator: false,
// …
})
For server-rendered frameworks, create one i18next instance per locale
rather than calling changeLanguage() on a shared one: a static build renders
many pages in the same process, and one mutable instance lets one page's
language leak into another page's HTML.
Next.js with next-intl
npm install next-intl
Point multilocale.json at the directory next-intl reads, and let it do the
rest:
{
"projectId": "…",
"format": "json",
"extension": "json",
"paths": ["messages/%lang%.json"]
}
// i18n/request.js
import { getRequestConfig } from 'next-intl/server'
export default getRequestConfig(async ({ requestLocale }) => {
const locale = await requestLocale
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
}
})
import { useTranslations } from 'next-intl'
export default function Page() {
const t = useTranslations()
return <h1>{t('welcome_message')}</h1>
}
next-intl expects nested message objects while Multilocale stores flat key/value pairs, so reshape after every download with a post-script rather than by hand:
{ "postScript": "node scripts/normalizeMessages.mjs" }
Lingui
Point paths at the catalog directory Lingui reads
(src/locales/%lang%/messages.json) and reshape the flat download into Lingui's
catalog format in the post-script, before lingui compile runs. The
Lingui example
does exactly this.
Android and iOS
The CLI writes the platform's native format directly — no library at all:
- Android:
downloaddetectsAndroidManifest.xmland writesres/values-<locale>/strings.xml, XML- and quote-escaped.getString(R.string.key)works unchanged. - iOS / macOS:
download --format swiftwrites<locale>.lproj/Localizable.strings.NSLocalizedStringworks unchanged.
Working examples
Each is a complete app, built in CI, with its translations committed:
| Example | Runtime it renders with |
|---|---|
| Next.js | next-intl |
| Remix | react-i18next + remix-i18next |
| Gatsby | react-i18next |
| Lingui | Lingui |
| Jekyll | Jekyll data files |
| iOS | Localizable.strings |
| Android | strings.xml |
Fetching at runtime instead
If you do want dictionaries over the network — an app that must pick up copy changes without a deploy — call the API yourself and cache the result:
curl "https://api.multilocale.com/api/phrases?organizationId=ORGANIZATION_ID&project=my-app&language=es"
That path is anonymous and read-only, and is meant for a deployed application reading one known public project dictionary. See Phrases for the parameters and the response shape.