- 閱讀時間 8 分鐘

Astro 中使用 i18n 和內容合集

Astro
Markdown logo

我想要我的應用程式有以下的路由邏輯:

  • URL 前綴表示語言 (frenzh 等),例如 /fr/about/en/about
  • 基於內容合集的動態 URL,例如 /fr/resources/my-resource/zh/articles/my-article
  • 如果 URL 中沒有指定語言,則自動重新導向到用戶的語言(依照瀏覽器組態的語言)

組態

Astro 應用程式中的多語言整合非常簡單。

主組態檔案中可以寫:

// astro.config.mjs
export default defineConfig({
  i18n: {
    locales: [
      { path: "fr", codes: ["fr", "fr-FR", "fr-CA"] },
      { path: "en", codes: ["en", "en-US", "en-GB"] },
      { path: "zh", codes: ["zh", "zh-CN", "zh-TW"] },
    ],
    defaultLocale: "fr",
    routing: {
      prefixDefaultLocale: true,
      redirectToDefaultLocale: false,
    },
  },

  //...
});
  • 每個語言(path)的 codes 用於定義支援的語言,這些語言必須跟瀏覽器標頭 Accept-Language 的語法相容的

  • prefixDefaultLocale 定義 URL 中一直涵蓋語言的前綴,例如 https://domain.com/fr/page

  • redirectToDefaultLocale: false 令我們手動處理用戶到達首頁時正確的語言轉址。

國際化檔案

按照 實用指南 中的建議,我們將創造以下檔案:

  • src/i18n/ui.ts : 定義我們的應用程式支援的語言並介面中顯示翻譯的字串(功能表、頁尾等)
export const languages = {
  fr: "Français",
  en: "English",
  zh: "繁體中文",
};

export const ui = {
  fr: {
    "nav.home": "Accueil",
    "nav.about": "À propos",
  },
  en: {
    "nav.home": "Home",
    "nav.about": "About",
  },
  zh: {
    //...
  },
} as const;

接下來,按照實用指南,我們還將建立一些函式,使我們能夠檢索國際化字串(src/i18n/utils.ts 中的 useTranslation)。

瀏覽器語言檢測

當用戶到達網站的首頁時,如果 URL 中沒有定義語言,我們會轉址到用戶偏好的語言。負責處理這個邏輯的網頁就是網站的首頁(/:根網頁)。

如果我們的應用程式不支援瀏覽器組態的語言,我們就會顯示預設語言(組態中定義的 defaultLocale)。用戶可以選擇他們偏好的語言,使用介面中的語言選擇器。

首頁(沒有定義語言代碼)的範例:

---
// src/pages/index.astro
import { i18n } from "astro:config/server";
import { getPathByLocale } from "astro:i18n";

export const prerender = false;

const targetLocale = Astro.preferredLocale
  ? getPathByLocale(Astro.preferredLocale)
  : i18n!.defaultLocale;

return Astro.redirect(`/${targetLocale}/`);
---

Redirecting to {`/${targetLocale}/`}...

所有 frontmatter 的部份(------ 之間)都是 在伺服器上 執行的。使用 Astro 時,所有頁面是應用程式編譯期間靜態算繪的,除非像上面一樣明確寫下 export const prerender = false;:一旦在這個 URL 上收到要求,它就會由伺服器處理。

Astro.preferredLocale

文件

此屬性僅在 伺服器端算繪 可用(prerender = false)。它將包含瀏覽器偏好的語言,對應應用程式中的組態語言(由 Astro 處理)。例如,如果瀏覽器將其第一個語言設置為 fr-FR,則 Astro 將組態的代碼中會找到 fr-FR 而把 Astro.preferredLocale 填寫相同的值。

如果我們的應用程式不支援瀏覽器組態的語言(例如我們不支援西班牙語 es-ES),則 Astro.preferredLocale 將包含 undefined:可以回退到 defaultLocale

getPathByLocale

getPathByLocale 函式來自 astro:i18n 模組:從瀏覽器收到的語言代碼(例如 en-USzh-TW),檢索要轉址的 URL 前綴(enzh)。都是組態中 pathcodes 屬性定義的:

// astro.config.mjs
export default defineConfig({
  i18n: {
    locales: [
      // 瀏覽器收到的語言代碼與對應應用程式中的路徑
      { path: "fr", codes: ["fr", "fr-FR", "fr-CA"] },
      { path: "en", codes: ["en", "en-US", "en-GB"] },
      { path: "zh", codes: ["zh", "zh-CN", "zh-TW"] },
    ],
    //...
  },

  //...
});

因此,使用者 fr-FR 將被轉址到 /fr/,使用者 en-GB 將被轉址到 /en/,等等…

資料夾結構

應用程式的主頁(src/pages/index.astro)負責為使用者轉址到正確的語言。但對於其餘頁面,我們將創造一個 src/pages/[locale]/ 資料夾,將允許以多種語言管理每個頁面。

因此,對於靜態頁面(如首頁或關於頁面),可以建立單一檔案,並整合國際化的字串。

對於更豐富的內容(如文章),會建立一個範本檔案,然後將內容在內容合集中組織。

內容合集

作為內容的入口,將創造個新 src/content 的資料夾。

裡面可以為每種語言創造新資料夾。因此,將有:

  • src/content/fr
  • src/content/en
  • src/content/zh

接下來,在每種語言的下方,可以創造新的資料夾,對應到內容合集(例如文章):

  • src/content/fr/articles
  • src/content/en/articles
  • src/content/zh/articles

要載入內容,會定義內容合集,其 pattern 包含所有支援的語言:

const articlesCollection = defineCollection({
  loader: glob({
    pattern: localizedPattern("articles/**/*.{md,mdx}"),
    base: "src/content/",
  }),
  schema: z.object({
    title: z.string(),
  }),
});

localizedPatternpattern 前面增加語言的前綴 :

export function localizedPattern(pattern: string): string {
  const langCodes = Object.keys(languages);
  return `(${langCodes.join("|")})/${pattern}`;
}

因此,就像上面的例子一樣,articles/**/*.{md,mdx} 就會變成 (fr|en|zh)/articles/**/*.{md,mdx}basesrc/content/ 資料夾)。

getStaticPaths

如果應用程式支援多種語言,可以將 [locale] 作為根資料夾,這意味著所有頁面至少有一個 URL 參數。對於靜態頁面,這可能意味著需要重複邏輯來檢索這個參數的所有值(/articles/about/contact、…)並生成所有 URL。

我們會創造一個 localizedPaths 的實用函式,它將負責返回包含我們 URL 參數 locale 的對象,每個語言一個。

如果沒有其他 URL 參數要填寫,我們可以直接將這個函式重新導出為 getStaticPaths:Astro 將為每個語言生成 URL。

type LocalizedPath = {
  params: {
    locale: string;
  };
};

/**
 * For a given page, grab all languages and create the necessary paths with the locale param
 *
 * @returns An array of all localized paths
 */
export async function localizedPaths(): Promise<LocalizedPath[]> {
  const langs = Object.keys(languages);

  return langs.map((locale) => {
    return { params: { locale } };
  });
}

例如,[locale]/about.astro 中:

export { localizedPaths as getStaticPaths } from "@/lib/utils";