Localising your React app the 'Tailwind' way: no keys & no JSON, just code
Source: Dev.to
So, you want to localise your app
Most i18n solutions push you towards a familiar pattern:
- Giant JSON/YAML files
- Tediously “naming” your translations via keys
- Made‑up string interpolation syntax
- Constantly jumping back and forth between UI code and translation files
Which works fine, but the ceremony can start to feel a little tedious after a while.
I remember a few years ago, when I first picked up Tailwind CSS, I was shocked by how much faster I could ship real UI once I started writing only what was needed. I wanted the same feeling for i18n.
Introducing react‑scoped‑i18n 🌐
The core idea is simple: instead of global translation files and keys, translations live right next to the components that render them.
How does i18n typically look?
// en.json
{
"profile": {
"header": "Hello, {{name}}"
}
}
// es.json
{
"profile": {
"header": "Hola, {{name}}"
}
}
// Header.tsx
export const Header = () => {
const { t } = useI18n();
return (
<h1>{t("profile.header", { name: "John" })}</h1>
);
};
This approach works and is battle‑tested, but it brings a few pain points:
- Typescript can’t help much without extra tooling.
- Translation keys give very little context about what actually renders.
- You cannot search the codebase for rendered text and find the component using it directly.
- String interpolation uses a custom syntax.
What does the “scoped” approach look like?
// Header.tsx
export const Header = () => {
const { t } = useI18n();
const name = "John";
return (
<h1>{t({
en: `Hello, ${name}`,
es: `Hola, ${name}`,
})}</h1>
);
};
No keys, no giant JSON files, no custom interpolation syntax—just plain code!
Note: This isn’t meant to replace full‑blown i18n platforms. It’s an alternative for code‑driven apps that need fast localisation.
Biggest benefits of this approach
Typesafety 🩵
Since it’s just code, everything is type‑safe.
createI18nContext({
languages: ["en", "es", "sl"],
fallbackLanguage: "en", // inferred from `languages` above
});
If a translation is missing, TypeScript flags it at compile time:
// ❌ Property 'es' is missing in type { en: string; sl: string; }
t({
en: "Hello",
sl: "Pozdravljeni",
});
Missing translations or unsupported languages become compile‑time errors instead of runtime surprises. 😄
No naming of keys
The content is the key. Searching for rendered text lands you directly in the component—a huge DX win. 😊
No additional build steps
The library works within the React Context ecosystem. After installing, initialise the context, wrap your app with the exported provider, and you’re good to go.
Out‑of‑the‑box number, date, time & currency formatting ‼️
react‑scoped‑i18n leverages the native Intl API, so as long as you use standard locale identifiers (en, en‑GB, es, es‑ES, …) you get full formatting support with zero extra configuration.
Typesafe “shared” translations
For strings shared throughout the app, define them via the commons API:
createI18nContext({
languages: ["en", "es"],
fallbackLanguage: "en",
commons: {
continue: {
en: "Continue",
es: "Continuar",
},
},
});
Then use them in a type‑safe way that works with IDE autocomplete:
{t(commons.continue)}
Pluralisation (ICU‑inspired, not ICU‑strict)
tPlural lets each language define only the categories it actually needs.
{tPlural(count, {
en: {
one: `You have one apple.`,
many: `You have ${count} apples.`,
},
sl: {
one: `Imaš eno jabolko.`,
two: `Imaš dve jabolki.`, // dual form in Slovenian
many: `Imaš ${count} jabolk.`,
},
})}
You can also target specific values outside of ICU categories:
{tPlural(count, {
en: {
one: `You have one apple.`,
many: `You have ${count} apples.`,
42: `You have THE PERFECT amount of apples!`,
},
es: {
one: `Tienes una manzana.`,
many: `Tienes ${count} manzanas.`,
},
})}
When this isn’t the right fit
(Continue with the rest of your original content…)
A Good Fit
This approach is developer‑centric by design, so if your workflow relies on:
- external translators
- “Crowdin”, “Lokalise”, or similar tools
- non‑technical editors touching translation files
- A LOT of supported languages
then this probably isn’t the right approach.
But if translations are written and maintained in code, and your supported language set is small‑to‑medium, the developer experience ends up being surprisingly nice.
If you’re curious, the project is open source. You can check it out:
- GitHub:
- npm:
Happy to hear any feedback!