sonamu.config.ts ์ค์
sonamu.config.ts์์ i18n ์ต์
์ ํ์ฑํํฉ๋๋ค:
import { defineConfig } from "sonamu";
export default defineConfig({
// ... ๊ธฐํ ์ค์
i18n: {
defaultLocale: "ko", // ๊ธฐ๋ณธ ์ธ์ด
supportedLocales: ["ko", "en"], // ์ง์ ์ธ์ด ๋ชฉ๋ก
},
});
| ์ต์
| ํ์
| ์ค๋ช
|
|---|
defaultLocale | string | ๊ธฐ๋ณธ ์ธ์ด ์ฝ๋ (fallback) |
supportedLocales | string[] | ์ง์ํ๋ ๋ชจ๋ ์ธ์ด ์ฝ๋ |
๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ
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๊ฐ ์์ฑ๋๋์ง ํ์ธํฉ๋๋ค:
์์ฑ๋ sd.generated.ts์๋ ๋ค์์ด ํฌํจ๋ฉ๋๋ค:
- Entity์์ ์ถ์ถํ ๋ผ๋ฒจ (
entityLabels)
SD() ํจ์
localizedColumn() ํจ์
defineLocale() ํจ์
SD ํจ์ ์ฌ์ฉ๋ฒ
๋์
๋๋ฆฌ ์์ฑ ๋ฐ ์ฌ์ฉ
Entity ๋ผ๋ฒจ
์๋ ์ถ์ถ๋๋ ๋ผ๋ฒจ