---
id: ts-typesafe-i18n
title: Type-safe i18n — 키 / 보간 변수 검증
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [typescript, i18n, vibe-coding]
tech_stack: { language: "TypeScript", applicable_to: ["Frontend", "Mobile"] }
applied_in: []
aliases: [typesafe-i18n, lingui, type-safe translations, ICU validation]
---

# Type-safe i18n

> 번역 키 오타 / 변수 누락 = 런타임 에러. **TS 가 컴파일 타임에 잡게** 하라. typesafe-i18n / Lingui / next-intl 의 generated types.

## 📖 핵심 개념
- 번역 catalog → TS type 자동 생성.
- t('foo.bar') 의 'foo.bar' 가 union literal 로 strict check.
- 보간 변수 `{name}` 도 type 으로 강제.

## 💻 코드 패턴

### typesafe-i18n
```jsonc
// .typesafe-i18n.json
{
  "baseLocale": "en",
  "outputFormat": "TypeScript"
}
```

```ts
// src/i18n/en/index.ts
const en = {
  greeting: 'Hello {name:string}',
  items: '{count:number, plural, one{# item} other{# items}}',
} as const satisfies BaseTranslation;
```

```bash
npx typesafe-i18n
# src/i18n/i18n-types.ts 자동 생성
```

```ts
import LL from './i18n/i18n-svelte';

LL.greeting({ name: 'Alice' }); // OK
LL.greeting({});                 // ❌ name required
LL.foo();                        // ❌ no key
```

### next-intl + 자동 type
```ts
// messages/en.json
{ "greeting": "Hello {name}" }

// global.d.ts
declare global {
  type Messages = typeof import('./messages/en.json');
  interface IntlMessages extends Messages {}
}
```

```ts
import { useTranslations } from 'next-intl';

const t = useTranslations();
t('greeting', { name: 'A' });   // OK
t('does.not.exist');             // ❌ TS error
```

### Lingui (compile-time)
```tsx
import { Trans, t } from '@lingui/macro';

<Trans>Hello {name}</Trans>;
const msg = t`You have ${n} items`;
// catalog 추출 + extract type 자동
```

### 자체 type-safe wrapper
```ts
// 모든 키 + 변수 명시
const messages = {
  'cart.empty': (): string => 'Your cart is empty',
  'cart.items': (vars: { count: number }) =>
    `${vars.count} ${vars.count === 1 ? 'item' : 'items'}`,
  'auth.welcome': (vars: { name: string }) => `Welcome, ${vars.name}`,
} as const;

type MessageKey = keyof typeof messages;
type MessageVars<K extends MessageKey> = Parameters<typeof messages[K]>[0];

function t<K extends MessageKey>(key: K, vars?: MessageVars<K>): string {
  return (messages[key] as any)(vars ?? {});
}

t('auth.welcome', { name: 'A' }); // OK
t('auth.welcome');                 // ❌ name required
t('does.not.exist' as any);        // 우회 시 cast 필요
```

### Plural 검증
```ts
const fmt = new Intl.PluralRules('en');

function pluralize(count: number, forms: { one: string; other: string }) {
  return forms[fmt.select(count) as 'one' | 'other'];
}

pluralize(1, { one: 'item', other: 'items' });
```

### CI: 누락된 번역 검출
```bash
# extract baseLocale → 다른 locale 검사
node scripts/check-translations.js
# en 에 있는 키가 ko / ja 에 모두 있는지
```

```ts
import en from './en.json';
import ko from './ko.json';

const missing = Object.keys(en).filter(k => !(k in ko));
if (missing.length) { console.error('Missing in ko:', missing); process.exit(1); }
```

## 🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| Next.js | next-intl + global type |
| 강 type + 자동 codegen | typesafe-i18n |
| 작은 bundle | Lingui (compile-time) |
| Mobile (RN) | typesafe-i18n / lingui |
| 자체 시스템 | 위 wrapper 패턴 |
| 런타임 dynamic key | Type 한계 — 런타임 검사 |

## ❌ 안티패턴
- **`t('random' + key)`**: dynamic — type 검사 못 함.
- **번역 누락 검사 없음**: 런타임 fallback 만.
- **변수 type 무시 (모든 string)**: number 가 string 으로 들어감.
- **Generated type commit X**: build 매번 — 잊으면 깨짐.
- **Catalog 거대 (1만 키)**: split. namespace.
- **Default value t API**: type-safe 깨짐 — i18next default 권장 X.

## 🤖 LLM 활용 힌트
- next-intl + global Messages type 가 가장 단순.
- typesafe-i18n / Lingui = full-featured.
- CI 에서 누락 키 검사 자동.

## 🔗 관련 문서
- [[Frontend_i18n_Patterns]]
- [[TS_tsconfig_Strategy]]
