AgentSkillsCN

i18n-localization

国际化与本地化模式。识别硬编码字符串,管理翻译资源、Locale 文件以及从右向左书写(RTL)的支持。

SKILL.md
--- frontmatter
name: i18n-localization
description: Internationalization and localization patterns. Detecting hardcoded strings, managing translations, locale files, RTL support.
allowed-tools: Read, Glob, Grep

i18n & Localization

Internationalization (i18n) and Localization (L10n) best practices.


1. Core Concepts

TermMeaning
i18nInternationalization - making app translatable
L10nLocalization - actual translations
LocaleLanguage + Region (en-US, tr-TR)
RTLRight-to-left languages (Arabic, Hebrew)

2. When to Use i18n

Project Typei18n Needed?
Public web app✅ Yes
SaaS product✅ Yes
Internal tool⚠️ Maybe
Single-region app⚠️ Consider future
Personal project❌ Optional

3. Implementation Patterns

Paraglide JS (Recommended for this project)

tsx
import { m } from '@/paraglide/messages'
import { getLocale, setLocale, locales } from '@/paraglide/runtime'

// Simple message
function Welcome() {
  return <h1>{m.welcome_title()}</h1>
}

// Message with parameters
function Greeting() {
  return <p>{m.example_message({ username: 'Alice' })}</p>
}

// Locale switching
function LocaleSwitcher() {
  const currentLocale = getLocale()
  return (
    <select value={currentLocale} onChange={(e) => setLocale(e.target.value)}>
      {locales.map(locale => (
        <option key={locale} value={locale}>{locale.toUpperCase()}</option>
      ))}
    </select>
  )
}

React (react-i18next)

tsx
import { useTranslation } from 'react-i18next';

function Welcome() {
  const { t } = useTranslation();
  return <h1>{t('welcome.title')}</h1>;
}

Next.js (next-intl)

tsx
import { useTranslations } from 'next-intl';

export default function Page() {
  const t = useTranslations('Home');
  return <h1>{t('title')}</h1>;
}

Python (gettext)

python
from gettext import gettext as _

print(_("Welcome to our app"))

4. File Structure

Paraglide JS (This Project)

code
project.inlang/
├── settings.json          # Inlang config
messages/
├── en.json               # English translations
└── no.json               # Norwegian translations
src/paraglide/            # Auto-generated by Vite plugin
├── messages.ts           # Type-safe message functions
└── runtime.ts            # getLocale, setLocale, locales

Traditional Structure (Other Projects)

code
locales/
├── en/
│   ├── common.json
│   ├── auth.json
│   └── errors.json
├── no/
│   ├── common.json
│   ├── auth.json
│   └── errors.json

5. Paraglide JS Deep Dive

Setup & Configuration

Inlang Project Settings (project.inlang/settings.json):

json
{
  "$schema": "https://inlang.com/schema/project-settings",
  "baseLocale": "en",
  "locales": ["en", "no"],
  "modules": [
    "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
    "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
  ],
  "plugin.inlang.messageFormat": {
    "pathPattern": "./messages/{locale}.json"
  }
}

Vite Plugin Configuration (vite.config.ts):

tsx
import { paraglideVitePlugin } from '@inlang/paraglide-js-adapter-vite'

export default defineConfig({
  plugins: [
    paraglideVitePlugin({
      project: './project.inlang',
      outdir: './src/paraglide',
      strategy: ['url'],  // URL-based locale switching
    }),
  ],
})

Message Files Format

Messages are stored as flat JSON objects with dot-notation keys:

messages/en.json:

json
{
  "welcome_title": "Welcome",
  "example_message": "Hello {username}!",
  "current_locale": "Current locale: {locale}",
  "nav_home": "Home",
  "nav_about": "About us"
}

messages/no.json:

json
{
  "welcome_title": "Velkommen",
  "example_message": "Hallo {username}!",
  "current_locale": "Gjeldende språk: {locale}",
  "nav_home": "Hjem",
  "nav_about": "Om oss"
}

Using Messages in Components

Simple messages (no parameters):

tsx
import { m } from '@/paraglide/messages'

function Header() {
  return <h1>{m.welcome_title()}</h1>
}

Messages with parameters:

tsx
function Greeting({ username }) {
  return <p>{m.example_message({ username })}</p>
}

Locale switching:

tsx
import { getLocale, setLocale, locales } from '@/paraglide/runtime'

function LocaleSwitcher() {
  const currentLocale = getLocale()

  return (
    <div>
      <span>{m.current_locale({ locale: currentLocale })}</span>
      <select value={currentLocale} onChange={(e) => setLocale(e.target.value)}>
        {locales.map(locale => (
          <option key={locale} value={locale}>{locale.toUpperCase()}</option>
        ))}
      </select>
    </div>
  )
}

With TanStack Router

Paraglide JS integrates seamlessly with TanStack Router for URL-based locale switching:

tsx
import { createFileRoute } from '@tanstack/react-router'
import { m } from '@/paraglide/messages'
import { getLocale, setLocale } from '@/paraglide/runtime'

export const Route = createFileRoute('/demo/i18n')({
  component: DemoPage,
})

function DemoPage() {
  return (
    <div>
      <h1>{m.welcome_title()}</h1>
      <p>{m.example_message({ username: 'TanStack Router' })}</p>
      <LocaleSwitcher />
    </div>
  )
}

Key Advantages

  • Type-safe: Auto-generated TypeScript functions
  • Zero runtime overhead: Compiled at build time
  • IDE support: Full autocomplete for message keys
  • No hooks needed: Simple function calls
  • Automatic locale detection: URL-based switching
  • Inlang ecosystem: Integrates with translation management tools

6. Best Practices

DO ✅

  • Use translation keys, not raw text
  • Namespace translations by feature (use dot notation: nav.home, errors.not_found)
  • Support pluralization with ICU format
  • Handle date/number formats per locale using Intl API
  • Use Paraglide's m() function for all user-facing strings
  • Keep message keys descriptive and consistent
  • Test locale switching in development
  • Use getLocale() to access current locale when needed

DON'T ❌

  • Hardcode strings in components
  • Concatenate translated strings (use parameters instead)
  • Assume text length (German is 30% longer, Chinese is shorter)
  • Mix languages in same file
  • Use m() in non-component contexts without proper setup
  • Forget to update all locale files when adding new messages

7. Common Issues

IssueSolution
Missing translationFallback to default language (baseLocale)
Hardcoded stringsUse linter/checker script (see Script section)
Date formatUse Intl.DateTimeFormat per locale
Number formatUse Intl.NumberFormat per locale
PluralizationUse ICU message format in JSON
m() not foundEnsure Vite plugin is configured and src/paraglide/ is generated
Locale not switchingCheck setLocale() is called and router is configured
Type errors on m.Run Vite dev server to regenerate src/paraglide/messages.ts
Missing keys in localeRun i18n_checker.py to find mismatches

8. RTL Support

css
/* CSS Logical Properties */
.container {
  margin-inline-start: 1rem;  /* Not margin-left */
  padding-inline-end: 1rem;   /* Not padding-right */
}

[dir="rtl"] .icon {
  transform: scaleX(-1);
}

9. Checklist

Before shipping:

  • All user-facing strings use m.key() function calls
  • Locale files exist for all supported languages (en, no, etc.)
  • Date/number formatting uses Intl API
  • RTL layout tested (if applicable)
  • Base locale configured in project.inlang/settings.json
  • No hardcoded strings in components
  • Vite plugin generates src/paraglide/ correctly
  • Locale switching works with setLocale()
  • i18n_checker.py passes with no critical issues
  • All message keys have translations in all locales

Script

ScriptPurposeCommand
scripts/i18n_checker.pyDetect hardcoded strings & missing translationspython scripts/i18n_checker.py <project_path>