---
name: sonamu-i18n
description: Sonamu internationalization (i18n) support. Dictionary configuration, SD function usage, helper functions. Use when implementing internationalization.
---

# i18n (Internationalization)

## Configuration

`sonamu.config.ts`:

```typescript
export default defineConfig({
  i18n: {
    defaultLocale: "ko",
    supportedLocales: ["ko", "en"],
  },
  // ...
});
```

## File Structure

```
packages/api/src/i18n/
├── ko.ts           # Default locale (defaultLocale)
├── en.ts           # Additional locale
└── sd.generated.ts # Auto-generated (pnpm sync)
```

## Dictionary Files

### Default locale (ko.ts)

```typescript
import { createFormat, josa } from "sonamu/dict";

const format = createFormat("ko");

export default {
  "common.save": "저장",
  "common.cancel": "취소",
  "validation.required": (field: string) => `${josa(field, "은는")} 필수입니다`,
  "common.results": (count: number) => `${count}개 결과`,
  test: (date: Date) => format.date(date),
} as const;
```

### Additional locale (en.ts)

```typescript
import { plural } from "sonamu/dict";
import { defineLocale } from "./sd.generated";

export default defineLocale({
  "common.save": "Save",
  "common.cancel": "Cancel",
  "validation.required": (field: string) => `${field} is required`,
  "common.results": (count: number) =>
    plural(count, { one: `${count} result`, other: `${count} results` }),
});
```

## Using the SD Function

### Basic Usage

```typescript
import { SD } from "@/i18n/sd.generated";

// Simple string
SD("common.save"); // → "저장" (ko) / "Save" (en)

// Function form (with parameters)
SD("validation.required")("이메일"); // → "이메일은 필수입니다"
SD("common.results")(5); // → "5개 결과" (ko) / "5 results" (en)
```

### Force a Specific Locale

```typescript
const EN = SD.locale("en");
EN("common.save"); // → "Save" (always English)
```

### Enum Labels

```typescript
// Enum labels defined in entity.json
SD.enumLabels("ProjectStatus")["in_progress"]; // → "진행중"
```

## Helper Functions

### plural (pluralization)

```typescript
import { plural } from "sonamu/dict";

// Handle singular/plural in English
plural(count, {
  zero: "No items", // count === 0
  one: "1 item", // count === 1
  other: `${count} items`, // everything else
});
```

### josa (Korean postposition helper)

```typescript
import { josa } from "sonamu/dict";

josa("이메일", "은는"); // → "이메일은"
josa("이름", "이가"); // → "이름이"
josa("파일", "을를"); // → "파일을"
josa("회사", "과와"); // → "회사와"
josa("서울", "으로"); // → "서울로"
```

### createFormat (number/date formatting)

```typescript
import { createFormat } from "sonamu/dict";

const format = createFormat("ko");
format.number(1234567); // → "1,234,567"
format.date(new Date()); // → "2024. 1. 15."
```

## Entity Integration

Entity title, prop descriptions, and enum labels are automatically included in the dictionary.

```
entity.{EntityId}              → Entity title
entity.{EntityId}.{propName}   → Property description
enum.{EnumId}.{value}          → Enum value label
```

Example:

```typescript
SD("entity.User"); // → "사용자"
SD("entity.User.email"); // → "이메일"
SD("enum.UserRole.admin"); // → "관리자"
```

## localizedColumn

Returns the value for the current locale when locale-specific columns or locale maps exist in the DB:

```typescript
import { localizedColumn } from "@/i18n/sd.generated";

// DB: { name: "태그", name_ko: "태그", name_en: "Tag" }
localizedColumn(tag, "name"); // → "태그" (ko) / "Tag" (en)

// DB: { name: { ko: ["태그"], en: ["Tag"] } }
localizedColumn(tag, "name"); // → ["태그"] (ko) / ["Tag"] (en)
```

- `string[]` values are returned as arrays instead of being stringified.
- Unsupported Context locales use `defaultLocale` before lookup.
- Direct suffix/base scalar values such as numbers are stringified for compatibility.
- Locale map values support only `string` and `string[]`.

## Sonamu UI Management

`http://localhost:34900/sonamu-ui` → i18n tab:

- Browse/edit dictionary
- Add/delete keys
- Excel import/export
- Detect unused keys

## Key Naming Conventions

| Pattern        | Purpose                   | Example                                   |
| -------------- | ------------------------- | ----------------------------------------- |
| `common.*`     | Common UI text            | `common.save`, `common.cancel`            |
| `error.*`      | Error messages            | `error.notFound`, `error.forbidden`       |
| `validation.*` | Validation messages       | `validation.required`, `validation.email` |
| `entity.*`     | Entity-related            | `entity.User`, `entity.User.email`        |
| `enum.*`       | Enum labels               | `enum.UserRole.admin`                     |
| `menu.*`       | Menu/navigation           | `menu.home`, `menu.settings`              |
| `rc.*`         | react-components built-in | `rc.pagination.next`                      |

## Troubleshooting

### LocalizedString Type Error

When you see `Type 'string' is not assignable to type '{ __brand: "LocalizedString"; }'`:

**Cause 1: Missing i18n key in ko.ts**

```typescript
// Wrong: type error when key is missing from ko.ts
SD("validation.password"); // key not in ko.ts
```

Fix: Add the key to ko.ts and run `pnpm sync`

**Cause 2: Not using SD() in the model**

```typescript
// Wrong: using a raw string directly
throw new BadRequestException("이메일이 잘못되었습니다");

// Correct: use the SD() function
throw new BadRequestException(SD("error.invalidEmail"));
```

All user-facing strings must be wrapped with the SD() function.

---

## Type Safety

### String PK Support

When an Entity uses a String PK, the i18n function types must reflect this.

**BAD: Number only**

```typescript
// ko.ts
notFound: (name: string, id: number) => `존재하지 않는 ${name} ID ${id}`,

// Type error when used with String PK entities
// Account, Session, User, Verification, etc.
throw new NotFoundException(SD("notFound")(this.modelName, id));
// Error: Argument of type 'string' is not assignable to parameter of type 'number'
```

**GOOD: Support both with a union type**

```typescript
// ko.ts
notFound: (name: string, id: number | string) => `존재하지 않는 ${name} ID ${id}`,

// Works for both Number PK and String PK
throw new NotFoundException(SD("notFound")(this.modelName, id));
```

**Applicable i18n keys:**

- `notFound`: Entity lookup failure
- `error.entityNotFound`: Specific entity lookup failure (deprecated, prefer `notFound`)

### Type Patterns by Namespace

| i18n key               | Parameter type                         | Purpose                   |
| ---------------------- | -------------------------------------- | ------------------------- |
| `notFound`             | `(name: string, id: number \| string)` | Entity lookup failure     |
| `search.invalidField`  | `(field: string)`                      | Invalid search field      |
| `validation.required`  | `(field: string)`                      | Required field validation |
| `validation.maxLength` | `(field: string, max: number)`         | Max length validation     |
| `validation.minLength` | `(field: string, min: number)`         | Min length validation     |
| `entity.create`        | `(name: string)`                       | Entity creation screen    |
| `entity.edit`          | `(name: string, id: number \| string)` | Entity edit screen        |

### How to Check PK Type

Check the id field type in the Entity file:

```json
// String PK
{
  "props": {
    "id": { "type": "varchar(255)", "autoIncrement": false }
  }
}

// Number PK (default)
{
  "props": {
    "id": { "type": "int", "autoIncrement": true }
  }
}
```

**String PK entities in the KOPRI project:**

- Account
- Session
- User
- Verification
