生成AIでi18n多言語対応を簡単爆速に行う  ※プロンプト例あり

# はじめに

みなさんが普段開発しているプロダクトでは多言語対応をしていますか？


グローバル対応を実現するにあたって基本的にはi18nなどを使って自前で実装するか、
外部のツールやサービスを利用するかの2択になると思います。

ちなみにそれぞれのメリットデメリットは下記のような感じかなと思います。
<table>
  <thead>
    <tr>
      <th></th> <th>i18nで実装</th><th>外部ツールを利用</th>
    </tr>
  </thead>
  <tr　>
    <td> メリット </td> <td>費用を抑えられる。</td><td>開発工数を抑えられる。
    </td>
  </tr>
  <tr>
    <td> デメリット </td> <td>コードの可読性低下。開発運用が大変。</td><td>費用が高い(年間数十~数百万円)。</td>
  </tr>
</table>

i18nで実装する場合費用は抑えられます。

しかし多言語ファイルを生成したり、いちいち翻訳用のマッピングをjsonファイルなどで大量に作成する必要がありちょっとめんどくさいですよね。

しかし生成AIを活用することで以前までに感じていたような多言語対応のネックな部分が解消され簡単爆速に多言語対応の実装を進めることができるのです。

# 今回使うもの

### 生成AI
僕自身エディタはCursor、LLMはo1を使いました。他の組み合わせでも問題ありませんがLLMはo1かgpt-4oレベルのものを使うことが好ましいです。

### svelte-i18n
今回はsvelteの例で説明します。vueやreactの場合はそれぞれのi18nライブラリがあるのでそれをお使いください。

https://github.com/kaisermann/svelte-i18n



## ライブラリのインストールと各種設定

まずは今回使う`svelte-i18n`をインストールしましょう。
```zsh
yarn add svelte-i18n
```

## 翻訳に必要なファイルの作成

`src`ディレクトリ配下に`loacales`ディレクトリを作成します。
さらにその配下に下記3つのファイルを作成します。
- en.json
- ja.json
- i18n.ts

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617821/9c4551fb-86d2-c8f3-d9e1-8b4c07b42eec.png)

:::note warn
　上記のディレクトリ構成は一例です。プロジェクトの状況に応じてこちらは変更してください。
:::

## jsonファイルに翻訳したいテキストを書く

```
// en.json
{
  "page_title": "welcome!",
  "sign_in": "Sign in",
  "sign_up": "Sign up"
}

// ja.json
{
  "page_title": "ようこそ！",
  "sign_in": "ログイン",
  "sign_up": "登録"
}
```

## i18n.tsの設定
```ts:i18n.ts
import { locale, waitLocale, init, register, getLocaleFromNavigator } from "svelte-i18n";


register("en", () => import("./en.json"));
register("ja", () => import("./ja.json"));

export async function initializeLocale() {
  init({
    fallbackLocale: "ja", // localeの読み込みに失敗した際にどの言語を表示するか
    initialLocale: getLocaleFromNavigator(), // localeの初期値。getLocaleFromNavigator()はブラウザ(OS)で使用されている言語設定を返す。
    loadingDelay: 200,
  });
}

// 引数で("en"or"ja")を渡してlocaleに設定する関数
export function changeLang(lang: string) {
  locale.set(lang);
}

// ブラウザ(OS)で使用されている言語設定をそのままlocaleに設定する関数
export function setLocaleToBrowserLang() {
  locale.set(getLocaleFromNavigator());
}

```
ここで定義した`initializeLocale`を上流のお好きなファイル(App.Svelte)などで呼んでください。

これでi18nがinitされlocaleに応じた翻訳テキストを表示できる準備が整いました。

## 翻訳コードの埋め込み
```svelte:test.svelte
<script lang="ts">
  import { t } from 'svelte-i18n'
</script>

<main>
  <div>{$t('page_title')}</div>

  <div>{$t('sign_in')}</div>
  <div>{$t('sign_up')}</div>
</main>
```

`t`はsvelte-18nが提供しているstoreになります。

具体的には` Readable<MessageFormatter>`型になっています。

localeの値が変更になる度に発火して、変更後のlocaleの値に応じた内容をテキストで返すような仕組みになっています。
![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617821/8a209bf2-4a38-2183-b260-76b430ba1f86.gif)




# 生成AIを用いて翻訳を進めていく

今回LLMは`o1-mini`を利用して進めていきますが、`gpt-4o`等でも大丈夫です。








## 1.対象ファイルからテキストを抽出してjsonファイルの書き出す

今回のケースでは`UserForm.svelte`という既存のファイルがあると仮定し、
そのテキストの日本語の内容を`ja.json`に抽出していきます。

- まずは翻訳したいファイルとja.jsonファイルを選択します。

- 次に下記のようなプロンプトを入力してください。


```
多言語対応するめにUserForm.svelteの中でハードコーディングされているテキストのみ全て抽出してja.jsonに書いて。

下記のUserFormkeyの中になるように書いて
"UserForm" : {

}

```



実行すると下記のような出力してくれます。
めちゃくちゃいい感じですね。

大体の場合は適切にテキストを抽出してjsonに書き出してくれますが、うまくいかない場合は
プロンプトなどを微調整してみてください。




![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617821/27d3082b-f7ad-7b52-359f-078f4e38e827.png)




## 2.作成したjsonの内容で対象ファイルのテキスト部分を書き換える

ja.jsonで日本語テキストのマッピングが完成しました。

これを用いて翻訳したい対象ファイルでハードコーディングしているテキストの箇所をjsonを参照して表示するようにしていきましょう。

- 翻訳したいファイルとja.jsonファイルを選択します。
- 次に下記のようなプロンプトを入力してください。

※`{$t("sampleKey.sampleValue”)}`この部分は利用しているライブラリやプロジェクトよって書き方が異なると思います。翻訳関数を呼び出している形に合わせてカスタマイズしてください。

```
先ほど作成したja.jsonファイルの内容をもとに、
下記のような書き方でUserForm.svelteを書き換えてください

{$t("sampleKey.sampleValue”)}

そのままコピペして使えるように修正後のファイル全体のコードを共有してください。

```

実行すると下記のような出力してくれます。
こちらもめちゃくちゃいい感じで出力してくますね。

ここで出力されたコードをそのまま翻訳対象のファイルの内容として書き換えてください。




![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617821/a6a9eb50-724c-26dd-6455-2a72dbbf4160.png)


```diff_svelte
diff --git a/src/components/User/UserForm.svelte b/src/components/User/UserForm.svelte
index d86a51e..09b7645 100644
--- a/src/components/User/UserForm.svelte
+++ b/src/components/User/UserForm.svelte
@@ -2,6 +2,7 @@
   import type { User } from '$src/types/user'
   import { UserRoleValues } from '$src/types/user'
   import { createEventDispatcher } from 'svelte'
+  import { t } from 'svelte-i18n' // Assuming you are using svelte-i18n or a similar library
 
   export let user: User
   export let formType: FormType
@@ -29,17 +30,17 @@
   $: userRoleLabel = (() => {
     switch (userVal.role) {
       case UserRoleValues.Staff:
-        return 'スタッフ'
+        return $t('UserForm.staff')
       case UserRoleValues.Admin:
-        return '管理者'
+        return $t('UserForm.admin')
       default:
         return ''
     }
   })()
 
   const roleOptions = [
-    { id: 0, value: 'スタッフ' },
-    { id: 1, value: '管理者' },
+    { id: 0, value: $t('UserForm.staff') },
+    { id: 1, value: $t('UserForm.admin') },
   ]
 
   const inputValue = (key: string) => {
@@ -77,7 +78,7 @@
   }
 
   const clickDelete = () => {
-    if (window.confirm('本当に削除しますか？')) {
+    if (window.confirm($t('UserForm.confirmDelete'))) {
       dispatch('delete')
     }
   }
@@ -123,14 +124,14 @@
               class="hover:bg-indigo-500 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
               on:click|preventDefault={clickEdit}
             >
-              編集
+              {$t('UserForm.edit')}
             </button>
             <button
               type="submit"
               class="rounded-md bg-red-100 px-3 py-2 text-sm font-semibold hover:bg-red-100 text-red-700 shadow-sm bg-red-200 focus-visible:outline"
               on:click|preventDefault={clickDelete}
             >
-              削除
+              {$t('UserForm.delete')}
             </button>
           </div>
         {/if}
@@ -142,11 +143,11 @@
             <label
               for="first-name"
               class="block text-sm font-medium leading-6 text-gray-900"
-              >名前
+              >{$t('UserForm.name')}
               {#if !isViewMode}
                 <span
                   class="ml-2 inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/10"
-                  >必須</span
+                  >{$t('UserForm.required')}</span
                 >
               {/if}
             </label>
@@ -176,11 +177,11 @@
 
               {#if !isInitialForm.name && isEmptyName}
                 <p class="mt-2 text-sm text-red-600" id="first-name-error">
-                  名前を入力してください
+                  {$t('UserForm.enterName')}
                 </p>
               {:else if !isInitialForm.name && isLengthOverName}
                 <p class="mt-2 text-sm text-red-600" id="last-name-error">
-                  名前は50文字以内で入力してください
+                  {$t('UserForm.nameLength')}
                 </p>
               {/if}
             </div>
@@ -190,7 +191,7 @@
             <label
               for="first-name"
               class="block text-sm font-medium leading-6 text-gray-900"
-              >権限</label
+              >{$t('UserForm.role')}</label
             >
             <div class="mt-2 h-20">
               {#if !isViewMode}
@@ -221,11 +222,11 @@
             <label
               for="email"
               class="block text-sm font-medium leading-6 text-gray-900"
-              >メールアドレス
+              >{$t('UserForm.email')}
               {#if !isViewMode}
                 <span
                   class="ml-2 inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/10"
-                  >必須
+                  >{$t('UserForm.required')}
                 </span>{/if}</label
             >
             <div class="relative mt-2 h-20">
@@ -254,11 +255,11 @@
 
               {#if !isInitialForm.email && isEmptyEmail}
                 <p class="mt-2 text-sm text-red-600" id="email-error">
-                  メールアドレスを入力してください
+                  {$t('UserForm.enterEmail')}
                 </p>
               {:else if !isInitialForm.email && isLengthOverEmail}
                 <p class="mt-2 text-sm text-red-600" id="email-error">
-                  メールアドレスは225文字以内で入力してください
+                  {$t('UserForm.emailLength')}
                 </p>
               {/if}
             </div>
@@ -269,11 +270,11 @@
               <label
                 for="email"
                 class="block text-sm font-medium leading-6 text-gray-900"
-                >パスワード
+                >{$t('UserForm.password')}
                 {#if !isViewMode}
                   <span
                     class="ml-2 inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/10"
-                    >必須</span
+                    >{$t('UserForm.required')}</span
                   >
                 {/if}
               </label>
@@ -302,12 +303,11 @@
 
                 {#if !isInitialForm.password && isEmptyPassword}
                   <p class="mt-2 text-sm text-red-600" id="email-error">
-                    パスワードを入力してください
+                    {$t('UserForm.enterPassword')}
                   </p>
                 {:else if !isInitialForm.password && isInvalidPassword}
                   <p class="mt-2 text-sm text-red-600" id="email-error">
-                    パスワードは英数字両方を含む<br
-                    />8文字以上32文字以下で入力してください
+                    {$t('UserForm.passwordInvalid')}
                   </p>
                 {/if}
               </div>
@@ -327,7 +327,7 @@
             : 'hover:bg-indigo-500'}"
           on:click|preventDefault={clickCreate}
         >
-          作成する
+          {$t('UserForm.create')}
         </button>
       {/if}
     </div>
@@ -339,7 +339,7 @@
           class="rounded-md bg-gray-600 px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 hover:bg-gray-500"
           on:click|preventDefault={clickCancel}
         >
-          キャンセル
+          {$t('UserForm.cancel')}
         </button>
         <button
           disabled={hasInvalidValue}
@@ -349,7 +349,7 @@
             : 'hover:bg-indigo-500'}"
           on:click|preventDefault={clickUpdate}
         >
-          更新
+          {$t('UserForm.update')}
         </button>
       {/if}
     </div>```
```

## 3.作成したja.sonの内容をもとにen.jsonファイルの内容を生成する

ここまでの手順でja.jsonファイルの内容とその内容を呼び出し元で呼ぶ準備が整いました。

最後にja.jsonの日本語テキストをもとにen.jsonフィルの内容を生成し、
英語テキストが画面上に表示されるようにしましょう。

具体的な手順は下記になります。

- ja.jsonファイル内で先ほど作成したマッピングの箇所を選択します。
- en.jsonファイルを選択します。
- 次に下記のようなプロンプトを実行してください。
 
```
ja.jsonのkeyは同一でvalue値は英語翻訳したものをen.jsonに書き出して
対象は先ほど作成したUserFormのkeyです。
```

実行結果は下記のようになっています。
こちらもいい感じですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617821/829e4fbf-269c-5f52-d4a2-2cfd474b3709.png)


実際に画面上で確認してみるとlocaleの言語設定に応じて日英のテキストがうまく表示されていることが確認できます。生成AIによる翻訳対応が無事完了しました:clap:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617821/78f2c4b3-6af1-8a13-d3ab-f398cd694347.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617821/c3c2370d-c812-d1c5-dac5-13cf293d72b1.png)


# 【さいごに】VSCode拡張機能「 i18n Alliy 」も使うとより生産性高く多言語対応ができる件

これまでは生成AIを通じて簡単爆速に多言語対応の実装コードを作成する手順をご紹介していきました。

しかし作成した翻訳のコードがどんどん実装ファイルに増えてくると可読性が大きく低下し、開発効率や思わぬバグにつながるケースが多々発生してくることが想定されます。

それらの悩みを解決してくれるとってもVSCodeの拡張機能が「i18n Alliy」になります。

VSCodeを使っている人はマストで入れた方が良いツールだと思います。
詳しくは下記の記事で書いてありますので、ご覧になってみてください。

https://qiita.com/kudotaka/items/9b3ec34d80ce5050ed01