Skip to main content

Rendering translations in your app

There is no Multilocale runtime package

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>
)
}
If your keys are whole English sentences

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: download detects AndroidManifest.xml and writes res/values-<locale>/strings.xml, XML- and quote-escaped. getString(R.string.key) works unchanged.
  • iOS / macOS: download --format swift writes <locale>.lproj/Localizable.strings. NSLocalizedString works unchanged.

Working examples

Each is a complete app, built in CI, with its translations committed:

ExampleRuntime it renders with
Next.jsnext-intl
Remixreact-i18next + remix-i18next
Gatsbyreact-i18next
LinguiLingui
JekyllJekyll data files
iOSLocalizable.strings
Androidstrings.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.