Skip to content

Quden(Web)の翻訳方法(i18n)

目的

  • Quden(Web)で表示されている文字の翻訳方法が理解できる

利用されているライブラリ

関連ドキュメント

i18n.ts の解説

翻訳可能な言語設定に関して

初期実装時には、日本語(ja)と英語(en)の 2 つの言語の翻訳を用意しています。

それらは、supportedLngs にて、設定されています。

typescript
supportedLngs: ["en", "ja"];

翻訳ファイルの読み込み

resources という変数に言語ごとの翻訳ファイルを設定しています。

また、利用可能な言語すべてに対して、同じ key で翻訳ファイルを用意する必要があります。

typescript
import enCommon from "public/locales/en.json";
import jaCommon from "public/locales/ja.json";
import enChannel from "@/components/model/channel/locales/en.json";
import jaChannel from "@/components/model/channel/locales/ja.json";

export const resources = {
  en: {
    common: enCommon,
    channel: enChannel,
  },
  ja: {
    common: jaCommon,
    channel: jaChannel,
  },
};

デフォルトの翻訳ファイルについて

翻訳ファイルは一つにまとめて管理されているのではなく、複数に分けられています。

そのため通常、翻訳時には利用する翻訳ファイルの指定を行います(のちの『翻訳方法』にて説明されています)。

しかし、何も指定をしなかった場合、デフォルトで翻訳ファイルが読み込まれるように設定されています。

typescript
defaultNS: "common";

デフォルト言語の読み込みについて

Quden(Web)に初めてアクセスしたユーザーは通常、ブラウザで利用している言語で文字が表示させるようになっています。

しかし、Quden の翻訳システムは、ブラウザで利用されている言語に対して、ユーザーが DB で保持している言語で上書きする仕組みになっています。(src/layouts/Authenticated.tsx)

上書きが実行された場合、ReactComponents 内で翻訳されているテキストデータはリアクティブに言語が切り替わるようになっていますが、静的ファイル内のテキストデータは元の言語で表示されたままになっています。

これらの問題を解決するために、デフォルト言語が参照される箇所を下記の設定で書き換えています。

typescript
import i18n from "i18next";

void i18n.init({
  detection: {
    order: ["localStorage", "navigator"],
    caches: ["localStorage"],
  },
});

order と記載されているのが、参照箇所の順番になっており、 navigator はブラウザの言語情報を参照することを示しています。

ユーザーの DB 情報を元に言語の切り替えを行なった際、同時にブラウザの LocalStorage へ言語情報の書き込みを行なっています。

2 回目以降、ページにアクセスした際、静的ファイルの翻訳は localStorage で保持している言語情報を最初に参照するようになります。

i18next.d.ts の解説

こちらのファイルでは、翻訳データの補完を TS として行うためのファイルになります。

新しい翻訳ファイルを追加した場合は、追加していただく必要があります。

また、key に対して指定する翻訳ファイルは英語の翻訳ファイルになります。

typescript
import "i18next";
import { defaultNS, resources } from "@/common/i18n";

declare module "i18next" {
  // Extend CustomTypeOptions
  interface CustomTypeOptions {
    defaultNS: typeof defaultNS;
    resources: {
      common: typeof resources.en.common;
      channel: typeof resources.en.channel;
    };
  }
}

翻訳ファイルについて

基本的には locales というディレクトリ内に言語ごとの翻訳ファイルが作成されています。

また翻訳ファイルは、以下のような箇所に配置されています。

  • public/locales/ 内の common ファイル
  • 特定のディレクトリ内に配置された locales ディレクトリ内の翻訳ファイル
    • components/model/**/locales/
    • components/organisms/locales/
    • containers/locales/

翻訳変数の命名規則

翻訳変数とは、翻訳ファイル内で記載されている翻訳データを識別するための key となるものです。

yaml
// cancel や archive、shareがkeyです
{
  "cancel": "キャンセル",
  "archive": "アーカイブ",
  "share": "シェア",
  "sign-in": "サインイン"
}

命名規則に関しては、以下をルールとします。

  • 全ての key をアルファベットで記載する
  • 複数の文字を利用する場合は、ハイフン(-)で繋げる
    • 複数の翻訳データを利用する場合、 common:cancel のように指定する必要があり、key に空白を作らないようにするため(common:sign-in
  • 大文字小文字などの指定はしない
    • 最初の翻訳データの全ての key は小文字で整えられていますが、場合によっては大文字だけの key の方が良い場合もあるため
  • key に以下の文字は利用できません
    • :
    • ,
    • ?

翻訳方法(基本)

具体的なソースコードを例に実装方法を紹介します。

翻訳箇所によって翻訳方法が異なります。

  • React Components 内
  • 静的ファイル内

React Components 内

useTransaction() の引数に何も渡さなかった場合、デフォルトで public/locales/*.json の翻訳ファイルが適用されます。

typescript
import { useTranslation } from "react-i18next";

const SettingUser: FC<Props> = (props): JSX.Element => {
  const { t } = useTranslation();

  return (
    <div>
      <p>{t("hello")}</p>
    </div>
  );
};

デフォルトの翻訳ファイル以外を利用したい場合は、 useTransaction() の引数に key として受け渡しを行います。

typescript
import { useTranslation } from "react-i18next";

const SettingUser: FC<Props> = (props): JSX.Element => {
  const { t } = useTranslation("channel");

  return (
    <div>
      <p>{t("channel-name")}</p>
    </div>
  );
};

複数の翻訳ファイルを利用したい場合は、 useTransaction() に配列の形で引数の受け渡しを行います。

また翻訳箇所の key 冒頭には、 common: のように翻訳ファイルの key を指定する必要があります。

typescript
import { usetranslation } from "react-i18next";

const SettingUser: FC<Props> = (props): JSX.Element => {
  const { t } = useTranslation(["common", "channel"]);

  return (
    <div>
      <p>{t("common:hello")}</p>
      <p>{t("channel:channel-name")}</p>
    </div>
  );
};

静的ファイル内

静的ファイルとは、config.tsconstants.ts などのファイルのことを指します。

静的ファイル内では、どの翻訳ファイルを利用しているのか、明示的に記載する必要があります。

typescript
import i18next from "i18next";

const PLAN_MAP = {
  free: i18next.t("common:free-plan"),
  team: i18next.t("common:team-plan"),
};

const TAB_MAP = {
  user: i18next.t("tab:user"),
  notification: i18next.t("tab:notification"),
};

翻訳方法(応用)

こちらでは用途によっては、少し工夫したやり方を行なっている翻訳を具体的な例を挙げて紹介します。

変数の受け渡し

Quden(Web)では、チャンネル名などを文章に加えて表示させたい場合があります。

言語ごとに文法が異なるため、可変する文字を変数として受け渡しを行い、翻訳データと一緒に表示させます。

変数の受け渡しは、利用しているライブラリごとに異なりますが、Quden で利用しているライブラリでは、下記のように行います。

  • react-i18next:
  • nestjs-i18n: {text}
yaml
// 日本語翻訳
{
  "join-channel": "#{{name}}に参加"
}

// 英語翻訳
{
  "join-channel": "Join #{{name}}"
}
typescript
const { t } = useTranslation(["channel"]);
const channelName = "開発";

return (
  <div>
    <span>{t("join-channel", { name: channelName })}</span>
  </div>
);

上記のように、 t() 内で option として翻訳データ内の命名(例では name)に沿った形で変数の受け渡しを行います。

特殊文字の利用

翻訳テキスト内で、"" などを使って表現したい場合は、下記のようにバックスラッシュを用いることで実現できます。

json
{
  "say-hello": "Say \"hell\""
}

一部の翻訳文字をコンポーネントでラップしている場合

表示文字の中には、一部のテキストをリンクなどに置き換えている場合があります。

i18n のライブラリでは、string のみを変数として受け渡すことが可能なため、暫定的に以下のような対応で実装することにします。

typescript
return (
  <div>
    {i18n.language === UserLanguageMap.ja && (
      <p>
        ダウンロードは
        <a href="https://zipunk.com/">こちら</a>
      </p>
    )}
    {i18n.language === UserLanguageMap.en && (
      <p>
        <a href="https://zipunk.com/">Click here </a>
        to download
      </p>
    )}
  </div>
);

現状、日本語と英語のみが対応しているため、上記のようなやり方でも問題ないかもしれませんが、対応言語が増えるごとにコードとして不適切なものになってしまうことが予想されます。

今後もっと良い方法を考えていきたいと考えています。