嗨咿,我是 illumi!昨天介紹了多語系網站的工具,今天號召所有用Next.js App 的大朋友小朋友,一起來用 next-intl 實踐語言切換吧!
首先來看看官網吧!但等等......我按照官網裝怎卡住了!
├── messages
│ ├── en.json
│ └── ...
├── next.config.ts
└── src
├── i18n
│ └── request.ts
└── app
├── layout.tsx
└── page.tsx
因為最新的Next版本沒有src ,所以採用下面的資料夾架構
project/
├── app/
│ ├── layout.tsx # 根 layout
│ └── [locale]/
│ ├── layout.tsx # 語言 layout
│ ├── page.tsx # 首頁 (記得把app裡面原有的page刪掉)
│ └── 頁面名稱/
│ └── page.tsx # 其他頁面
├── i18n/
│ ├── routing.ts # 路由設定
│ └── request.ts # 請求設定
├── messages/
│ ├── en.json # 英文翻譯
│ └── zh.json # 中文翻譯
├── middleware.ts # 中間件
└── next.config.ts # Next.js 設定
按照 next-intl 官網的基本設定,會遇到以下問題:
官網省略setRequestLocale(locale),但這可以讓 server-side 組件正確獲取語言。
沒辦法幫你直接導向語言
middleware 負責處理語言協商和重定向,沒有它就無法正確導向。
每個檔案我都再放了一次檔案結構並標注(貼心吧~)
project/
├── app/
│ ├── layout.tsx # 根 layout
│ └── [locale]/
│ ├── layout.tsx # 語言 layout
│ ├── page.tsx # 首頁 (記得把app裡面原有的page刪掉)
│ └── 頁面名稱/
│ └── page.tsx # 其他頁面
├── i18n/
│ ├── routing.ts # 路由設定
│ └── request.ts # 請求設定
├── messages/
│ ├── en.json # 英文翻譯
│ └── zh.json # 中文翻譯
├── middleware.ts # 中間件
└── next.config.ts # Next.js 設定
npm install next-intl
project/
├── app/
│ ├── layout.tsx # 根 layout
│ └── [locale]/
│ ├── layout.tsx # 語言 layout
│ ├── page.tsx # 首頁 (記得把app裡面原有的page刪掉)
│ └── 頁面名稱/
│ └── page.tsx # 其他頁面
├── i18n/
│ ├── routing.ts # 路由設定
│ └── request.ts # 請求設定
├── messages/
│ ├── en.json # 英文翻譯
│ └── zh.json # 中文翻譯
├── middleware.ts # 中間件
└── next.config.ts # Next.js 設定
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default withNextIntl(nextConfig);
project/
├── app/
│ ├── layout.tsx # 根 layout
│ └── [locale]/
│ ├── layout.tsx # 語言 layout
│ ├── page.tsx # 首頁 (記得把app裡面原有的page刪掉)
│ └── 頁面名稱/
│ └── page.tsx # 其他頁面
├── i18n/
│ ├── routing.ts # 路由設定
│ └── request.ts # 請求設定
├── messages/
│ ├── en.json # 英文翻譯
│ └── zh.json # 中文翻譯
├── middleware.ts # 中間件
└── next.config.ts # Next.js 設定
import { defineRouting } from 'next-intl/routing';
import { createNavigation } from 'next-intl/navigation';
export const routing = defineRouting({
// A list of all locales that are supported
locales: ['en', 'zh'],
// Used when no locale matches
defaultLocale: 'en'
});
export const { Link, redirect, usePathname, useRouter } =
createNavigation(routing);
project/
├── app/
│ ├── layout.tsx # 根 layout
│ └── [locale]/
│ ├── layout.tsx # 語言 layout
│ ├── page.tsx # 首頁 (記得把app裡面原有的page刪掉)
│ └── 頁面名稱/
│ └── page.tsx # 其他頁面
├── i18n/
│ ├── routing.ts # 路由設定
│ └── **request.ts ** # 請求設定
├── messages/
│ ├── en.json # 英文翻譯
│ └── zh.json # 中文翻譯
├── middleware.ts # 中間件
└── next.config.ts # Next.js 設定
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 "en" | "zh")) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (
await import(`../messages/${locale}.json`).catch(() =>
import(`../messages/${routing.defaultLocale}.json`)
)
).default
};
});
project/
├── app/
│ ├── layout.tsx # 根 layout
│ └── [locale]/
│ ├── layout.tsx # 語言 layout
│ ├── page.tsx # 首頁 (記得把app裡面原有的page刪掉)
│ └── 頁面名稱/
│ └── page.tsx # 其他頁面
├── i18n/
│ ├── routing.ts # 路由設定
│ └── request.ts # 請求設定
├── messages/
│ ├── en.json # 英文翻譯
│ └── zh.json # 中文翻譯
├── middleware.ts # 中間件
└── next.config.ts # Next.js 設定
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: ['/', '/(zh|en)/:path*']
};
這個就變成單純傳遞頁面,而把最主要layout的功能移到[locale]/layout.tsx
中
project/
├── app/
│ ├── layout.tsx # 根 layout
│ └── [locale]/
│ ├── layout.tsx # 語言 layout
│ ├── page.tsx # 首頁 (記得把app裡面原有的page刪掉)
│ └── 頁面名稱/
│ └── page.tsx # 其他頁面
├── i18n/
│ ├── routing.ts # 路由設定
│ └── request.ts # 請求設定
├── messages/
│ ├── en.json # 英文翻譯
│ └── zh.json # 中文翻譯
├── middleware.ts # 中間件
└── next.config.ts # Next.js 設定
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}
project/
├── app/
│ ├── layout.tsx # 根 layout
│ └── [locale]/
│ ├── layout.tsx # 語言 layout
│ ├── page.tsx # 首頁 (記得把app裡面原有的page刪掉)
│ └── 頁面名稱/
│ └── page.tsx # 其他頁面
├── i18n/
│ ├── routing.ts # 路由設定
│ └── request.ts # 請求設定
├── messages/
│ ├── en.json # 英文翻譯
│ └── zh.json # 中文翻譯
├── middleware.ts # 中間件
└── next.config.ts # Next.js 設定
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { setRequestLocale } from 'next-intl/server';
export default async function LocaleLayout({
children,
params
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
setRequestLocale(locale);
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
project/
├── app/
│ ├── layout.tsx # 根 layout
│ └── [locale]/
│ ├── layout.tsx # 語言 layout
│ ├── page.tsx # 首頁 (記得把app裡面原有的page刪掉)
│ └── 頁面名稱/
│ └── page.tsx # 其他頁面
├── i18n/
│ ├── routing.ts # 路由設定
│ └── request.ts # 請求設定
├── messages/
│ ├── en.json # 英文翻譯
│ └── zh.json # 中文翻譯
├── middleware.ts # 中間件
└── next.config.ts # Next.js 設定
官網:
export default function HomePage() {
const t = useTranslations('HomePage');
return <h1>{t('title')}</h1>;
}
我:
import { getTranslations, setRequestLocale } from 'next-intl/server';
export default async function HomePage({
params
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations('HomePage'); //假設你要取用json中在HomePage物件中的文字
return (
<div>
<h1>{t('title')}</h1>
<p>{t('about')}</p>
</div>
);
}
這邊可以換成其他UI元件庫,或者你要手刻
npx shadcn@latest init
npx shadcn@latest add select
// components/LanguageSwitcher.tsx
'use client';
import { useRouter, usePathname } from '@/i18n/routing';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useParams } from 'next/navigation';
const languages = [
{ value: 'en', label: 'Eng' },
{ value: 'zh', label: '中' }
];
export default function LanguageSwitcher() {
const router = useRouter();
const pathname = usePathname();
const params = useParams();
const currentLocale = params.locale as string;
const handleLanguageChange = (newLocale: string) => {
router.replace(pathname, { locale: newLocale });
};
const currentLanguage = languages.find(lang => lang.value === currentLocale);
return (
<Select value={currentLocale} onValueChange={handleLanguageChange}>
<SelectTrigger className="w-[140px]">
<SelectValue>
<span className="flex items-center gap-2">
{currentLanguage?.flag} {currentLanguage?.label}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
{languages.map((language) => (
<SelectItem key={language.value} value={language.value}>
<span className="flex items-center gap-2">
{language.flag} {language.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
);
}
// app/[locale]/page.tsx
import { getTranslations } from "next-intl/server";
export default async function HomePage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const t = await getTranslations("HomePage");
return (
<div>
<h1>{t("title")}</h1>
</div>
);
}
project/
├── app/
│ ├── layout.tsx
│ └── [locale]/
│ ├── layout.tsx
│ └── page.tsx
├── i18n/
│ ├── routing.ts
│ └── request.ts
├── messages/
│ ├── ** en.json **
│ └── zh.json
├── middleware.ts
└── next.config.ts
{
"HomePage": {
"title": "Welcome to Our Website",
"about": "This is an amazing platform that helps you achieve your goals.",
"getStarted": "Get Started",
"learnMore": "Learn More"
},
"Navigation": {
"home": "Home",
"about": "About",
"services": "Services",
"contact": "Contact",
"login": "Login",
"logout": "Logout"
},
"Common": {
"loading": "Loading...",
"error": "Something went wrong",
"success": "Success!",
"cancel": "Cancel",
"confirm": "Confirm",
"save": "Save",
"delete": "Delete",
"edit": "Edit",
"search": "Search",
"filter": "Filter",
"sort": "Sort",
"previous": "Previous",
"next": "Next",
"close": "Close"
},
"Form": {
"name": "Name",
"email": "Email",
"password": "Password",
"confirmPassword": "Confirm Password",
"phone": "Phone Number",
"address": "Address",
"submit": "Submit",
"reset": "Reset",
"required": "This field is required",
"invalidEmail": "Please enter a valid email address",
"passwordTooShort": "Password must be at least 8 characters",
"passwordMismatch": "Passwords do not match"
},
"User": {
"profile": "Profile",
"settings": "Settings",
"preferences": "Preferences",
"account": "Account",
"billing": "Billing",
"notifications": "Notifications",
"privacy": "Privacy",
"security": "Security"
},
"Time": {
"today": "Today",
"yesterday": "Yesterday",
"tomorrow": "Tomorrow",
"thisWeek": "This Week",
"thisMonth": "This Month",
"thisYear": "This Year",
"minute": "minute",
"minutes": "minutes",
"hour": "hour",
"hours": "hours",
"day": "day",
"days": "days",
"week": "week",
"weeks": "weeks",
"month": "month",
"months": "months",
"year": "year",
"years": "years"
},
"Status": {
"active": "Active",
"inactive": "Inactive",
"pending": "Pending",
"approved": "Approved",
"rejected": "Rejected",
"completed": "Completed",
"inProgress": "In Progress",
"cancelled": "Cancelled"
}
}
project/
├── app/
│ ├── layout.tsx
│ └── [locale]/
│ ├── layout.tsx
│ └── page.tsx
├── i18n/
│ ├── routing.ts
│ └── request.ts
├── messages/
│ ├── en.json
│ └── ** zh.json **
├── middleware.ts
└── next.config.ts
{
"HomePage": {
"title": "歡迎來到我們的網站",
"about": "這是一個幫助您實現目標的絕佳平台。",
"getStarted": "開始使用",
"learnMore": "了解更多"
},
"Navigation": {
"home": "首頁",
"about": "關於我們",
"services": "服務項目",
"contact": "聯絡我們",
"login": "登入",
"logout": "登出"
},
"Common": {
"loading": "載入中...",
"error": "發生錯誤",
"success": "成功!",
"cancel": "取消",
"confirm": "確認",
"save": "儲存",
"delete": "刪除",
"edit": "編輯",
"search": "搜尋",
"filter": "篩選",
"sort": "排序",
"previous": "上一頁",
"next": "下一頁",
"close": "關閉"
},
"Form": {
"name": "姓名",
"email": "電子郵件",
"password": "密碼",
"confirmPassword": "確認密碼",
"phone": "電話號碼",
"address": "地址",
"submit": "提交",
"reset": "重設",
"required": "此欄位為必填",
"invalidEmail": "請輸入有效的電子郵件地址",
"passwordTooShort": "密碼長度至少需要8個字元",
"passwordMismatch": "密碼不一致"
},
"User": {
"profile": "個人檔案",
"settings": "設定",
"preferences": "偏好設定",
"account": "帳戶",
"billing": "帳單",
"notifications": "通知",
"privacy": "隱私",
"security": "安全性"
},
"Time": {
"today": "今天",
"yesterday": "昨天",
"tomorrow": "明天",
"thisWeek": "本週",
"thisMonth": "本月",
"thisYear": "今年",
"minute": "分鐘",
"minutes": "分鐘",
"hour": "小時",
"hours": "小時",
"day": "天",
"days": "天",
"week": "週",
"weeks": "週",
"month": "月",
"months": "月",
"year": "年",
"years": "年"
},
"Status": {
"active": "啟用",
"inactive": "停用",
"pending": "待處理",
"approved": "已核准",
"rejected": "已拒絕",
"completed": "已完成",
"inProgress": "處理中",
"cancelled": "已取消"
}
}
最終就可以像這樣切換
貼上官網安裝方式出 bug?
那我們明天再來講,如果想在裡面塞多行文字怎麼辦呢?