18ways 部落格

如何為 Next.js 應用程式加入多種語言

如何在不損害 SEO 或從頭重建的情況下,為現有的 Next.js 應用程式加入多語系 i18n 與在地化。

I18n(國際化)、l10n(在地化)、多語言支援……不管你怎麼稱呼它,最後你都需要更新你的 Next.js 應用,讓它支援不只一種語言。

困難的地方不是翻譯一句話,而是在不破壞 SEO、不把程式碼庫攪得一團亂、也不替自己製造維護負擔的前提下,為一個真實的 Next.js 應用加入多種語言。

If you want to jump straight to the code, check out the 18ways-next GitHub examples .

先把基礎設施架好

在做其他事情之前,我們先安裝套件。我們會設定 18ways 的函式庫,但如果你的專案很簡單,而且只有靜態內容,像 i18next 這類工具也很好用。

bash
npm install @18ways/next @18ways/react

建立你的設定檔:

js
// 18ways.config.js
module.exports = {
  apiKey: 'pk_dummy_demo_token',
  baseLocale: 'en-GB',
  router: 'app', // 'app', or 'path' depending on which Next.js router you are using
};

將你的 Next.js 設定包起來:

js
// next.config.js
const { withWays } = require('@18ways/next/config');
 
const nextConfig = {
  /*
   * your normal Next.js config here
   */ 
};
 
module.exports = withWays(nextConfig);

新增一個根層代理,讓 / 能重新導向到正確的地區設定:

js
// proxy.js
export { default, config } from '@18ways/next/proxy';

接著加入本地化版面配置:

jsx
// app/layout.jsx
import './styles.css';
import { WaysRoot } from '@18ways/next/server';
 
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className="next-demo-body">
        <WaysRoot>{children}</WaysRoot>
      </body>
    </html>
  );
}

我們完成了!你現在有:

翻譯你的第一個頁面

你現在可以翻譯第一個頁面了。像 i18next 這類工具會要求你把所有文字拆成翻譯鍵,再在程式碼中引用它們。實際會長什麼樣子,取決於你選擇的函式庫。

如果你用的是 18ways,只要把想翻譯的文字包在 <T> 元件裡就行:

jsx
// src/components/MyExampleComponent.jsx
import { useT, T } from '@18ways/react';
 
export default function MyExampleComponent() {
  const t = useT();
 
  return (
    <div>
      <T>Hello world!</T>
      <img
        src="https://example.com/image.png"
        alt={t('Example image')}
      />
    </div>
  );
}

你大概會想讓使用者可以切換語言選擇:

jsx
// src/components/Footer.jsx
import { T, LanguageSwitcher } from '@18ways/react';
 
export default function Footer() {
  return (
    <footer>
      <T>My footer content</T>
      <LanguageSwitcher />
    </footer>
  );
}

小心這些陷阱

一旦基礎架構就位,這些專案通常會有幾種常見的出錯方式。

伺服器端渲染

伺服器端渲染(SSR)能讓你的 Next.js 應用程式在伺服器端預先產生 HTML。這對 SEO 很重要,也能避免使用者看到錯誤內容或語言不對的閃現。

如果你使用像 i18next 這類函式庫,就必須非常小心,確保翻譯內容會在伺服器端渲染期間被載入並填入。你可以檢查頁面的 view-source: 來測試,例如 view-source:http://localhost:3000/。你也應該在正式環境中檢查這一點,確保它在正式版建置中也能正常運作。

如果你用的是 18ways,不用擔心這個。它都會替你處理好。

翻譯鍵

如果你用的是 18ways,完全不用擔心這個。18ways 不需要翻譯鍵,你可以像平常一樣把文字放在原處。

許多 i18n 系統都要求你把程式碼拆成翻譯鍵。命名這些鍵時務必要小心。

不好的 key 會很模糊:

js
// bad-keys.en-GB.js
module.exports = {
  title: 'Continue',
  button: 'Pay now',
  label: 'Home',
};

那些鍵對譯者和開發者來說,幾乎看不出文字出現在哪裡。

更好的鍵會包含脈絡:

js
// better-keys.en-GB.js
module.exports = {
  'checkout.payment.primaryButton': 'Pay now',
  'checkout.payment.stepTitle': 'Complete your payment',
  'account.sidebar.homeLink': 'Home',
};

也要避免動態產生鍵名:

js
// bad-dynamic-keys.js
const key = `checkout.${status}.${buttonType}`;
const translatedText = t(key);

這會讓那些試圖讓翻譯鍵不那麼難用的 IDE 工具失效。它也會讓搜尋與清理舊翻譯鍵變得極其困難。

js
// better-dynamic-keys.js
const keyMap = {
  success: {
    primary: t('checkout.success.primary'),
    default: t('checkout.success.default'),
  },
  error: {
    primary: t('checkout.error.primary'),
    default: t('checkout.error.default'),
  },
};
 
const translatedText = keyMap[status][buttonType];

這樣就沒那麼 DRY,但這是避免翻譯鍵變得難以管理的最佳做法。

更好的做法是使用像 18ways 這樣的工具,這樣你就完全不需要翻譯鍵:

jsx
// app/[lang]/checkout/page.jsx
<T>Pay now</T>

地區偵測

地區偵測是指在使用者明確切換之前,先決定他們應該看到哪種語言。

這通常會包含下列幾種組合:

在一般的 Next.js middleware 中,通常會長得像這樣:

js
// middleware.js
 
/**
 * You don't need any of this if you're using 18ways 
 */
 
import { NextResponse } from 'next/server';
 
const acceptedLocales = ['en-GB', 'fr-FR'];
 
export function middleware(request) {
  const savedLocale =
    request.cookies.get('preferred-locale')?.value;
  const browserLocale =
    request.headers
      .get('accept-language')
      ?.split(',')[0] || 'en-GB';
 
  const locale = acceptedLocales.includes(savedLocale)
    ? savedLocale
    : acceptedLocales.includes(browserLocale)
      ? browserLocale
      : 'en-GB';
 
  if (request.nextUrl.pathname === '/') {
    return NextResponse.redirect(
      new URL(`/${locale}`, request.url)
    );
  }
 
  return NextResponse.next();
}

有些函式庫提供輔助工具,能在不同程度上讓這件事更容易。在 18ways 中,初始偵測與重新導向層都會由系統替你處理。

日期與地區特定的基本型別

不同地區對日期、數字和金錢的格式化方式各不相同。

例如:

在原生 JavaScript 裡,你會用 Intl 自己處理:

js
// formatting-dates-and-currency.js
const myLocale = getCurrentLocale(); // depends on your lib
 
const someTimestamp = new Date('2026-04-13T09:00:00Z');
const dateLabel = new Intl.DateTimeFormat('fr-FR', {
  dateStyle: 'long',
}).format(someTimestamp);
 
const someMoney = {
  amount: 1999,
  currency: 'EUR',
};
const moneyLabel = new Intl.NumberFormat('de-DE', {
  style: 'currency',
  currency: someMoney.currency,
}).format(someMoney.amount);
 
const translatedText = t(
  'my.translation.key',
  { dateLabel, moneyLabel }
);

如果你用的是 18ways,這一切都會替你處理好:

jsx
const someTimestamp = new Date('2026-04-13T09:00:00Z');
const someMoney = {
  amount: 1999,
  currency: 'EUR',
};
 
<T>My text with {{ someTimestamp }} and {{ someMoney }}</T>

把字串串接起來

不要把翻譯後的 UI 用碎片東拼西湊起來:

jsx
// bad-string-joining.jsx
const clickHereText = t('click.here');
const toGetStartedText = t('to.get.started')
<p><a href="#">{clickHereText}</a> {toGetStartedText}.</p>

這在多種語言中都會出問題。

英文裡你可能會說「👉👉Click here👈👈 to get started」,但在法文裡更自然的說法是「Pour commencer, 👉👉cliquez ici👈👈」。有些語言像日文,甚至需要前後都帶文字,例如「始めるには👉👉こちら👈👈をクリックしてください」。

句子結構可能會改變,所以把它拆成零碎片段,反而會讓翻譯困難得多。

整句翻譯通常更好,因為譯者可以自然地重新排列詞序,並將意思視為一個完整單位。

如果你要像這樣翻譯 JSX:

jsx
// rich-text-message.jsx
<p>
  Something <strong>that we want to be bold</strong>
</p>

你得自行查閱 i18n 函式庫,了解如何管理這件事,因為不同函式庫的處理方式差異很大。

如果你使用 18ways,就可以像平常一樣直接翻譯整個 JSX 區塊。

jsx
// rich-text-message.jsx
<p>
  <T>Something <strong>that we want to be bold</strong></T>
</p>

變數

變數讓你在保留完整句子的同時,仍能插入執行時的值。

大多數 i18n 函式庫都有某種形式的支援。在 18ways 中:

jsx
// app/[lang]/page.jsx
<T>Hello {{ name: 'Ada' }}</T>

這種模式很適合:

句子依然好讀,而值也依然明確。

如果你也需要把變數本身翻譯,記得把它包進 t(...)

jsx
const animal = t('dog');
<T>Favourite animal: {{ animal }}</T>

複數形式

複數規則會因語言而異。英文只有一種複數形式(1 year2 years 等)。波蘭文有多種複數形式(1 rok2 lata5 lat)。日文則完全沒有複數形式(1 年2 年3 年)。

大多數 i18n 函式庫都允許你指定類似 ICU 的語法來處理複數:

jsx
// app/[lang]/page.jsx
<T>
  {{
    unreadCount,
    format:
      'plural, =0{No unread messages} =1{One unread message} other{{unreadCount} unread messages}',
  }}
</T>

上面的方法在 18ways 中都行得通,但多數情況下你也可以直接這樣做:

jsx
// app/[lang]/page.jsx
<T>{{ unreadCount }} unread messages</T>

18ways 會幫你處理複數形式!

立即開始

為 Next.js 應用加入多種語言,不必變成一次大改寫。

如果你沒有使用像 18ways 這樣的解決方案——請務必先把路由與 SSR 做對,再著手把文案移到翻譯鍵,並確實避開常見的 i18n 陷阱。

如果你使用 18ways,這一切都會替你處理好,你只需要開始把文字包在 <T> 區塊裡!

正在切换语言