You're Doing Next.js i18n Wrong
Internationalization in JavaScript has settled into a flawed convention: extract every user-facing string into a JSON file, assign it a key, and reference that key in your components. t('home.hero.title') instead of the text itself. Your UI lives in one place, your content lives in another.
This works. Thousands of apps ship this way. But it's not a great developer experience.
Reading t('checkout.summary.total') in a code review tells you nothing — you have to open a JSON file to see what changed. Keys have to be invented, namespaced, and kept in sync. Stale translations accumulate because nobody knows which keys are still in use. The problem is widespread enough that entire categories of tooling exist just to manage it: IDE extensions that auto-suggest keys, type generators that validate them, linters that flag unused ones. These tools are solving a problem that a poorly designed paradigm created.
The <T> component
Content should not be separated from where it's used. A component that renders a heading, a paragraph, and a button should be the single source of truth for what those elements say — not a proxy that points to strings stored somewhere else. When your code and your translations are two parallel systems, they drift. Invariably.
What if the library worked the other way around — adapting to your code instead of asking you to restructure it? Here's what i18n should look like.
import { T } from 'gt-next';
function Hero() {
return (
<T>
<h1>Ship your product worldwide</h1>
<p>Reach every market without rewriting your app.</p>
</T>
);
}Wrap your JSX in <T>. The English text stays right where you wrote it. When a user visits in Spanish or Japanese, the content inside <T> is translated — structure, formatting, and all.
No keys. No JSON files. No cross-referencing. The source of truth is your code.
Setup
The syntax above comes from gt-next, an open-source i18n library for Next.js App Router. Getting started takes one command:
npx gtx-cli@latest initThe setup wizard installs dependencies, wraps your Next.js config with withGTConfig, adds GTProvider to your root layout, creates a gt.config.json with your locales, sets up dev API keys for translation hot reloading, and configures CDN translation storage — all interactively.
Once that's done, wrap content in <T>, run your dev server, and use the <LocaleSelector> component to switch between languages:
import { LocaleSelector } from 'gt-next';
function Header() {
return (
<header>
<nav>{/* ... */}</nav>
<LocaleSelector />
</header>
);
}Translations happen on-demand in development so you can see your app in any language immediately.
Deployment
In production, translations are pre-generated.
-
Get a production API key from dash.generaltranslation.com. Production keys start with
gtx-api-(different from thegtx-dev-keys used locally). -
Add the translate step to your build:
{
"scripts": {
"build": "npx gtx-cli translate --publish && next build"
}
}The translate command scans your codebase for all <T> usage, generates translations, and publishes them to a CDN. When your app builds, every locale is ready.
Next steps
- Variable components — handle dynamic content inside
<T>with<Var>,<Num>, and<Currency> - Branching components — conditionally render content based on locale with
<Plural>and<Branch> useGTandgetGT— translate plain strings for attributes, placeholders, and metadata- Standalone mode — use gt-next without the General Translation platform