메인 μ½˜ν…μΈ λ‘œ κ±΄λ„ˆλ›°κΈ°

sonamu.config.ts μ„€μ •

sonamu.config.tsμ—μ„œ i18n μ˜΅μ…˜μ„ ν™œμ„±ν™”ν•©λ‹ˆλ‹€:
import { defineConfig } from "sonamu";

export default defineConfig({
  // ... 기타 μ„€μ •

  i18n: {
    defaultLocale: "ko", // κΈ°λ³Έ μ–Έμ–΄
    supportedLocales: ["ko", "en"], // 지원 μ–Έμ–΄ λͺ©λ‘
  },
});
μ˜΅μ…˜νƒ€μž…μ„€λͺ…
defaultLocalestringκΈ°λ³Έ μ–Έμ–΄ μ½”λ“œ (fallback)
supportedLocalesstring[]μ§€μ›ν•˜λŠ” λͺ¨λ“  μ–Έμ–΄ μ½”λ“œ

디렉토리 ꡬ쑰

i18n을 ν™œμ„±ν™”ν•˜λ©΄ api/src/i18n/ 디렉토리가 ν•„μš”ν•©λ‹ˆλ‹€:

λ”•μ…”λ„ˆλ¦¬ 파일 생성

defaultLocale λ”•μ…”λ„ˆλ¦¬ (ko.ts)

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

const format = createFormat("ko");

export default {
  // 곡톡 UI
  "common.save": "μ €μž₯",
  "common.cancel": "μ·¨μ†Œ",
  "common.delete": "μ‚­μ œ",
  "common.results": (count: number) => `${count}개 결과`,

  // μ—λŸ¬ λ©”μ‹œμ§€
  "error.notFound": "찾을 수 μ—†μŠ΅λ‹ˆλ‹€",
  "error.unauthorized": "인증이 ν•„μš”ν•©λ‹ˆλ‹€",

  // 검증 λ©”μ‹œμ§€ (ν•¨μˆ˜ν˜•)
  "validation.required": (field: string) => `${josa(field, "μ€λŠ”")} ν•„μˆ˜μž…λ‹ˆλ‹€`,
  "validation.email": "μ˜¬λ°”λ₯Έ 이메일 ν˜•μ‹μ΄ μ•„λ‹™λ‹ˆλ‹€",

  // λ‹¨μˆœ 킀도 μ‚¬μš© κ°€λŠ₯ (단, λ„€μž„μŠ€νŽ˜μ΄μŠ€ νŒ¨ν„΄ ꢌμž₯)
  notFound: (name: string, id: number) => `μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ${name} ID ${id}`,
  test: (date: Date) => format.date(date),
} as const;
ν‚€ 넀이밍 ꢌμž₯ νŒ¨ν„΄: "도메인.ν•­λͺ©" ν˜•μ‹μ˜ λ„€μž„μŠ€νŽ˜μ΄μŠ€ νŒ¨ν„΄μ„ μ‚¬μš©ν•˜λ©΄ ν‚€λ₯Ό μ²΄κ³„μ μœΌλ‘œ 관리할 수 있고 IDE μžλ™μ™„μ„±λ„ νŽΈλ¦¬ν•©λ‹ˆλ‹€. - "common.*" - 곡톡 UI - "error.*" - μ—λŸ¬ λ©”μ‹œμ§€ - "validation.*" - 검증 λ©”μ‹œμ§€ - "entity.*" - Entity κ΄€λ ¨

λ‹€λ₯Έ locale λ”•μ…”λ„ˆλ¦¬ (en.ts)

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

export default defineLocale({
  // 곡톡 UI
  "common.save": "Save",
  "common.cancel": "Cancel",
  "common.delete": "Delete",
  "common.results": (count: number) =>
    plural(count, { one: `${count} result`, other: `${count} results` }),

  // μ—λŸ¬ λ©”μ‹œμ§€
  "error.notFound": "Not found",
  "error.unauthorized": "Authentication required",

  // 검증 λ©”μ‹œμ§€
  "validation.required": (field: string) => `${field} is required`,
  "validation.email": "Invalid email format",

  // λ‹¨μˆœ ν‚€ (λ„€μž„μŠ€νŽ˜μ΄μŠ€ νŒ¨ν„΄ ꢌμž₯)
  notFound: (name: string, id: number) => `${name} ID ${id} not found`,
});
defineLocale은 sd.generated.tsμ—μ„œ export되며, defaultLocale의 ν‚€λ₯Ό κΈ°μ€€μœΌλ‘œ νƒ€μž… 체크λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.

Locale μ„€μ •

ν”„λ‘ νŠΈμ—”λ“œ (ν΄λΌμ΄μ–ΈνŠΈ)

sd.generated.tsμ—λŠ” ν΄λΌμ΄μ–ΈνŠΈ μΈ‘ locale 관리λ₯Ό μœ„ν•œ setLocaleκ³Ό getCurrentLocale ν•¨μˆ˜κ°€ ν¬ν•¨λ©λ‹ˆλ‹€:
import { setLocale, getCurrentLocale, SUPPORTED_LOCALES } from "@/i18n/sd.generated";

// locale λ³€κ²½
setLocale("en");

// ν˜„μž¬ locale 확인
const current = getCurrentLocale(); // "en"
sonamu.shared.ts의 axios interceptorκ°€ λͺ¨λ“  API μš”μ²­μ— Accept-Language 헀더λ₯Ό μžλ™μœΌλ‘œ μΆ”κ°€ν•©λ‹ˆλ‹€:
// sonamu.shared.ts (μžλ™ 생성됨 β€” 직접 μž‘μ„±ν•  ν•„μš” μ—†μŒ)
axios.interceptors.request.use((config) => {
  config.headers["Accept-Language"] = getCurrentLocale();
  return config;
});
λ”°λΌμ„œ setLocale("en")을 ν˜ΈμΆœν•œ λ’€μ˜ λͺ¨λ“  API μš”μ²­μ€ Accept-Language: en 헀더λ₯Ό ν¬ν•¨ν•©λ‹ˆλ‹€.

λ°±μ—”λ“œ (μ„œλ²„)

μš”μ²­λ³„λ‘œ locale을 μ„€μ •ν•˜λ €λ©΄ λ―Έλ“€μ›¨μ–΄μ—μ„œ Contextλ₯Ό κ΅¬μ„±ν•©λ‹ˆλ‹€:
// api/src/middlewares/locale.ts
import { Sonamu } from "sonamu";

export async function localeMiddleware(request: FastifyRequest) {
  // Accept-Language 헀더 λ˜λŠ” 쿼리 νŒŒλΌλ―Έν„°μ—μ„œ locale μΆ”μΆœ
  const locale =
    request.headers["accept-language"]?.split(",")[0]?.split("-")[0] ||
    request.query.locale ||
    "ko";

  // Context에 locale μ„€μ •
  const ctx = Sonamu.getContext();
  ctx.locale = locale;
}
ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ setLocale()둜 locale을 μ„€μ •ν•˜λ©΄, axios interceptorκ°€ Accept-Language 헀더λ₯Ό μžλ™ μ „λ‹¬ν•˜κ³ , μ„œλ²„μ˜ locale 미듀웨어가 이λ₯Ό 읽어 SD() 호좜 μ‹œ ν•΄λ‹Ή μ–Έμ–΄μ˜ λ²ˆμ—­μ„ λ°˜ν™˜ν•©λ‹ˆλ‹€. λ³„λ„μ˜ μΆ”κ°€ μ„€μ • 없이 ν΄λΌμ΄μ–ΈνŠΈ-μ„œλ²„ κ°„ locale이 λ™κΈ°ν™”λ©λ‹ˆλ‹€.

μ΄ˆκΈ°ν™” 확인

섀정이 μ™„λ£Œλ˜λ©΄ pnpm syncλ₯Ό μ‹€ν–‰ν•˜μ—¬ sd.generated.tsκ°€ μƒμ„±λ˜λŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€:
cd api
pnpm sync
μƒμ„±λœ sd.generated.tsμ—λŠ” λ‹€μŒμ΄ ν¬ν•¨λ©λ‹ˆλ‹€:
  • Entityμ—μ„œ μΆ”μΆœν•œ 라벨 (entityLabels)
  • SD() ν•¨μˆ˜
  • localizedColumn() ν•¨μˆ˜
  • defineLocale() ν•¨μˆ˜

SD ν•¨μˆ˜ μ‚¬μš©λ²•

λ”•μ…”λ„ˆλ¦¬ μž‘μ„± 및 μ‚¬μš©

Entity 라벨

μžλ™ μΆ”μΆœλ˜λŠ” 라벨