๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ

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/ ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค:
๐Ÿ“api/
๐Ÿ“src/
๐Ÿ“i18n/
๐Ÿ“„TSko.ts - ํ•œ๊ตญ์–ด ๋”•์…”๋„ˆ๋ฆฌ (defaultLocale)
๐Ÿ“„TSen.ts - ์˜์–ด ๋”•์…”๋„ˆ๋ฆฌ
๐Ÿ“„TSsd.generated.ts - ์ž๋™ ์ƒ์„ฑ ํŒŒ์ผ

๋”•์…”๋„ˆ๋ฆฌ ํŒŒ์ผ ์ƒ์„ฑ

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์˜ ํ‚ค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํƒ€์ž… ์ฒดํฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

Context์—์„œ Locale ์„ค์ •

์š”์ฒญ๋ณ„๋กœ 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;
}

์ดˆ๊ธฐํ™” ํ™•์ธ

์„ค์ •์ด ์™„๋ฃŒ๋˜๋ฉด pnpm sync๋ฅผ ์‹คํ–‰ํ•˜์—ฌ sd.generated.ts๊ฐ€ ์ƒ์„ฑ๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค:
cd api
pnpm sync
์ƒ์„ฑ๋œ sd.generated.ts์—๋Š” ๋‹ค์Œ์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค:
  • Entity์—์„œ ์ถ”์ถœํ•œ ๋ผ๋ฒจ (entityLabels)
  • SD() ํ•จ์ˆ˜
  • localizedColumn() ํ•จ์ˆ˜
  • defineLocale() ํ•จ์ˆ˜