Skip to content

Commit e3362a2

Browse files
committed
Add i18next internationalization
- Install i18next and react-i18next with full SSR support - Configure client-side i18n with browser language detection - Configure server-side i18n with filesystem backend - Add improved German translations (idiomatic business German) - Integrate with Vite via i18next-loader plugin - Add i18next middleware to Express server - Update server-side entry to serialize translations for hydration - Update client-side entry to hydrate with server state - Translate all Welcome page content with defaultValue pattern - Document i18n usage and architecture in CLAUDE.md English text stays inline in components as defaultValue. Other languages defined in src/locales/*.json files. Browser automatically detects user's preferred language. No language flicker due to proper SSR hydration.
1 parent deea9da commit e3362a2

File tree

10 files changed

+1001
-94
lines changed

10 files changed

+1001
-94
lines changed

package-lock.json

Lines changed: 771 additions & 50 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,14 @@
3232
"classnames": "^2.5.1",
3333
"compression": "^1.7.5",
3434
"express": "^5.0.1",
35+
"i18next": "^25.6.0",
36+
"i18next-browser-languagedetector": "^8.2.0",
37+
"i18next-fs-backend": "^2.6.0",
38+
"i18next-http-middleware": "^3.8.1",
39+
"i18next-resources-to-backend": "^1.2.1",
3540
"react": "^19.0.0",
3641
"react-dom": "^19.0.0",
42+
"react-i18next": "^16.2.4",
3743
"react-router": "^7.5.2",
3844
"sirv": "^3.0.0"
3945
},
@@ -70,6 +76,7 @@
7076
"stylelint-plugin-carbon-tokens": "^3.2.3",
7177
"stylelint-use-logical-spec": "^5.0.1",
7278
"vite": "^7.0.0",
79+
"vite-plugin-i18next-loader": "^3.1.3",
7380
"vitest": "^3.0.7"
7481
},
7582
"lint-staged": {

src/entry-client.jsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,37 @@
99
import { StrictMode } from 'react';
1010
import { hydrateRoot } from 'react-dom/client';
1111
import { BrowserRouter } from 'react-router';
12+
import { I18nextProvider } from 'react-i18next';
1213
import { Router } from './routes';
1314

1415
// App level imports
1516
import { ThemeProvider } from './context/ThemeContext';
17+
import i18n from './i18n.client.js';
18+
19+
// Hydrate i18n with server state to prevent flicker
20+
const { initialI18nStore, initialLanguage } =
21+
window.__INITIAL_I18N_STATE__ || {};
22+
23+
if (initialI18nStore && initialLanguage) {
24+
// Add server translations to client instance
25+
Object.keys(initialI18nStore).forEach((lng) => {
26+
Object.keys(initialI18nStore[lng]).forEach((ns) => {
27+
i18n.addResourceBundle(lng, ns, initialI18nStore[lng][ns], true, true);
28+
});
29+
});
30+
// Set the language from server
31+
i18n.changeLanguage(initialLanguage);
32+
}
1633

1734
hydrateRoot(
1835
document.getElementById('root'),
1936
<StrictMode>
20-
<ThemeProvider>
21-
<BrowserRouter>
22-
<Router />
23-
</BrowserRouter>
24-
</ThemeProvider>
37+
<I18nextProvider i18n={i18n}>
38+
<ThemeProvider>
39+
<BrowserRouter>
40+
<Router />
41+
</BrowserRouter>
42+
</ThemeProvider>
43+
</I18nextProvider>
2544
</StrictMode>,
2645
);

src/entry-server.jsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,45 @@
99
import { StrictMode } from 'react';
1010
import { renderToPipeableStream } from 'react-dom/server';
1111
import { StaticRouter } from 'react-router';
12+
import { I18nextProvider } from 'react-i18next';
1213

1314
// App level imports
1415
import { Router } from './routes/index.jsx';
1516
import { getStatusCodeForPath } from './routes/utils.js';
1617

1718
/**
1819
* @param {string} url
20+
* @param {import('i18next').i18n} i18n
1921
* @param {import('react-dom/server').RenderToPipeableStreamOptions} [options]
2022
*/
21-
export function render(_url, options) {
23+
export function render(_url, i18n, options) {
2224
const url = `/${_url}`;
2325
const statusCode = getStatusCodeForPath(url);
2426

2527
const { pipe, abort } = renderToPipeableStream(
2628
<StrictMode>
27-
<StaticRouter location={url}>
28-
<Router />
29-
</StaticRouter>
29+
<I18nextProvider i18n={i18n}>
30+
<StaticRouter location={url}>
31+
<Router />
32+
</StaticRouter>
33+
</I18nextProvider>
3034
</StrictMode>,
3135
options,
3236
);
3337

34-
const head = '<meta name="description" content="Server-side rendered page">';
38+
// Serialize i18n state to pass to client
39+
const initialI18nStore = {};
40+
i18n.languages.forEach((lng) => {
41+
initialI18nStore[lng] = i18n.services.resourceStore.data[lng];
42+
});
43+
44+
const initialState = {
45+
initialI18nStore,
46+
initialLanguage: i18n.language,
47+
};
48+
49+
const head = `<meta name="description" content="Server-side rendered page">
50+
<script>window.__INITIAL_I18N_STATE__ = ${JSON.stringify(initialState).replace(/</g, '\\u003c')}</script>`;
3551

3652
return { pipe, head, abort, statusCode };
3753
}

src/i18n.client.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Copyright IBM Corp. 2025
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import i18next from 'i18next';
9+
import { initReactI18next } from 'react-i18next';
10+
import LanguageDetector from 'i18next-browser-languagedetector';
11+
import resourcesToBackend from 'i18next-resources-to-backend';
12+
13+
i18next
14+
// Detect user language
15+
.use(LanguageDetector)
16+
// Pass the i18n instance to react-i18next
17+
.use(initReactI18next)
18+
// Load translation files dynamically
19+
.use(
20+
resourcesToBackend((language, namespace) => {
21+
// Only load non-English translations from files
22+
// English is provided as defaultValue in components
23+
if (language === 'en') {
24+
return Promise.resolve({});
25+
}
26+
return import(`./locales/${language}.json`);
27+
}),
28+
)
29+
// Init i18next
30+
.init({
31+
debug: false,
32+
fallbackLng: 'en',
33+
supportedLngs: ['en', 'de'],
34+
35+
// Important for SSR
36+
useSuspense: false,
37+
38+
// Return key if translation missing (useful with defaultValue pattern)
39+
returnNull: false,
40+
returnEmptyString: false,
41+
42+
interpolation: {
43+
escapeValue: false, // React already escapes values
44+
},
45+
46+
detection: {
47+
// Order of language detection
48+
order: ['navigator', 'htmlTag'],
49+
// Don't cache language in cookies/localStorage for this boilerplate
50+
caches: [],
51+
},
52+
});
53+
54+
export default i18next;

src/i18n.server.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Copyright IBM Corp. 2025
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import i18next from 'i18next';
9+
import { initReactI18next } from 'react-i18next';
10+
import Backend from 'i18next-fs-backend';
11+
import { fileURLToPath } from 'node:url';
12+
import { dirname, resolve } from 'node:path';
13+
14+
const __filename = fileURLToPath(import.meta.url);
15+
const __dirname = dirname(__filename);
16+
17+
i18next
18+
// Load translation files from filesystem
19+
.use(Backend)
20+
// Pass the i18n instance to react-i18next
21+
.use(initReactI18next)
22+
// Init i18next
23+
.init({
24+
debug: false,
25+
fallbackLng: 'en',
26+
supportedLngs: ['en', 'de'],
27+
preload: ['en', 'de'], // Preload all languages on server
28+
29+
// Important for SSR
30+
useSuspense: false,
31+
32+
// Return key if translation missing (useful with defaultValue pattern)
33+
returnNull: false,
34+
returnEmptyString: false,
35+
36+
backend: {
37+
// Path to translation files
38+
loadPath: resolve(__dirname, 'locales/{{lng}}.json'),
39+
},
40+
41+
interpolation: {
42+
escapeValue: false, // React already escapes values
43+
},
44+
});
45+
46+
export default i18next;

src/locales/de.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"welcome": {
3+
"runTemplate": "↳ Template starten",
4+
"nodeRequirement": "Benötigt Node v.20",
5+
"whatIsThis": "↳ Worum geht's?",
6+
"purpose": {
7+
"title": "Verwendungszweck",
8+
"description": "Dieses Repository hilft beim Einstieg in das Carbon Design System mit React. Es spart Zeit durch eine fertig konfigurierte Basis für neue Projekte."
9+
},
10+
"consistency": {
11+
"title": "Standards einhalten",
12+
"description": "Nutzen Sie dies als Referenz, um IBM Design-Standards einzuhalten. Flexibel anpassbar, dabei konsistent in der User Experience."
13+
},
14+
"customize": {
15+
"title": "Individuell anpassen",
16+
"description": "Verstehen Sie dies als Startpunkt und Leitfaden, nicht als starres Framework. Passen Sie es an Ihre Anforderungen an oder nutzen Sie es als Inspiration."
17+
},
18+
"features": "↳ Features",
19+
"dataFetching": {
20+
"title": "↳ Beispiel: Daten laden",
21+
"description": "Hier wird eine Nachricht dynamisch von einem API-Endpunkt geladen. Das zeigt, wie Sie Daten laden können, während Komponenten sauber und Netzwerklogik getrennt bleiben.",
22+
"message": "Nachricht:",
23+
"loading": "Lädt...",
24+
"failed": "Laden fehlgeschlagen"
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)