スマホアプリを作るなら、世界のマーケットを攻めた方がコストパフォーマンスが良いでしょう。日本のスマホユーザーだけを相手にするより、世界中に向けたほうが当然マーケットが大きいからです。
世界に向けてアプリを出す場合、多言語化対応は面倒ですが必須となります。
expo-localization を使ってアプリの言語をローカライズ(i18n対応)する
はじめに expo-localization
をインストールします。
npx expo install expo-localization
国際化対応するためのファイルを作成します。
mkdir -p functions/i18n
touch functions/i18n/i18n.ts
i18n.ts
に以下のように書いてみましょう。
import { getLocales } from 'expo-localization';
export const deviceLanguage = getLocales()[0].languageCode;
console.log('deviceLanguage', deviceLanguage); /ja と表示される
端末の言語情報が取得できます。日本語の場合は ja
です。
以下のドキュメントが参考になります。
i18n-jsで翻訳する
i18n
パッケージをインストールする。
npx expo install i18n-js
i18n.ts
を以下のように編集すると、「こんにちは」というログが出力されます。
import { getLocales } from 'expo-localization';
import { I18n } from 'i18n-js';
// Set the key-value pairs for the different languages you want to support.
export const i18n = new I18n({
en: { welcome: 'Hello' },
ja: { welcome: 'こんにちは' },
});
// Set the locale once at the beginning of your app.
export const deviceLanguage = getLocales()[0].languageCode;
i18n.locale = deviceLanguage;
console.log(i18n.t('welcome'));
Localization.locale は ja-US(言語 + 地域) のような文字列になる
上の例では languageCode
を使っていますが、 公式ドキュメントには Localization.locale
で言語設定を取得するような例があります。 locale
だと ja-US
のように、言語 + 地域の情報が取得されてしまいます。
import * as Localization from 'expo-localization';
import { I18n } from 'i18n-js';
// Set the key-value pairs for the different languages you want to support.
const i18n = new I18n({
en: { welcome: 'Hello' },
ja: { welcome: 'こんにちは' },
});
// Set the locale once at the beginning of your app.
export const deviceLanguage = Localization.locale;
i18n.locale = deviceLanguage;
console.log(i18n.t('welcome'));
上の例だと LOG [missing "ja-US.welcome" translation]
というエラーが出ます。なぜなら、 ja
と ja-US
がマッチしないからです。
fallback language を設定する
設定されていない言語が端末に設定されていた場合、デフォルトで設定した言語にするような設定ができます。
以下の例では、あえて ja
を設定ファイルから消しています。シミュレーターの言語は ja
ですが、設定ファイルにないので、翻訳できません。
そんなときに、 デフォルトで設定したen
で翻訳してもらえれば助かります。最終的に以下の i18n.ts
を用意すれば良さそうです。
import { getLocales } from 'expo-localization';
import { I18n } from 'i18n-js';
export const i18n = new I18n({
en: { welcome: 'Hello' },
ze: { welcome: 'こんにちは' },
});
i18n.locale = getLocales()[0].languageCode;
i18n.enableFallback = true;
i18n.defaultLocale = 'en';
コンポーネント側で利用する
以下のように {i18n.t('welcome')}
とすればOKです。
<Text style={{ fontFamily: 'VarelaRound_400Regular', fontSize: 50 }}>
{i18n.t('welcome')}
</Text>
i18n.t に変数を渡して、動的に翻訳メッセージを変更する方法
「◯◯を削除しますか?」の 「◯◯」 の部分に変数で動的に値を指定したいケースもあるでしょう。以下のようにします。
`${i18n.t('deleteAlertDescriptionWithName', { name: `${repeatTask.name}` })}`
i18n.t
の第2引数に変数を渡せばOKです。
翻訳を定義しているオブジェクトには以下のようにします。
deleteAlertDescriptionWithName: 'Is it okay to delete {{name}}?',
{{}}
で渡す変数名を囲ってあげればOKです。
Unable to resolve “make-plural” from “node_modules/i18n-js/dist/import/Pluralization.js”
iOS Bundling failed
Unable to resolve "make-plural" from "node_modules/i18n-js/dist/import/Pluralization.js"
2023年8月に突然、上記のようなエラーが出ましたが、 i18n-js
のバージョンを 4.3.0
から 4.2.0
にダウングレードしたら直りました。
"i18n-js": "4.2.0"
ダウングレードせずに解決するには、 metro.config.js を以下のように修正すればOKです。
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname, {
// [Web-only]: Enables CSS support in Metro.
isCSSEnabled: true,
});
// Adds support for `mjs` files
config.resolver.sourceExts.push('mjs');
module.exports = config;
その他、国際化に必要そうな設定まとめ
数字のカンマの区切り方や $
, ¥
などの違いも国際化対応には必要です。日付の表示の仕方も異なるので、それぞれフォーマッタがあったほうがいいです。
import { getLocales } from 'expo-localization';
import {I18n} from "i18n-js";
export const i18n = new I18n({
en: { welcome: 'Hello' },
ja: { welcome: 'こんにちは' },
});
export const languageTag = getLocales()[0].languageTag;
export const languageCode = getLocales()[0].languageCode;
export const numericFormatter = new Intl.NumberFormat(languageTag);
export const dateTimeFormatter = new Intl.DateTimeFormat(languageTag);
export const currencyCode = getLocales()[0].currencyCode;
export const currencyFormatter = new Intl.NumberFormat(languageTag, {
style: 'currency',
currency: currencyCode ?? 'USD',
});
export const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
i18n.locale = languageCode;
i18n.enableFallback = true;
i18n.defaultLocale = 'en';
i18n の定義に型を設定してちょっと安全につかう
言語間で翻訳漏れを防ぐために、型を設定します。
コピーしました!
export interface TextDefinition {
common: {
save: string;
selectDate: string;
};
}
export interface Translations {
en: TextDefinition;
ja: TextDefinition;
}
翻訳ファイルを作成します。 自分は mkdir functions/i18n/translatios
以下に各言語の翻訳ファイルを作成しています。
コピーしました!
touch functions/i18n/translatios
/en.ts
touch functions/i18n/translatios
/ja.ts
touch functions/i18n/translatios
/index.ts
各ファイルに以下のように書きます。
コピーしました!
// ja.ts
import { type TextDefinition } from '@/functions/i18n/i18nTypes';
export const ja: TextDefinition = {
common: {
save: '保存',
selectDate: '日付を選択',
},
};
// en.ts
import { type TextDefinition } from '@/functions/i18n/i18nTypes';
export const en: TextDefinition = {
common: {
save: '保存',
selectDate: '日付を選択',
},
};
// index.ts
import { type Translations } from '@/functions/i18n/i18nTypes';
import { ja } from '@/functions/i18n/translations/ja';
import { en } from '@/functions/i18n/translations/en';
export const translations: Translations = {
ja,
en,
};
functions/i18n/i18n.ts
に以下のように記載します。
コピーしました!
import { getLocales, getCalendars } from 'expo-localization';
import { I18n } from 'i18n-js';
import { translations } from '@/functions/i18n/translations';
export const i18n = new I18n();
export const deviceLanguage = getLocales()[0]?.languageCode;
const { timeZone } = getCalendars()[0];
export const deviceTimeZone = timeZone ?? 'Asia/Tokyo';
const fallbackLanguage = 'ja';
i18n.locale = deviceLanguage ?? fallbackLanguage;
// ここで型付きの翻訳ファイルを設定
i18n.translations = translations;
i18n のキーにも型を設定して、i18nをラップしてキーの打ち間違いを防ぐ
コピーしました!
export interface TextDefinition {
common: {
complete: string;
};
}
export interface Translations {
en: TextDefinition;
ja: TextDefinition;
}
type DotNotation<T, Prefix extends string = ''> = {
[K in Extract<keyof T, string>]: T[K] extends object
? DotNotation<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[Extract<keyof T, string>];
export type TranslationKeys = DotNotation<TextDefinition>;
i18n.ts
に i18n をラップした translate
関数を作ります。
コピーしました!
import { getLocales, getCalendars } from 'expo-localization';
import { I18n, type TranslateOptions } from 'i18n-js';
import { translations } from '@/functions/i18n/translations';
import { type TranslationKeys } from '@/functions/i18n/i18nTypes';
export const i18n = new I18n();
export const deviceLanguage = getLocales()[0]?.languageCode;
const { timeZone } = getCalendars()[0];
export const deviceTimeZone = timeZone ?? 'Asia/Tokyo';
const fallbackLanguage = 'ja';
i18n.locale = deviceLanguage ?? fallbackLanguage;
i18n.translations = translations;
export function translate(key: TranslationKeys, options?: TranslateOptions): string {
return i18n.translate(key, options);
}
使う側は以下のように translate
関数を呼び出して使います。
コピーしました!
<Text>{translate('common.complete')}</Text>
ここの common.complete
の部分が予測変換で出てくるし、型チェックしてくれるので、キーの部分(i18n の Scope)を打ち間違えることもなくなりました。