Astro + @astrolicious/i18n 實作 i18n 多語系功能

之前有使用過 用 Paraglide 做 Astro i18n,但做起來就是很卡。後來發現 I18n for Astro (@astrolicious/i18n) 這個套件更好用,於是就改用這個套件來做多語系功能。

安裝 @astrolicious/i18n

@astrolicious/i18n 是基於 i18next 這個套件來做的,因此也需要安裝 i18next:

Terminal window
yarn add i18next @astrolicious/i18n

然後註冊 i18n 套件:

astro.config.ts
import i18n from '@astrolicious/i18n'
export default defineConfig({
integrations: [
i18n({
defaultLocale: 'zh-TW',
locales: ['zh-TW', 'en'],
}),
],
})

在 Layout 中加入 I18nHead 元件來處理 SEO 相關的 meta:

src/layouts/Layout.astro
---
import I18nHead from '@astrolicious/i18n/components/I18nHead.astro'
import { getHtmlAttrs, getLocale } from 'i18n:astro'
const locale = getLocale()
const htmlLangMap: Record<string, string> = {
'zh-TW': 'zh-Hant-TW',
}
const htmlAttrs = getHtmlAttrs()
htmlAttrs.lang = htmlLangMap[locale] || locale
---
<!doctype html>
<html {...htmlAttrs}>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<I18nHead />
</head>
<body>
...
</body>
</html>

設定多語系檔案

src/locales/ 目錄下建立語系檔案,檔案格式為 JSON:

src/locales/en/common.json
{
"nav_about": "About",
"nav_home": "Home",
"nav_posts": "Posts"
}
src/locales/zh-TW/common.json
{
"nav_about": "關於",
"nav_home": "首頁",
"nav_posts": "文章"
}

使用翻譯

然後在元件或頁面中使用 t 函式來取得翻譯:

src/layouts/Layout.astro
---
import { t, getHtmlAttrs, getLocale, getLocalePath } from 'i18n:astro'
---
<!doctype html>
<html {...htmlAttrs}>
<head>
...
</head>
<body>
<ul class="menu">
<li class:list={['menu-item', pathname === getLocalePath('/') && 'active']}>
<a href={getLocalePath('/')}>{t('nav_home')}</a>
</li>
<li class:list={['menu-item', pathname.startsWith(getLocalePath('/posts/')) && 'active']}>
<a href={getLocalePath('/posts/')}>{t('nav_posts')}</a>
</li>
<li class:list={['menu-item', pathname === getLocalePath('/about/') && 'active']}>
<a href={getLocalePath('/about/')}>{t('nav_about')}</a>
</li>
</ul>
</body>
</html>

新增語言選單

可以顯示語系文字後,就可以新增語言選單切換語系:

src/layouts/Layout.astro
---
import { t, getHtmlAttrs, getLocale, getLocalePath, getSwitcherData } from 'i18n:astro'
const switcherData = getSwitcherData()
const switcherLabels: Record<string, string> = {
en: 'English',
'zh-TW': '繁體中文',
}
---
<!doctype html>
<html {...htmlAttrs}>
<head>
...
</head>
<body>
<ul class="menu">
...
<li class="menu-item menu-item-lang menu-item-has-children">
<a href="#">
<i class="fas fa-globe"></i>
{switcherLabels[locale]}
</a>
<ul class="sub-menu">
{switcherData.map(item => (
<li class:list={['menu-item', item.locale === locale && 'active']}>
<a href={`${item.href.replace(/\/$/, '')}/`}>{switcherLabels[item.locale]}</a>
</li>
))}
</ul>
</li>
</ul>
</body>
</html>

完整程式碼

以下是完整的 Layout 程式碼:

src/layouts/Layout.astro
---
import { t, getHtmlAttrs, getLocale, getLocalePath, getSwitcherData } from 'i18n:astro'
const locale = getLocale()
const switcherData = getSwitcherData()
const switcherLabels: Record<string, string> = {
en: 'English',
'zh-TW': '繁體中文',
}
const htmlLangMap: Record<string, string> = {
'zh-TW': 'zh-Hant-TW',
}
const htmlAttrs = getHtmlAttrs()
htmlAttrs.lang = htmlLangMap[locale] || locale
---
<!doctype html>
<html {...htmlAttrs}>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<I18nHead />
</head>
<body>
<ul class="menu">
<li class:list={['menu-item', pathname === getLocalePath('/') && 'active']}>
<a href={getLocalePath('/')}>{t('nav_home')}</a>
</li>
<li class:list={['menu-item', pathname.startsWith(getLocalePath('/posts/')) && 'active']}>
<a href={getLocalePath('/posts/')}>{t('nav_posts')}</a>
</li>
<li class:list={['menu-item', pathname === getLocalePath('/about/') && 'active']}>
<a href={getLocalePath('/about/')}>{t('nav_about')}</a>
</li>
<li class="menu-item menu-item-lang menu-item-has-children">
<a href="#">
<i class="fas fa-globe"></i>
{switcherLabels[locale]}
</a>
<ul class="sub-menu">
{switcherData.map(item => (
<li class:list={['menu-item', item.locale === locale && 'active']}>
<a href={`${item.href.replace(/\/$/, '')}/`}>{switcherLabels[item.locale]}</a>
</li>
))}
</ul>
</li>
</ul>
</body>
</html>