React Native 奇幻之旅(20)-多語系切換 (react-i18next)

React Native logo

i18n 是什麼

YGz60MR

i18n 是國際化(internationalization)的縮寫,開頭i跟n中間有18個英文字母所以簡稱叫 i18n

簡單來說,i18n 是一種使應用能夠在不同語言系統下使用且不需要對程式碼進行大規模修改的方式。

為什麼需要 i18n

  • 用戶來源:如果你的應用需要在不同國家、地區上架,為了讓不同語言母語者能夠使用就需要 i18n。
  • 特定法規:某些國家或地區可能對系統語言有特定的法規要求,比如大陸就要求要支持簡體中文、俄羅斯要求應用支持俄語…等。

總之,i18n 是一個常見的需求,現在大部分的應用都支持多語系。我個人比較常使用 react-i18next 來做 i18n,這篇文章會分享使用方式以及常見的一些設置。

安裝及初始化

首先需要先安裝 react-i18next 和 i18next

npm install react-i18next i18next --save

在根目錄底下建一個 locale 資料夾,專門用來管理 i18n 的檔案。

URabA2W

新增 i18n.ts,初始化 i18next

import { initReactI18next } from 'react-i18next'
import i18n from 'i18next'

import en from './en.json'
import zh_tw from './zh-tw.json'

export const resources = {
  en: {
    translation: en,
  },
  zh_tw: {
    translation: zh_tw,
  },
}

i18n
  .use(initReactI18next)
  .init({
    resources,
    fallbackLng: {
      'en-*': ['en'],
      'zh-*': ['zh_tw']
    },
    lng: 'zh_tw', <em>// 預設為繁體中文</em>
  })

export default i18n

這邊引入的 en.json 和 zh-tw.json 是英文和繁體中文的語系檔:

<em>// zh-tw.json</em>
{
  "Login": "登入",
  "Email": "信箱",
  "Password": "密碼",
  "Confirm": "確認",
  "Cancel": "取消"
}

設定好之後在 index.ts 引入 i18n.ts

import App from 'App'
import { name as appName } from './app.json'
import './locale/i18n'

AppRegistry.registerComponent(appName, () => App)

基本使用

使用 useTranslation 這個 hook 回傳的 t 函數並傳入 i18n key:

import { Text } from 'react-native'
import { useTranslation } from 'react-i18next'

export const App = (): JSX.Element => {
    const { t } = useTranslation()

    return (
        <>
            <Text>{t('Home')}</Text>
            <Text>{t('Error.SomethingWrong')}</Text>
        </>
    )    
}
<em>// zh-tw.json</em>
{
    "Home": "首頁",
    "Error": {
        "SomethingWrong": "發生了預期外的錯誤..."
    }
}
<em>// en.json</em>
{
    "Home": "Home",
    "Error": {
        "SomethingWrong": "Something wrong..."
    }
}

IDE 可能會提示下面這個 error,雖然是 Error 但實際上還是可以正常使用

i18next::pluralResolver: 
Your environment seems not to be Intl API compatible, 
use an Intl.PluralRules polyfill. 
Will fallback to the compatibilityJSON v3 format handling.

這個錯誤其實官方也有列在 FAQ 中,就是說你當前的環境不兼容 Intl API,要改用 Intl.PluralRules

npm install intl-pluralrules --save

在 index.js 中引入 intl-pluralrules 就能解決。

<em>// index.js</em>
import 'intl-pluralrules'

多層結構

為了讓語系檔案方便閱讀,可以將同類型、同頁面的 i18n 放在同一層,比如將錯誤訊息的 i18n 都放在 Error 底下:

{
    "Login": "登入",
    "Register": "註冊",
    "Home": "首頁",
    "Email": "電子信箱",
    "Password": "密碼",
    "Error": {
        "Required": "{{field}}必填",
        "Invalid": "無效的{{field}}"
    }
}

如果要使用 Error 底下的 i18n 預設是用 . 連接:

t('Error.Required', { field: t('Email') })

自定義連接字符

如果需要自定義連接的字符,可以設置 keySeparator

<em>// i18n.ts</em>
i18n.use(initReactI18next).init({
  <em>// ...</em>
  keySeparator: '/'
})

就變成用 / 連接

t('Error/Required', { field: t('Email') })

切換語系

使用 i18n.changeLanguage 方法並傳入語系名稱就能切換成指定語系

<em>// App.tsx</em>
import { useTranslation } from 'react-i18next'

export const App = (): JSX.Element => {
  const { i18n } = useTranslation()
  
  const changeLanguage = () => {
      i18n.changeLanguage('en')
  }

  <em>// ...  </em>
}

但前提是在 i18n.ts 中需要設置該語系的翻譯檔。

假如只有設置 en 和 zh_tw 卻想切換成 vi 會發生什麼事?

<em>// i18n.ts</em>
export const resources = {
  en: {
    translation: en,
  },
  zh_tw: {
    translation: zh_tw,
  },
}

i18n
.use(initReactI18next)
.init({
  resources,
  fallbackLng: 'en',
  lng: 'zh_tw',
})

export default i18n

如果有設置 fallbackLng,當找不到指定的語言時就會自動切成 fallbackLng 設置的語言,這邊是 en

那如果沒有設置 fallbackLng 的話,就是會將 i18n key 直接顯示出來,比如 {t('DemoKey')} 顯示出來的就是 DemoKey

而 fallbackLng 下方的 lng 就是 App 預設語言,不管設備的系統語言設置為何,初次開啟 App 一律翻譯為該語言,也就是 zh_tw

IDE 自動提示

當我設置了一個新的 i18n key HomePage 的時候,我希望在我輸入 Home 它就會自動提示我有 HomePage 可以選擇,但它現在還無法做到,因為還需要建立型別聲明檔。

jyYjQGY

在 locale 中新建 i18next.d.ts

<em>// i18next.d.ts</em>
import 'i18next'
import { resources } from "./i18n"

declare module 'i18next' {
  interface CustomTypeOptions {
    resources: typeof resources['zh_tw']
  }
}

這個意思就是會從 zh_tw.json 中自動抓取所有翻譯的 key 和對應的 value,並且根據這些 key value 建立一個型別 CustomTypeOptions,用於提示我們有哪些 i18n key 是可以使用的。

9PhBXO6

自動推斷單複數

這是 i18next 預設定義好的分辨單複數的命名方式, 在 key 後面加上 _other 就會自動使用複數的 i18n

(當前語系是要有複數的語系才會自動轉換, 比如英文, 阿拉伯文…等)

{
  "hours": "{{ count }} hour",
  "hours_other": "{{ count }} hours",
  "minutes": "{{ count }} minute",
  "minutes_other": "{{ count }} minutes",
  "seconds": "{{ count }} second",
  "seconds_other": "{{ count }} seconds",
  "diff": "$t(hours, {\"count\": {{ hours }} }) $t(minutes, {\"count\": {{ minutes }} }) $t(seconds, {\"count\": {{ seconds }} })"
}
t('hours', { count: 10 }) 
<em>// 10 hours</em>
t('hours', { count: 1 })
<em>// 1 hour</em>
t('diff', { hours: 1, minutes: 20, seconds: 15 }) 
<em>// 1 hour 20 minutes 15 seconds</em>

那如果是 0 怎麼辦?實際上以上面的例子 0 會選擇的是複數i18n:

t('hours', { count: 0 }) <em>// 0 hours</em>

如果希望單獨為 0 定義的話,可以在後面加上 _zero

"hours": "{{ count }} hour",
"hours_other": "{{ count }} hours",
"hours_zero": "Now",
t('hours', { count: 0 }) <em>// Now</em>

https://github.com/i18next/i18next/issues/1220

自定義單複數分隔符

i18next 預設是識別後綴為 _zero_other 的 key 名來區分單複數,但有些時候你的 key 名本身就帶這些後綴,你並不想和單複數的 i18n 搞混,就可以修改分隔符。

自定義分隔符的方式是修改 pluralSeparator

<em>// i18n.ts</em>
i18n.use(initReactI18next).init({
  resources,
  fallbackLng: 'en',
  lng: 'zh',
  pluralSeparator: '__', <em>// 兩條底線</em>
})
{
    "days": "{{count}} day",
    "days__other": "{{count}} days",
}

這樣就不會混淆了。

Trans 組件

react-i18next 內建一個 Trans 組件,可以用來處理複雜的翻譯需求。

假設我們有一個需要顯示動態內容的句子,例如:Hello {userName}, You have {count} unread message(s). Go to messages.,其中的 userName 和 count 是會隨著資料而變動的。

在傳統的寫法中,我們可能需要寫出冗長的 JSX,並且將翻譯的內容拆分成多個片段:

const App = ({ userName, messages }) => {
  const count = messages.length
  return (
    <Text>
      Hello <Text style={{ fontWeight: '500' }}>{userName}</Text>, 
      you have {count} unread message(s). 
      <Pressable onPress={() => {}}>
        <Text style={{ color: '#4682A9' }}>Go to messages</Text>
      </Pressable>.
    </Text>
  );
}

然而,當翻譯的內容包含變數和符號時,Trans 組件會自動將其拆分為多個元素,每個變數或符號都會成為一個單獨的元素。

例如,將長句 Hello {{user_name}}, you have {{count}} unread message. Go to message. 轉換成 Trans 組件的結構,會得到:

[
  'Hello ',
  { children: [{ user_name: 'Admin' }] },
  ', you have ',
  { count: 10 },
  ' unread messages. ',
  { children: ['Go to messages'] },
  '.'
]
<em>// en.json</em>
{
    "userMessagesUnread_one": "Hello <1>{{userName}}</1>, you have {{count}} unread message. <5>Go to message</5>.",
    "userMessagesUnread_other": "Hello <1>{{userName}}</1>, you have {{count}} unread messages.  <5>Go to messages</5>."
}

轉換為 Trans 組件即:

<Trans
    defaults={t('userMessagesUnread')}
    values={{ count, userName }}
    parent={Text}
    components={{
        1: <Text style={{ fontWeight: '500' }} />,
        5: <Text style={{ color: '#4682A9' }} onPress={() => {}} />
    }}
/>

透過 components 我們可以藉由指定索引來設置這個元素該給什麼樣的樣式或屬性。

gifXGei

VSCode 擴展 i18n ally

如果平時使用的 IDE 是 VSCode,推薦一個擴展 i18n-ally 給大家。

這個擴展能直接在 code 中顯示翻譯的結果,就不需要多個檔案切換來切換去的查找,可以更有效率的管理多語系翻譯。

annotation animated

react-i18next 還有很多進階的使用方式,比如說內嵌翻譯、時間格式化…等,有興趣可以到官方文檔查看。

留言

目前沒有留言。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *