fe-i18n
1
总安装量
1
周安装量
#52540
全站排名
安装命令
npx skills add https://github.com/ingpdw/pdw-fe-dev-tool --skill fe-i18n
Agent 安装分布
mcpjam
1
claude-code
1
replit
1
junie
1
zencoder
1
Skill 文档
FE Internationalization (i18n)
$ARGUMENTS를 ë¶ìíì¬ ë¤êµì´ ì¤ì ì 구ì±íê±°ë ë²ìì ì ì©íë¤.
ë¶ì ì ì°¨
- íì¬ ìí íì : íë¡ì í¸ì i18n ì¤ì ì íì¸íë¤
- ì구ì¬í íì : ì§ìí ì¸ì´, 기본 ì¸ì´, ë¼ì°í ì ëµì íì¸íë¤
- ì¤ì /구í: next-intl 기ë°ì¼ë¡ ë¤êµì´ë¥¼ ì¤ì íê±°ë ë²ìì ì ì©íë¤
- ê²ì¦: ê° ë¡ì¼ì¼ìì ì ì ëìíëì§ íì¸íë¤
next-intl ì´ê¸° ì¤ì
1. í¨í¤ì§ ì¤ì¹
pnpm add next-intl
2. íë¡ì í¸ êµ¬ì¡°
src/
âââ app/
â âââ [locale]/ # ë¡ì¼ì¼ë³ ë¼ì°í
â âââ layout.tsx
â âââ page.tsx
â âââ about/page.tsx
âââ i18n/
â âââ request.ts # ìë² ì¬ì´ë i18n ì¤ì
â âââ routing.ts # ë¼ì°í
ì¤ì
âââ messages/ # ë²ì íì¼
â âââ ko.json
â âââ en.json
âââ middleware.ts # ë¡ì¼ì¼ ê°ì§ & 리ë¤ì´ë í¸
3. ë¼ì°í ì¤ì
// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
locales: ["ko", "en"],
defaultLocale: "ko",
});
4. 미ë¤ì¨ì´
// src/middleware.ts
import createMiddleware from "next-intl/middleware";
import { routing } from "@/i18n/routing";
export default createMiddleware(routing);
export const config = {
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};
5. ìë² ì¬ì´ë ì¤ì
// src/i18n/request.ts
import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (!locale || !routing.locales.includes(locale as "ko" | "en")) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});
6. next.config
// next.config.ts
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin();
const nextConfig = {};
export default withNextIntl(nextConfig);
7. Root Layout
// src/app/[locale]/layout.tsx
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { notFound } from "next/navigation";
import { routing } from "@/i18n/routing";
interface LayoutProps {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}
export default async function LocaleLayout({ children, params }: LayoutProps) {
const { locale } = await params;
if (!routing.locales.includes(locale as "ko" | "en")) {
notFound();
}
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
ë²ì íì¼ êµ¬ì¡°
ë¤ìì¤íì´ì¤ ê¸°ë° êµ¬ì¡°
// messages/ko.json
{
"common": {
"submit": "ì ì¶",
"cancel": "ì·¨ì",
"save": "ì ì¥",
"delete": "ìì ",
"loading": "ë¡ë© ì¤...",
"error": "ì¤ë¥ê° ë°ìíìµëë¤",
"confirm": "íì¸"
},
"nav": {
"home": "í",
"about": "ìê°",
"blog": "ë¸ë¡ê·¸",
"contact": "문ì"
},
"auth": {
"login": "ë¡ê·¸ì¸",
"logout": "ë¡ê·¸ìì",
"signup": "íìê°ì
",
"email": "ì´ë©ì¼",
"password": "ë¹ë°ë²í¸",
"forgotPassword": "ë¹ë°ë²í¸ 찾기"
},
"home": {
"title": "íìí©ëë¤",
"description": "ìµê³ ì ìë¹ì¤ë¥¼ ì ê³µí©ëë¤",
"cta": "ììí기"
},
"blog": {
"title": "ë¸ë¡ê·¸",
"readMore": "ë ì½ê¸°",
"publishedAt": "{date}ì ê²ìë¨",
"readingTime": "ì½ë ìê° {minutes}ë¶"
},
"validation": {
"required": "{field}ì(를) ì
ë ¥í´ì£¼ì¸ì",
"email": "ì í¨í ì´ë©ì¼ì ì
ë ¥í´ì£¼ì¸ì",
"minLength": "{field}ì(ë) ìµì {min}ì ì´ìì´ì´ì¼ í©ëë¤"
}
}
// messages/en.json
{
"common": {
"submit": "Submit",
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
"loading": "Loading...",
"error": "An error occurred",
"confirm": "Confirm"
},
"nav": {
"home": "Home",
"about": "About",
"blog": "Blog",
"contact": "Contact"
},
"auth": {
"login": "Log in",
"logout": "Log out",
"signup": "Sign up",
"email": "Email",
"password": "Password",
"forgotPassword": "Forgot password"
},
"home": {
"title": "Welcome",
"description": "We provide the best service",
"cta": "Get Started"
},
"blog": {
"title": "Blog",
"readMore": "Read more",
"publishedAt": "Published on {date}",
"readingTime": "{minutes} min read"
},
"validation": {
"required": "Please enter {field}",
"email": "Please enter a valid email",
"minLength": "{field} must be at least {min} characters"
}
}
ë²ì ì¬ì© í¨í´
Server Componentìì ì¬ì©
// src/app/[locale]/page.tsx
import { useTranslations } from "next-intl";
export default function HomePage() {
const t = useTranslations("home");
return (
<main>
<h1>{t("title")}</h1>
<p>{t("description")}</p>
<Button>{t("cta")}</Button>
</main>
);
}
Client Componentìì ì¬ì©
"use client";
import { useTranslations } from "next-intl";
function LoginForm() {
const t = useTranslations("auth");
const tCommon = useTranslations("common");
return (
<form>
<Input placeholder={t("email")} />
<Input placeholder={t("password")} type="password" />
<Button type="submit">{t("login")}</Button>
<Button variant="outline">{tCommon("cancel")}</Button>
</form>
);
}
ë³ì ì½ì (ICU 문ë²)
const t = useTranslations("blog");
// ë¨ì ë³ì
<p>{t("publishedAt", { date: "2024-01-15" })}</p>
// â "2024-01-15ì ê²ìë¨" (ko)
// â "Published on 2024-01-15" (en)
// ë³µìí (messagesì ì ì)
// "items": "{count, plural, =0 {í목 ìì} one {1ê° í목} other {{count}ê° í목}}"
<p>{t("items", { count: 5 })}</p>
// â "5ê° í목"
// ë¦¬ì¹ í
ì¤í¸
// "terms": "<link>ì´ì©ì½ê´</link>ì ëìí©ëë¤"
<p>
{t.rich("terms", {
link: (chunks) => <a href="/terms">{chunks}</a>,
})}
</p>
ë ì§/ì«ì í¬ë§·
import { useFormatter } from "next-intl";
function PriceDisplay({ price, date }: { price: number; date: Date }) {
const format = useFormatter();
return (
<div>
<p>
{format.number(price, { style: "currency", currency: "KRW" })}
</p>
{/* â "â©10,000" (ko) / "$10,000" (en, currencyì ë°ë¼) */}
<p>
{format.dateTime(date, { year: "numeric", month: "long", day: "numeric" })}
</p>
{/* â "2024ë
1ì 15ì¼" (ko) / "January 15, 2024" (en) */}
<p>
{format.relativeTime(date)}
</p>
{/* â "3ì¼ ì " (ko) / "3 days ago" (en) */}
</div>
);
}
ì¸ì´ ì í ì»´í¬ëí¸
"use client";
import { useLocale } from "next-intl";
import { useRouter, usePathname } from "@/i18n/routing";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const localeLabels: Record<string, string> = {
ko: "íêµì´",
en: "English",
};
function LocaleSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
function handleChange(newLocale: string) {
router.replace(pathname, { locale: newLocale });
}
return (
<Select value={locale} onValueChange={handleChange}>
<SelectTrigger className="w-[120px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(localeLabels).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
export { LocaleSwitcher };
ë¤ë¹ê²ì´ì ë§í¬
import { Link } from "@/i18n/routing";
// ë¡ì¼ì¼ì´ ìëì¼ë¡ URLì í¬í¨ë¨
<Link href="/about">ìê°</Link>
// ko â /ko/about
// en â /en/about
// src/i18n/routing.ts â navigation í¬í¨
import { defineRouting } from "next-intl/routing";
import { createNavigation } from "next-intl/navigation";
export const routing = defineRouting({
locales: ["ko", "en"],
defaultLocale: "ko",
});
export const { Link, redirect, usePathname, useRouter } =
createNavigation(routing);
SEO + i18n ì°ë
ë¡ì¼ì¼ë³ Metadata
// src/app/[locale]/layout.tsx
import { getTranslations } from "next-intl/server";
export async function generateMetadata({ params }: LayoutProps) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "metadata" });
return {
title: t("title"),
description: t("description"),
alternates: {
canonical: `https://example.com/${locale}`,
languages: {
ko: "https://example.com/ko",
en: "https://example.com/en",
},
},
};
}
ë¡ì¼ì¼ë³ Sitemap
// src/app/sitemap.ts
import type { MetadataRoute } from "next";
const locales = ["ko", "en"];
export default function sitemap(): MetadataRoute.Sitemap {
const pages = ["", "/about", "/blog"];
return pages.flatMap((page) =>
locales.map((locale) => ({
url: `https://example.com/${locale}${page}`,
lastModified: new Date(),
changeFrequency: "weekly" as const,
priority: page === "" ? 1 : 0.8,
}))
);
}
ë²ì í¤ ê´ë¦¬ ê·ì¹
- ë¤ìì¤íì´ì¤: íì´ì§/기ë¥ë³ë¡ ë¶ë¦¬ (
nav,auth,home,blog) - ê³µíµ í¤:
commonë¤ìì¤íì´ì¤ì 모ìì ì¬ì¬ì© - ë¤ì´ë°: camelCase, 구체ì ì¸ ì´ë¦ (
submitButton>btn1) - 구조í: 2ë¨ê³ê¹ì§ë§ ì¤ì²© (ê¹ì ì¤ì²© ì§ì)
- ë³ì: ICU MessageFormat ì¬ì© (
{count},{name}) - ëë½ ë°©ì§: TypeScript íì ì¼ë¡ ë²ì í¤ ê²ì¦
TypeScript íì ìì ì±
// src/types/i18n.d.ts
import ko from "@/messages/ko.json";
type Messages = typeof ko;
declare module "next-intl" {
interface IntlMessages extends Messages {}
}
ì¤í ê·ì¹
setupì¸ì ì next-intl ì´ê¸° ì¤ì ì ì ì²´ ì§ííë¤- íì¼ ê²½ë¡ê° ì ë¬ëë©´ í´ë¹ íì¼ì ë²ìì ì ì©íë¤
add-locale [locale]ì¸ì ì ì ì¸ì´ë¥¼ ì¶ê°íë¤- ë²ì íì¼ ìì± ì 기존 í¤ êµ¬ì¡°ë¥¼ ë¶ìíì¬ ì¼ê´ì±ì ì ì§íë¤
- íëì½ë©ë íêµì´ 문ìì´ì íì§íì¬ ë²ì í¤ë¡ ì¶ì¶ì ì ìíë¤
- ì¸ì´ ì í ì URL 구조ì SEO를 í¨ê» ê³ ë ¤íë¤