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

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 ๋ผ๋ฒจ

์ž๋™ ์ถ”์ถœ๋˜๋Š” ๋ผ๋ฒจ