Skip to content
Back to skills

Set Up I18n

ASecurity

Use when internationalizing a frontend project — sets up i18next + react-i18next (React) or vue-i18n (Vue) with typed message keys, lazy-loaded per-locale catalogs, Intl-based number/date/relative formatting, and a persisted locale choice detected from the browser.

  • 5 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 10, 2026
developmenttypescriptbashreactvueapifrontend

Works with

  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned October 10, 2026

npx -y skills add VelimirMueller/lab-claude-skills --skill set-up-i18n --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Set Up I18n?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Set Up I18n
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/velimirmueller-set-up-i18n-lab-claude-skills/badge)](https://www.skillsdirectory.com/skills/velimirmueller-set-up-i18n-lab-claude-skills)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: set-up-i18n
description: Use when internationalizing a frontend project — sets up i18next + react-i18next (React) or vue-i18n (Vue) with typed message keys, lazy-loaded per-locale catalogs, Intl-based number/date/relative formatting, and a persisted locale choice detected from the browser.
---

# Set Up i18n

## 1. Audit current state

```bash
cat .claude/stack-profile.md 2>/dev/null || cat ~/.claude/stack-profile.md 2>/dev/null   # frontend.framework, frontend.meta, package_manager
grep -E '"(i18next|react-i18next|vue-i18n)"' package.json 2>/dev/null
ls src/locales 2>/dev/null
grep -rn ">[A-Z][a-z]\+ [a-z]" src/components 2>/dev/null | head   # hardcoded UI strings (rough)
```

Read `frontend.framework` (react → i18next, vue → vue-i18n) and `frontend.meta`: `nuxt` → `@nuxtjs/i18n`, `next` → `next-intl` or the framework's routing i18n; this skill is the Vite-SPA path. Commands use pnpm; translate for `package_manager`. **Prerequisite:** `@/` alias. The locale switcher is UI state (per `set-up-state-management`).

## 2. Decide what to do
- No i18n → full setup.
- Library present, strings still hardcoded → extract to catalogs (step 5) + type the keys (step 7).
- Set up, no lazy loading → split catalogs per locale (step 6).

## 3. Detect framework
React → **i18next** + **react-i18next**. Vue → **vue-i18n** (Composition API). Both format with the **`Intl`** APIs. Why these libraries: `i18n-patterns.md`.

## 4. Install

### React
```bash
pnpm add i18next react-i18next
```
### Vue
```bash
pnpm add vue-i18n
```

## 5. Message catalogs

```
src/locales/
├── en/common.json
└── de/common.json
```
```json
// src/locales/en/common.json
{
  "greeting": "Hello, {{name}}",
  "todos": { "count_one": "{{count}} todo", "count_other": "{{count}} todos" }
}
```
Keys are namespaced and use interpolation + plurals — never concatenate sentence fragments.

**Default locale pair: `de` + `en`.** Both catalogs ship from day one with the same keys; `en` is the fallback (and the type source for keys). German strings run ~30 % longer — design components to wrap, never to fit one English word. Use the informal or formal German address consistently per product (`du` vs `Sie`), decided once in the catalog, not per string.

## 6. Initialize (with the default locale only; lazy-load the rest)

```ts
// src/libs/i18n.ts (React)
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from '@/locales/en/common.json';

export const SUPPORTED_LOCALES = ['de', 'en'] as const;
export type Locale = (typeof SUPPORTED_LOCALES)[number];
const browser = navigator.language.split('-')[0];
export const initialLocale: Locale = SUPPORTED_LOCALES.includes(browser as Locale)
  ? (browser as Locale)
  : 'en';

i18n.use(initReactI18next).init({
  resources: { en: { common: en } },
  lng: 'en', // switched to initialLocale by loadLocale() at boot — no untranslated flash
  fallbackLng: 'en',
  defaultNS: 'common',
  interpolation: { escapeValue: false }, // React already escapes
});

export async function loadLocale(lng: Locale) {
  if (!i18n.hasResourceBundle(lng, 'common')) {
    const messages = await import(`@/locales/${lng}/common.json`);
    i18n.addResourceBundle(lng, 'common', messages.default);
  }
  await i18n.changeLanguage(lng);
}

export default i18n;
```
Only `en` ships in the bundle (the `@/` alias in the dynamic `import()` must be an absolute path in `vite.config.ts`, as the `configure-typescript` alias setup has it; Vite prints an `INEFFECTIVE_DYNAMIC_IMPORT` notice for `en` because it is also imported statically — harmless, `loadLocale` skips it); `main.tsx` awaits `loadLocale(persistedLocale ?? initialLocale)` before the first render, so a German visitor never sees English first. `persistedLocale` comes from the locale store (step 9); `initialLocale` covers first visits. In Nuxt/Next, `navigator` does not exist on the server — use `@nuxtjs/i18n` / the request's `Accept-Language` instead of this module. Vue: `createI18n({ legacy: false, locale, fallbackLocale: 'en', messages: { en } })` + dynamic `import()` + `i18n.global.setLocaleMessage` to lazy-add.

## 7. Type the keys (autocomplete + no missing-key bugs)

```ts
// src/types/i18next.d.ts (React)
import 'i18next';
import type common from '@/locales/en/common.json';

declare module 'i18next' {
  interface CustomTypeOptions {
    defaultNS: 'common';
    resources: { common: typeof common };
  }
}
```
Now `t('greeting')` autocompletes and `t('typo')` is a type error. Limit: i18next infers interpolation variables only from `as const` TS resources or `.d.ts` interfaces, not from JSON, so `t('greeting', { nam: 'x' })` is not caught.

Vue (vue-i18n 11): declare the schema globally from the default catalog. Do not use `createI18n<[MessageSchema], 'en' | 'de'>` here: it requires every listed locale in `messages`, which defeats lazy loading.
```ts
// src/libs/i18n.ts
import { createI18n } from 'vue-i18n';
import enMessages from '@/locales/en/common.json';

declare module 'vue-i18n' {
  export interface DefineLocaleMessage extends Readonly<typeof enMessages> {}
}

export const i18n = createI18n({
  legacy: false, // Composition API
  locale: 'en',
  fallbackLocale: 'en',
  messages: { en: enMessages },
});
```
Limit (checked against vue-i18n 11.4): `t()` accepts any string key, so the schema gives autocomplete and typed results, **not** a compile error on `t('typo')`. Close the gap in lint with `@intlify/eslint-plugin-vue-i18n` (`no-missing-keys`) and with the key-parity test in `i18n-patterns.md`. Vue catalogs use `{name}` and pipe plurals (`"no todos | one todo | {count} todos"`), not the `{{name}}` / `_one` form from step 5.

## 8. Format with `Intl`, not by hand

```ts
new Intl.NumberFormat(locale, { style: 'currency', currency: 'EUR' }).format(amount);
new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(date);
new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(-1, 'day'); // "yesterday"
```
i18next (`t('k', { val, formatParams })`) and vue-i18n (`$n`/`$d`) wrap `Intl` — use those or `Intl` directly. Never hand-roll date/number/plural logic.

## 9. Locale switch = persisted UI state

A small store holds the chosen locale; switching lazy-loads then sets it:
```ts
// on switch: await loadLocale(next); store.setLocale(next);  (loadLocale also changes the language)
document.documentElement.lang = next; // screen readers pick the voice from <html lang>; see set-up-document-head
```
Persist the choice (like the theme store) and default to the browser locale on first visit.

## 10. Verify
```bash
pnpm typecheck   # typed keys compile; a wrong key fails
pnpm dev            # switch locale → catalog lazy-loads, strings + formats update
```

## References
- ./i18n-patterns.md — typed keys, lazy loading, Intl formatting, locale-is-UI-state, plurals/interpolation, extraction, when to skip.
- ../_shared/glossary.md — locale choice as UI state.
- ../_shared/conventions.md — `@/` alias, `libs/` seam.

Files in this skill

  • SKILL.md6.9 KB
  • i18n-patterns.md4.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.