├── .env.example ├── .gitignore ├── docker-compose.yml ├── package.json ├── fix_editor-ui.old.patch ├── fix_editor-ui.patch ├── README.md ├── .github └── workflows │ └── node.js.yml └── script └── translate.js /.env.example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nemumusito/n8n-i18n-japanese/HEAD/.env.example -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | .env 3 | 4 | # Dependencies 5 | node_modules/ 6 | 7 | # Build artifacts (if any, good practice to add) 8 | # dist/ 9 | # build/ 10 | 11 | # Log files 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | pnpm-debug.log* 17 | 18 | # OS generated files 19 | .DS_Store 20 | Thumbs.db -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | n8ntest: 5 | image: n8nio/n8n:latest 6 | container_name: n8ntest 7 | ports: 8 | - "15678:5678" 9 | environment: 10 | - N8N_DEFAULT_LOCALE=ja 11 | - N8N_SECURE_COOKIE=false 12 | volumes: [] 13 | stdin_open: true 14 | tty: true 15 | restart: unless-stopped 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "n8n-i18n-chinese", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "i18n:translate": "node ./script/translate.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/other-blowsnow/n8n-i18n-chinese.git" 12 | }, 13 | "private": true, 14 | "devDependencies": { 15 | "dotenv": "^16.4.7" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /fix_editor-ui.old.patch: -------------------------------------------------------------------------------- 1 | Subject: [PATCH] fix: When translation is needed, the page console prompts null data. 2 | --- 3 | Index: packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue 4 | IDEA additional info: 5 | Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP 6 | <+>UTF-8 7 | =================================================================== 8 | diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue 9 | --- a/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue (revision d2dd1796a871ee41681acc44ad01dfb0bbd5eee1) 10 | +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue (date 1741319010499) 11 | @@ -96,6 +96,8 @@ 12 | props.credentialType.name, 13 | ); 14 | 15 | + if (!credTranslation) return; 16 | + 17 | addCredentialTranslation( 18 | { [props.credentialType.name]: credTranslation }, 19 | rootStore.defaultLocale, 20 | -------------------------------------------------------------------------------- /fix_editor-ui.patch: -------------------------------------------------------------------------------- 1 | Subject: [PATCH] fix: When translation is needed, the page console prompts null data. 2 | --- 3 | Index: packages/frontend/editor-ui/src/components/CredentialEdit/CredentialConfig.vue 4 | IDEA additional info: 5 | Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP 6 | <+>UTF-8 7 | =================================================================== 8 | diff --git a/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialConfig.vue b/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialConfig.vue 9 | --- a/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialConfig.vue (revision d2dd1796a871ee41681acc44ad01dfb0bbd5eee1) 10 | +++ b/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialConfig.vue (date 1741319010499) 11 | @@ -96,6 +96,8 @@ 12 | props.credentialType.name, 13 | ); 14 | 15 | + if (!credTranslation) return; 16 | + 17 | addCredentialTranslation( 18 | { [props.credentialType.name]: credTranslation }, 19 | rootStore.defaultLocale, 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # n8n 日本語化プロジェクト 2 | 3 | このプロジェクトは、n8n のユーザーインターフェース (UI) を日本語で利用可能にすることを目的としています。 4 | 5 | **多くのユーザーにとって最も簡単な方法は、GitHub Releases からビルド済みの日本語化パッケージをダウンロードすることです。** 6 | 7 | [**最新の日本語化パッケージはこちらからダウンロード (GitHub Releases)**](https://github.com/nemumusito/n8n-i18n-japanese/releases) 8 | 9 | このリリースには、特定の n8n バージョンに対応した翻訳済み `editor-ui` が `editor-ui.tar.gz` として含まれています。 10 | これを利用することで、煩雑なビルド作業なしに n8n を日本語で利用開始できます。 11 | 12 | ## 主な機能 13 | 14 | * **翻訳済み `editor-ui` の配布 (推奨)**: 最新の n8n リリースに対応した、日本語化済みの `editor-ui` ビルド成果物を [GitHub Releases](https://github.com/nemumusito/n8n-i18n-japanese/releases) で公開しています。 15 | * **日本語翻訳ファイルの提供**: n8n の UI テキストに対応する日本語の翻訳ファイル ([`languages/ja.json`](languages/ja.json:0)) を提供します。これは主に開発者や、自身でビルドを行いたい方向けです。 16 | * **Docker イメージでの簡易利用**: `docker-compose` を使用して、日本語化された n8n 環境を簡単に起動できます。 17 | 18 | ## 利用方法 19 | 20 | ### 1. GitHub Releases からパッケージを取得して利用する (推奨) 21 | 22 | 1. **パッケージのダウンロード**: 23 | [GitHub Releases ページ](https://github.com/nemumusito/n8n-i18n-japanese/releases) から、利用したい n8n のバージョンに対応する `editor-ui.tar.gz` をダウンロードします。 24 | 2. **パッケージの展開**: 25 | ダウンロードした `editor-ui.tar.gz` を展開し、`editor-ui-dist` ディレクトリを取得します。 26 | 3. **Docker で n8n を起動**: 27 | 以下のいずれかの方法で Docker を利用して n8n を起動します。 28 | 29 | * **`docker-compose.yml` を利用する場合 (簡単):** 30 | 1. このリポジトリをクローンまたはダウンロードします。 31 | ```shell 32 | git clone https://github.com/nemumusito/n8n-i18n-japanese.git # またはあなたのフォーク 33 | cd n8n-i18n-japanese 34 | ``` 35 | 2. 取得した `editor-ui-dist` ディレクトリを、クローンしたリポジトリのルート (`docker-compose.yml` と同じ階層) に配置します。 36 | 3. `docker-compose.yml` 内の `image: n8nio/n8n:{version}` の `{version}` を、ダウンロードしたパッケージが対応する n8n のバージョンに合わせてください (例: `n8nio/n8n:1.23.0`)。 37 | 4. Docker Compose を起動します。 38 | ```shell 39 | docker-compose up 40 | ``` 41 | これにより、配置した `editor-ui-dist` がコンテナにマウントされ、n8n が日本語 UI で起動します。 42 | 43 | * **`docker run` コマンドを利用する場合:** 44 | ```shell 45 | # 【】内はご自身の環境に合わせて変更してください 46 | docker run -it --rm --name n8n-japanese-test \ 47 | -p 15678:5678 \ 48 | -v 【ローカルの editor-ui-dist ディレクトリのパス】:/usr/local/lib/node_modules/n8n/node_modules/n8n-editor-ui/dist \ 49 | -e N8N_DEFAULT_LOCALE=ja \ 50 | -e N8N_SECURE_COOKIE=false \ 51 | n8nio/n8n:【n8nのバージョン】 52 | ``` 53 | 【n8nのバージョン】には、ダウンロードしたパッケージが対応するバージョンを指定してください。 54 | 55 | ### 2. 手動で翻訳とビルドを行う場合 (開発者向け) 56 | 57 | 最新の翻訳を自分で生成したり、開発目的で利用したりする場合は、以下の手順で手動実行できます。 58 | 59 | 1. **環境変数の設定**: 60 | * n8n のデフォルト言語を日本語に設定します。プロジェクトルートに `.env` ファイルを作成し、以下を記述するか、環境変数として設定してください。 61 | ``` 62 | N8N_DEFAULT_LOCALE=ja 63 | ``` 64 | * 翻訳スクリプトは Gemini API を利用します。プロジェクトルートに `.env` ファイルを作成 (または `.env.example` をコピーして編集) し、以下の環境変数を設定してください。 65 | ```dotenv 66 | # Gemini API Key (必須) 67 | GEMINI_API_KEY="YOUR_GEMINI_API_KEY_HERE" 68 | 69 | # Gemini Model (任意、デフォルト: gemini-pro) 70 | # 例: GEMINI_MODEL="gemini-1.5-flash-latest" 71 | GEMINI_MODEL="gemini-pro" 72 | 73 | # 翻訳プロンプト (任意、未設定の場合はスクリプト内のデフォルトプロンプトを使用) 74 | # LLMに翻訳方法を指示するプロンプトです。 75 | # カスタマイズする場合は、スクリプトが期待するプレースホルダー (${language}, ${uniqueSeparator}, ${textsToTranslate}) を含める必要があります。 76 | # 詳細は .env.example ファイルおよび script/translate.js 内のデフォルトプロンプトを参照してください。 77 | # TRANSLATION_PROMPT="ここにカスタムプロンプトを記述..." 78 | # 79 | # 翻訳対象言語 (任意、未設定の場合は日本語のみを対象とします) 80 | # JSON形式の文字列で、オブジェクトの配列として指定します。 81 | # 各オブジェクトは "name" (言語コード) と "label" (言語名) を持つ必要があります。 82 | # 例: TARGET_LANGUAGES='[{"name":"ja","label":"日本語"},{"name":"en","label":"English"}]' 83 | # TARGET_LANGUAGES='[{"name":"ja","label":"日本語"}]' 84 | ``` 85 | 2. **依存関係のインストール**: 86 | ```shell 87 | npm install 88 | ``` 89 | 3. **翻訳スクリプトの実行**: 90 | 以下のコマンドで、最新の英語ロケールファイルを取得し、日本語に翻訳して [`languages/ja.json`](languages/ja.json:0) を更新します。 91 | ```shell 92 | npm run i18n:translate 93 | ``` 94 | このスクリプトは、[`script/en.json`](script/en.json:0) (前回取得した英語ロケール) と比較し、差分のみを翻訳します。 95 | 4. **ビルド済み `editor-ui` の配置**: 96 | 上記スクリプトは翻訳ファイルを作成するのみです。n8n 本体に日本語 UI を適用するには、n8n のソースコードを取得し、`languages/ja.json` を配置した上で `editor-ui` パッケージをビルドし、生成された `dist` ディレクトリを n8n のインストール先に配置する必要があります。 97 | このリポジトリの GitHub Actions では、このビルドプロセスを自動化し、`editor-ui-dist` ディレクトリを生成して GitHub Releases に公開しています。 98 | 99 | ## 翻訳の仕組み 100 | 101 | 1. **英語ロケールファイルの取得**: n8n の公式リポジトリから最新の英語ロケールファイル (`en.json`) を取得します。 102 | 2. **差分翻訳**: ローカルに保存されている前回の英語ロケールファイル ([`script/en.json`](script/en.json:0)) と比較し、新規追加または変更されたテキストのみを抽出します。 103 | 3. **自動翻訳**: 抽出されたテキストを、[`script/translate.js`](script/translate.js:0) スクリプトが Gemini API を利用して日本語に翻訳します。 104 | 4. **翻訳ファイルの保存**: 翻訳結果を既存の日本語翻訳ファイル ([`languages/ja.json`](languages/ja.json:0)) にマージ・更新します。 105 | 5. **UI への適用**: 更新された `ja.json` を含む `editor-ui` パッケージをビルドし、n8n 環境に配置します。環境変数 `N8N_DEFAULT_LOCALE=ja` を設定することで、n8n の UI が日本語で表示されます。 106 | 107 | ## GitHub Actions による自動化 108 | 109 | このリポジトリでは、GitHub Actions ([`.github/workflows/node.js.yml`](.github/workflows/node.js.yml:0)) を利用して、以下の処理を自動化し、[GitHub Releases](https://github.com/nemumusito/n8n-i18n-japanese/releases) に日本語化パッケージを公開しています。 110 | 111 | * **定期的な n8n リリースチェック**: 毎日定時に n8n の GitHub リポジトリを監視し、新しいリリースがないか確認します。 112 | * **自動ビルドとパッケージ化**: 113 | * 新しい n8n のリリースが検出されると、そのバージョンの n8n ソースコードをチェックアウトします。 114 | * [`languages/ja.json`](languages/ja.json:0) を n8n のソースツリー内の適切な位置にコピーします。 115 | * n8n のバージョンに応じて、適切なパッチファイル ([`fix_editor-ui.patch`](fix_editor-ui.patch:0) または [`fix_editor-ui.old.patch`](fix_editor-ui.old.patch:0)) を適用します。これらのパッチは、翻訳表示に関する軽微な不具合を修正します。 116 | * 日本語化された `editor-ui` パッケージをビルドします。 117 | * ビルドされた `dist` ディレクトリを `editor-ui-dist` としてリポジトリにコミットし、さらに `editor-ui.tar.gz` としてアーカイブします。 118 | * `docker-compose.yml` 内の n8n バージョンを、処理対象の n8n バージョンに更新します。 119 | * **GitHub Release の作成**: 120 | * ビルドされた `editor-ui.tar.gz` を添付ファイルとして、新しい GitHub Release を作成します。 121 | * リリースには、対応する n8n のバージョンを示すタグ (例: `n8n@1.86.1`) が付けられます。このタグはリポジトリにもプッシュされます。 122 | 123 | この自動化により、ユーザーは常に最新の n8n バージョンに対応した日本語化パッケージを容易に入手できます。 124 | 125 | ## コントリビューション 126 | 127 | 翻訳の改善や、他の言語パックの追加、機能改善に関するプルリクエストを歓迎します。 128 | 129 | * **他の言語パックの追加**: 130 | 1. [`languages/`](languages/) ディレクトリに `{言語コード}.json` (例: `ko.json`) の形式でファイルを作成します。 131 | 2. [`script/translate.js`](script/translate.js:0) の `targetLanguages` 配列に新しい言語の情報を追加します。 132 | 3. プルリクエストを送信してください。可能であれば、翻訳スクリプトが新しい言語に対応できるように修正を加えてください。 133 | * **不具合報告・改善提案**: Issues にてご報告ください。 134 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: editor-ui 言語ファイルのパッケージ化 5 | 6 | on: 7 | schedule: 8 | - cron: "0 * * * *" 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [22.x] 17 | steps: 18 | - name: Get latest GitHub release version 19 | id: latest_release 20 | run: | 21 | LATEST_VERSION=$(curl -s https://api.github.com/repos/n8n-io/n8n/releases/latest | jq -r '.tag_name') 22 | echo "Latest release tag: $LATEST_VERSION" 23 | echo "latest=$LATEST_VERSION" >> $GITHUB_OUTPUT 24 | 25 | - name: Checkout local 26 | uses: actions/checkout@v4 27 | with: 28 | path: ./n8n-i18n-japanese 29 | 30 | # - name: Read version from package.json 31 | # id: get_version 32 | # run: | 33 | # VERSION=$(jq -r '.version' package.json) 34 | # echo "Version: $VERSION" 35 | # echo "version=$VERSION" >> $GITHUB_OUTPUT 36 | # working-directory: /home/runner/work/n8n-i18n-japanese/n8n-i18n-japanese/n8n 37 | 38 | - name: Check if tag exists 39 | id: check_tag 40 | run: | 41 | TAG="${{ steps.latest_release.outputs.latest }}" 42 | echo "Checking for remote tag: $TAG" 43 | if git ls-remote --tags origin | grep "refs/tags/$TAG$"; then 44 | echo "exists=true" >> $GITHUB_OUTPUT 45 | else 46 | echo "exists=false" >> $GITHUB_OUTPUT 47 | fi 48 | working-directory: /home/runner/work/n8n-i18n-japanese/n8n-i18n-japanese/n8n-i18n-japanese 49 | 50 | - name: Exit if tags exist 51 | if: steps.check_tag.outputs.exists == 'true' 52 | run: | 53 | echo "Tags exist, stopping workflow." 54 | 55 | - name: Checkout n8n 56 | if: steps.check_tag.outputs.exists == 'false' 57 | uses: actions/checkout@v4 58 | with: 59 | repository: n8n-io/n8n 60 | ref: ${{ steps.latest_release.outputs.latest }} 61 | path: ./n8n 62 | 63 | - uses: actions/setup-node@v4.2.0 64 | if: steps.check_tag.outputs.exists == 'false' 65 | with: 66 | node-version: 20.x 67 | 68 | - name: Setup corepack and pnpm 69 | if: steps.check_tag.outputs.exists == 'false' 70 | run: | 71 | npm i -g corepack@0.31 72 | corepack enable 73 | 74 | - run: pnpm install --frozen-lockfile 75 | if: steps.check_tag.outputs.exists == 'false' 76 | working-directory: /home/runner/work/n8n-i18n-japanese/n8n-i18n-japanese/n8n 77 | 78 | - name: check n8n editor-ui is old 79 | id: editorui_dir 80 | run: | 81 | DIR_PATH="/home/runner/work/n8n-i18n-japanese/n8n-i18n-japanese/n8n/packages/frontend/editor-ui" # 確認するディレクトリ 82 | FALLBACK_DIR="/home/runner/work/n8n-i18n-japanese/n8n-i18n-japanese/n8n/packages/editor-ui" # フォールバックディレクトリ 83 | 84 | if [ -d "$DIR_PATH" ]; then 85 | echo "flag=new" >> $GITHUB_OUTPUT 86 | echo "dir=$DIR_PATH" >> $GITHUB_OUTPUT 87 | else 88 | echo "flag=old" >> $GITHUB_OUTPUT 89 | echo "dir=$FALLBACK_DIR" >> $GITHUB_OUTPUT 90 | fi 91 | 92 | - name: Move editor-ui i18n language 93 | if: steps.check_tag.outputs.exists == 'false' 94 | run: cp -r /home/runner/work/n8n-i18n-japanese/n8n-i18n-japanese/n8n-i18n-japanese/languages/* "${{ steps.editorui_dir.outputs.dir }}/src/plugins/i18n/locales/" 95 | 96 | - name: Patch New editor-ui 97 | if: steps.editorui_dir.outputs.flag == 'new' && steps.check_tag.outputs.exists == 'false' 98 | run: git apply /home/runner/work/n8n-i18n-japanese/n8n-i18n-japanese/n8n-i18n-japanese/fix_editor-ui.patch 99 | working-directory: /home/runner/work/n8n-i18n-japanese/n8n-i18n-japanese/n8n 100 | 101 | - name: Patch Old editor-ui 102 | if: steps.editorui_dir.outputs.flag == 'old' && steps.check_tag.outputs.exists == 'false' 103 | run: git apply /home/runner/work/n8n-i18n-japanese/n8n-i18n-japanese/n8n-i18n-japanese/fix_editor-ui.old.patch 104 | working-directory: /home/runner/work/n8n-i18n-japanese/n8n-i18n-japanese/n8n 105 | 106 | - name: Pnpm build editor-ui 107 | if: steps.check_tag.outputs.exists == 'false' 108 | run: pnpm build 109 | working-directory: ${{ steps.editorui_dir.outputs.dir }} 110 | 111 | - name: dist成果物のパッケージ化 112 | if: steps.check_tag.outputs.exists == 'false' 113 | run: | 114 | tar -czvf editor-ui.tar.gz dist 115 | working-directory: ${{ steps.editorui_dir.outputs.dir }} 116 | 117 | - name: dist をプロジェクトディレクトリにコピー 118 | if: steps.check_tag.outputs.exists == 'false' 119 | run: | 120 | cp -r ${{ steps.editorui_dir.outputs.dir }}/dist /home/runner/work/n8n-i18n-japanese/n8n-i18n-japanese/n8n-i18n-japanese/editor-ui-dist 121 | 122 | - name: Update docker-compose.yml version 123 | if: steps.check_tag.outputs.exists == 'false' 124 | run: | 125 | RAW_TAG="${{ steps.latest_release.outputs.latest }}" 126 | VERSION="${RAW_TAG#n8n@}" # プレフィックス n8n@ を削除 127 | echo "Replacing {version} with $VERSION in docker-compose.yml" 128 | sed -i "s/{version}/$VERSION/g" /home/runner/work/n8n-i18n-japanese/n8n-i18n-japanese/n8n-i18n-japanese/docker-compose.yml 129 | 130 | - name: Gitタグの作成 131 | if: steps.check_tag.outputs.exists == 'false' 132 | run: | 133 | TAG="${{ steps.latest_release.outputs.latest }}" 134 | git config --global user.name "github-actions" 135 | git config --global user.email "github-actions@github.com" 136 | git add editor-ui-dist 137 | git add docker-compose.yml 138 | git commit -m "chore: add built editor-ui dist for ${{ steps.latest_release.outputs.latest }}" 139 | git tag $TAG 140 | git push origin $TAG 141 | working-directory: /home/runner/work/n8n-i18n-japanese/n8n-i18n-japanese/n8n-i18n-japanese 142 | 143 | - name: GitHub Release の作成 144 | if: steps.check_tag.outputs.exists == 'false' 145 | id: create_release 146 | uses: softprops/action-gh-release@v2 147 | with: 148 | tag_name: ${{ steps.latest_release.outputs.latest }} 149 | name: Release editor-ui (ja) to ${{ steps.latest_release.outputs.latest }} 150 | body: | 151 | 自動公開される日本語の言語パックバージョンです。 (Automated release of Japanese language pack.) 152 | draft: false 153 | prerelease: false 154 | files: ${{ steps.editorui_dir.outputs.dir }}/editor-ui.tar.gz 155 | env: 156 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 157 | -------------------------------------------------------------------------------- /script/translate.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const fs = require('fs'); 3 | const path = require('path'); // path モジュールをインポート 4 | 5 | // Gemini API Key and Model from environment variables 6 | const GEMINI_API_KEY = process.env.GEMINI_API_KEY; 7 | const GEMINI_MODEL = process.env.GEMINI_MODEL || 'gemini-pro'; // Default to gemini-pro if not set 8 | const GEMINI_API_ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`; 9 | 10 | if (!GEMINI_API_KEY){ 11 | console.error("環境変数 GEMINI_API_KEY を設定してください。"); 12 | process.exit(1); 13 | } 14 | 15 | let targetLanguages; 16 | const defaultTargetLanguages = [ 17 | { 18 | "name": "ja", // Or "ja-JP" if you prefer 19 | "label": "日本語", 20 | } 21 | ]; 22 | 23 | if (process.env.TARGET_LANGUAGES) { 24 | try { 25 | const parsedLanguages = JSON.parse(process.env.TARGET_LANGUAGES); 26 | if (Array.isArray(parsedLanguages) && parsedLanguages.every(lang => lang && typeof lang.name === 'string' && typeof lang.label === 'string')) { 27 | targetLanguages = parsedLanguages; 28 | console.log("環境変数 TARGET_LANGUAGES から翻訳対象言語を読み込みました:", targetLanguages.map(l => l.label).join(', ')); 29 | } else { 30 | console.warn("環境変数 TARGET_LANGUAGES の形式が正しくありません。デフォルトの翻訳対象言語(日本語)を使用します。"); 31 | targetLanguages = defaultTargetLanguages; 32 | } 33 | } catch (e) { 34 | console.error("環境変数 TARGET_LANGUAGES のJSONパースに失敗しました。デフォルトの翻訳対象言語(日本語)を使用します。", e); 35 | targetLanguages = defaultTargetLanguages; 36 | } 37 | } else { 38 | targetLanguages = defaultTargetLanguages; 39 | console.log("環境変数 TARGET_LANGUAGES が未設定です。デフォルトの翻訳対象言語(日本語)を使用します。"); 40 | } 41 | 42 | // 翻訳結果を分割するための区切り文字(API呼び出しごとにユニークにする) 43 | function generateUniqueSeparator() { 44 | return `__TRANSLATION_ITEM_SEPARATOR_${Date.now()}__${Math.random().toString(36).substring(2, 15)}__`; 45 | } 46 | 47 | async function doTranslate(messagesToTranslate, language) { // messagesToTranslate は文字列の配列 48 | if (!messagesToTranslate || messagesToTranslate.length === 0) { 49 | return []; 50 | } 51 | 52 | const uniqueSeparator = generateUniqueSeparator(); 53 | // 各メッセージに番号を付け、区切り文字で結合 54 | const textsToTranslate = messagesToTranslate.map((msg, index) => `ITEM_START [${index + 1}] ${msg} ITEM_END`).join(`\n${uniqueSeparator}\n`); 55 | 56 | const defaultPromptTemplate = `あなたは高度な翻訳APIです。以下の複数の英語のテキストを、それぞれ忠実に ${language} に翻訳してください。 57 | 各英語テキストは「ITEM_START [番号]」で始まり「ITEM_END」で終わります。そして、各英語テキスト間は「${uniqueSeparator}」という特殊な文字列で区切られています。 58 | 59 | あなたのタスクは、各英語テキストを ${language} に翻訳し、翻訳結果のみを返すことです。 60 | 翻訳結果も、必ず原文の順番通りに、「${uniqueSeparator}」という全く同じ文字列で区切って返してください。 61 | 「ITEM_START [番号]」や「ITEM_END」といったマーカーは翻訳結果に含めないでください。 62 | 挨拶、説明、前置き、後書き、その他の追加情報は一切含めず、翻訳されたテキスト群だけを「${uniqueSeparator}」で区切って出力してください。 63 | 64 | 英語テキスト群: 65 | ${textsToTranslate} 66 | 67 | ${language}への翻訳結果群 (各翻訳を「${uniqueSeparator}」で区切ってください):`; 68 | 69 | const userPromptTemplate = process.env.TRANSLATION_PROMPT; 70 | let prompt; 71 | 72 | if (userPromptTemplate) { 73 | prompt = userPromptTemplate 74 | .replace(/\$\{language\}/g, language) 75 | .replace(/\$\{uniqueSeparator\}/g, uniqueSeparator) 76 | .replace(/\$\{textsToTranslate\}/g, textsToTranslate); 77 | } else { 78 | prompt = defaultPromptTemplate; 79 | } 80 | 81 | const response = await fetch(GEMINI_API_ENDPOINT, { 82 | method: "POST", 83 | headers: { 84 | "Content-Type": "application/json" 85 | }, 86 | body: JSON.stringify({ 87 | "contents": [{ 88 | "parts": [{ 89 | "text": prompt 90 | }] 91 | }], 92 | // Optional: Add generationConfig if needed 93 | }), 94 | }); 95 | 96 | if (!response.ok) { 97 | const errorData = await response.json(); 98 | console.error("Gemini API Error:", errorData); 99 | throw new Error(`Gemini API request failed with status ${response.status} for a batch of ${messagesToTranslate.length} items.`); 100 | } 101 | 102 | const data = await response.json(); 103 | 104 | if (data.candidates && data.candidates.length > 0 && data.candidates[0].content && data.candidates[0].content.parts && data.candidates[0].content.parts.length > 0) { 105 | let combinedTranslations = data.candidates[0].content.parts[0].text.trim(); 106 | 107 | let translatedTexts = combinedTranslations.split(uniqueSeparator).map(t => t.trim()); 108 | 109 | // 後処理用のパターン定義 110 | const patternsToRemove = [ 111 | /^(承知しました|はい、分かりました|了解しました|かしこまりました)[、。]?\s*.*?翻訳します[:。\s]*/i, 112 | /^(承知しました|はい、分かりました|了解しました|かしこまりました)[、。]?\s*.*?翻訳結果です[:。\s]*/i, 113 | /^(承知しました|はい、分かりました|了解しました|かしこまりました)[、。]?\s*/i, 114 | /.*?翻訳結果は以下の通りです[:。\s]*/i, 115 | /.*?翻訳は以下の通りです[:。\s]*/i, 116 | /.*?翻訳しました[:。\s]*/i, 117 | /^以下に翻訳します[:。\s]*/i, 118 | /^翻訳結果[:。\s]*/i, 119 | /^(Okay|Sure|Alright|Understood|Got it),?\s*here's the translation:?\s*/i, 120 | /^(Okay|Sure|Alright|Understood|Got it),?\s*I will translate that for you:?\s*/i, 121 | /^(Okay|Sure|Alright|Understood|Got it),?\s*/i, 122 | /^ITEM_START\s*\[\d+\]\s*/i, // ITEM_START [番号] が残っていたら削除 123 | /\s*ITEM_END\s*$/i, // ITEM_END が残っていたら削除 124 | /^\[\d+\]\s*/, // [番号] のみが残っていたら削除 125 | ]; 126 | 127 | const cleanText = (text) => { 128 | let cleanedText = text; 129 | for (const pattern of patternsToRemove) { 130 | cleanedText = cleanedText.replace(pattern, ''); 131 | } 132 | cleanedText = cleanedText.replace(/^["「『]/, '').replace(/["」『]$/, ''); // 引用符削除 133 | return cleanedText.trim(); 134 | }; 135 | 136 | if (translatedTexts.length !== messagesToTranslate.length) { 137 | console.warn(`警告: 分割後の翻訳アイテム数(${translatedTexts.length})が原文アイテム数(${messagesToTranslate.length})と一致しません。APIレスポンス: "${combinedTranslations.substring(0, 500)}..."`); // レスポンスが長い場合があるので一部表示 138 | 139 | // 翻訳結果数が原文数より極端に多い場合(区切り文字が機能しなかった可能性が高い) 140 | if (translatedTexts.length > messagesToTranslate.length * 2 && messagesToTranslate.length > 1) { 141 | console.error(`エラー: 翻訳結果の分割数が原文アイテム数に対して著しく多い(${translatedTexts.length} vs ${messagesToTranslate.length})。APIが区切り文字を正しく解釈しなかった可能性が高いです。このチャンクの翻訳は信頼性が低いため、空として扱います。`); 142 | return new Array(messagesToTranslate.length).fill(''); // 全て空文字で返す 143 | } 144 | 145 | // それ以外の場合(多少のズレ)、原文の数に合わせる 146 | const adjustedTranslations = new Array(messagesToTranslate.length).fill(''); 147 | for(let i=0; i < messagesToTranslate.length; i++) { 148 | if (translatedTexts[i] !== undefined) { 149 | adjustedTranslations[i] = cleanText(translatedTexts[i]); 150 | } 151 | } 152 | return adjustedTranslations; 153 | } 154 | 155 | // アイテム数が一致した場合も、各要素に対してクリーニング処理 156 | return translatedTexts.map(text => cleanText(text)); 157 | 158 | } else { 159 | console.error("バッチ翻訳に対する予期しないGemini APIレスポンス構造:", data); 160 | throw new Error("バッチのGemini APIレスポンスから翻訳を抽出できませんでした。"); 161 | } 162 | } 163 | 164 | function putObjectValue(obj, key, value) { 165 | const keys = key.split('##'); 166 | let current = obj; 167 | 168 | for (let i = 0; i < keys.length - 1; i++) { 169 | const k = keys[i]; 170 | if (!current[k]) { 171 | current[k] = {}; 172 | } 173 | current = current[k]; 174 | } 175 | 176 | current[keys[keys.length - 1]] = value; 177 | } 178 | 179 | const TRANSLATION_CHUNK_SIZE = 50; // 1回のAPI呼び出しで翻訳するアイテム数 180 | const REQUEST_INTERVAL_MS = 6500; // 10 RPM (6秒/リクエスト) + バッファ0.5秒 181 | 182 | async function translate(waitTranslateList, targetObject, targetLanguageLabel) { 183 | let totalTranslatedCount = 0; 184 | const totalItems = waitTranslateList.length; 185 | 186 | console.log(`翻訳開始: 合計 ${totalItems} アイテム, チャンクサイズ: ${TRANSLATION_CHUNK_SIZE}`); 187 | 188 | for (let i = 0; i < totalItems; i += TRANSLATION_CHUNK_SIZE) { 189 | const chunk = waitTranslateList.slice(i, i + TRANSLATION_CHUNK_SIZE); 190 | const messagesInChunk = chunk.map(item => item.message); // 原文メッセージの配列 191 | 192 | if (messagesInChunk.length === 0) continue; 193 | 194 | console.log(`チャンク ${Math.floor(i / TRANSLATION_CHUNK_SIZE) + 1} / ${Math.ceil(totalItems / TRANSLATION_CHUNK_SIZE)} を翻訳中 (${chunk.length} アイテム)...`); 195 | 196 | try { 197 | const translatedMessages = await doTranslate(messagesInChunk, targetLanguageLabel); 198 | 199 | if (translatedMessages.length === chunk.length) { 200 | chunk.forEach((originalItem, index) => { 201 | const translatedText = translatedMessages[index]; 202 | if (translatedText && translatedText.length > 0) { 203 | console.log(`翻訳成功: key "${originalItem.key}" => "${translatedText.substring(0, 50)}..."`); // 長い翻訳は一部表示 204 | putObjectValue(targetObject, originalItem.key, translatedText); 205 | totalTranslatedCount++; 206 | } else { 207 | console.log(`翻訳結果が空またはエラー: key "${originalItem.key}" はスキップされました。`); 208 | } 209 | }); 210 | } else { 211 | // このケースは doTranslate 内で調整されるはずだが、念のため 212 | console.error(`致命的エラー: doTranslate後のアイテム数(${translatedMessages.length})とチャンク内のアイテム数(${chunk.length})が一致しません。このチャンクはスキップされます。`); 213 | chunk.forEach(originalItem => { 214 | console.log(`翻訳失敗 (致命的エラーのためスキップ): key "${originalItem.key}"`); 215 | }); 216 | } 217 | } catch (e) { 218 | console.error(`チャンク翻訳中にエラーが発生しました: ${e.message}`); 219 | console.error(`エラーが発生したチャンクのキー (最初の5件):`); 220 | chunk.slice(0, 5).forEach(originalItem => { 221 | console.error(` - ${originalItem.key}`); 222 | }); 223 | } 224 | 225 | console.log(`処理済みアイテム: ${Math.min(i + TRANSLATION_CHUNK_SIZE, totalItems)} / ${totalItems}`); 226 | console.log(`現在までの成功翻訳数: ${totalTranslatedCount}`); 227 | 228 | if (i + TRANSLATION_CHUNK_SIZE < totalItems) { 229 | console.log(`${REQUEST_INTERVAL_MS / 1000}秒待機します...`); 230 | await new Promise(resolve => setTimeout(resolve, REQUEST_INTERVAL_MS)); 231 | } 232 | } 233 | console.log(`翻訳完了。合計 ${totalTranslatedCount} / ${totalItems} アイテムが翻訳されました。`); 234 | } 235 | 236 | // 翻訳が必要なキーとメッセージを収集する 237 | function collectMessages(oldSourceLanguages, newSourceLanguages, targetLanguages, parentKey = '', waitTranslateList=[]){ 238 | for (const key in newSourceLanguages) { 239 | let currentKey = parentKey ? parentKey + "##" + key : key; 240 | if (newSourceLanguages[key] instanceof Object) { 241 | collectMessages(oldSourceLanguages[key] || {}, newSourceLanguages[key], targetLanguages[key] || {}, currentKey, waitTranslateList); 242 | } else { 243 | if (targetLanguages[key] === undefined 244 | || oldSourceLanguages[key] === undefined 245 | || oldSourceLanguages[key] !== newSourceLanguages[key]) { 246 | waitTranslateList.push({ 247 | key: currentKey, 248 | message: newSourceLanguages[key] 249 | }) 250 | } 251 | } 252 | } 253 | } 254 | 255 | 256 | async function run(){ 257 | const localEnFilePath = path.join(__dirname, "en.json"); // スクリプトと同じディレクトリの en.json 258 | let oldEnLanguages = {}; 259 | if (fs.existsSync(localEnFilePath)) { 260 | try { 261 | oldEnLanguages = require(localEnFilePath); // ローカルの古い英語言語ファイル 262 | } catch (e) { 263 | console.warn(`${localEnFilePath} の読み込みに失敗しました。空のオブジェクトとして扱います。エラー:`, e); 264 | } 265 | } else { 266 | console.warn(`${localEnFilePath} が見つかりません。空のオブジェクトとして扱います。`); 267 | } 268 | 269 | const newEnLanguages = await fetch("https://ghfast.top/https://raw.githubusercontent.com/n8n-io/n8n/master/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json") // 最新の英語言語ファイルを取得 270 | .then(res => res.json()) 271 | .catch(err => { 272 | console.error("最新の英語言語ファイルの取得に失敗しました:", err); 273 | console.log("ローカルの en.json を使用して続行します。"); 274 | return oldEnLanguages; // フォールバックとしてローカルファイルを使用 (読み込めていれば) 275 | }); 276 | 277 | for (const targetLanguage of targetLanguages) { 278 | let targetJson = {}; // ターゲット言語のJSONオブジェクト 279 | // 翻訳ファイルのパスを __dirname (scriptディレクトリ) からの相対パスで解決 280 | let fileName = path.join(__dirname, '..', 'languages', `${targetLanguage.name}.json`); 281 | 282 | if (fs.existsSync(fileName)){ 283 | try { 284 | targetJson = JSON.parse(fs.readFileSync(fileName, "utf8")); // 既存の翻訳ファイルを読み込み 285 | } catch (e) { 286 | console.error(`${fileName} の読み込みまたは解析に失敗しました。空のオブジェクトとして扱います。エラー:`, e); 287 | targetJson = {}; // エラー時は空のオブジェクト 288 | } 289 | } 290 | const waitTranslateList = [] // 翻訳待ちリスト 291 | collectMessages(oldEnLanguages, newEnLanguages , targetJson, "", waitTranslateList); // 翻訳が必要なアイテムを収集 292 | 293 | if (waitTranslateList.length === 0) { 294 | console.log(`${targetLanguage.label} の翻訳対象アイテムはありませんでした。既存の翻訳ファイルを保持します。`); 295 | // 既存のファイルをそのまま保存し直すか、何もしないか。ここでは何もしない。 296 | // もし、キーのソートだけは行いたい場合は、以下のソート処理を有効化する。 297 | /* 298 | const sortedTargetLanguages = {}; 299 | for (const key in newEnLanguages) { 300 | if (targetJson[key] !== undefined) { 301 | sortedTargetLanguages[key] = targetJson[key]; 302 | } 303 | } 304 | fs.writeFileSync(fileName, JSON.stringify(sortedTargetLanguages, null, 4)); 305 | */ 306 | continue; // 次の言語へ 307 | } 308 | 309 | await translate(waitTranslateList, targetJson, targetLanguage.label); // 翻訳処理を実行 310 | 311 | // newEnLanguages のキー順序に基づいて targetJson をソートする 312 | const sortedTargetLanguages = {}; 313 | for (const key in newEnLanguages) { // 最新の英語ファイルのキー順でソート 314 | if (targetJson[key] !== undefined) { 315 | sortedTargetLanguages[key] = targetJson[key]; 316 | } else if (oldEnLanguages[key] !== undefined && targetJson[key] === undefined) { 317 | // 翻訳されなかったが、元々古い翻訳があった場合はそれを引き継ぐか検討。 318 | // ここでは、翻訳されなかったものは含めない方針。 319 | } 320 | } 321 | // 翻訳後の言語ファイルを書き出す 322 | fs.writeFileSync(fileName, JSON.stringify(sortedTargetLanguages, null, 4)); 323 | console.log(`${fileName} にソート済みの翻訳を書き込みました。`); 324 | } 325 | // ローカルの英語言語ファイルを最新版に更新 326 | fs.writeFileSync(localEnFilePath, JSON.stringify(newEnLanguages, null, 4)); 327 | console.log(`${localEnFilePath} を最新版に更新しました。`); 328 | } 329 | 330 | run(); 331 | --------------------------------------------------------------------------------