Skip to content

Commit

Permalink
Merge pull request #314 from tegnike/develop
Browse files Browse the repository at this point in the history
本番リリース
  • Loading branch information
tegnike authored Mar 2, 2025
2 parents c7a57e5 + 925e570 commit 319ce8b
Show file tree
Hide file tree
Showing 4 changed files with 392 additions and 1 deletion.
File renamed without changes.
52 changes: 52 additions & 0 deletions .github/workflows/locale-updater.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Locale Files Updater

on:
pull_request:
types: [opened, synchronize, reopened]
branches:
- develop
paths:
- 'locales/ja/translation.json'
workflow_dispatch:

jobs:
update-locales:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0 # 履歴を全て取得して差分比較に使用
ref: ${{ github.event.pull_request.head.ref }} # PRのブランチをチェックアウト

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install openai GitPython
- name: Update locale files
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: python scripts/update_locales.py

- name: Commit and push changes
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add locales/*/translation.json
if git diff --staged --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "update locale files"
git push origin ${{ github.event.pull_request.head.ref }}
339 changes: 339 additions & 0 deletions scripts/update_locales.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import json
import git
import sys
import time
from pathlib import Path
from openai import OpenAI

# 環境変数からAPIキーを取得
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
if not OPENAI_API_KEY:
print("Error: OPENAI_API_KEY environment variable is not set.")
sys.exit(1)

# OpenAI APIの初期化
client = OpenAI(api_key=OPENAI_API_KEY)

# リポジトリのルートディレクトリを取得
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
locales_dir = os.path.join(repo_root, "locales")
ja_translation_path = os.path.join(locales_dir, "ja", "translation.json")

# サポートしているロケール
SUPPORTED_LOCALES = [
"ar",
"de",
"en",
"es",
"fr",
"hi",
"it",
"ko",
"pl",
"pt",
"ru",
"th",
"vi",
"zh",
]

# Gitリポジトリの初期化
repo = git.Repo(repo_root)


def get_changed_keys():
"""
日本語のtranslation.jsonファイルの変更を検出し、変更されたキーを返す
"""
# 追加・変更されたキーと削除されたキーを格納する辞書
added_modified_keys = {}
deleted_keys = []

try:
# 現在のファイル内容を取得
with open(ja_translation_path, "r", encoding="utf-8") as f:
current_json = json.load(f)

# GitHubのPR環境変数からベースブランチのSHAを取得
base_sha = os.getenv("GITHUB_BASE_SHA")
if not base_sha:
print(
"Warning: GITHUB_BASE_SHA not found. Using current branch comparison."
)
# PRのベース(通常はdevelop)との差分を取得
base_sha = repo.git.merge_base("HEAD", "origin/develop")

try:
# ベースブランチの日本語ファイルの内容を取得
old_content = repo.git.show(
f"{base_sha}:{ja_translation_path.replace(repo_root + '/', '')}"
)
old_json = json.loads(old_content)
except git.exc.GitCommandError:
# ファイルが存在しない場合(新規作成時)は空の辞書を使用
print("Base version of the file not found. Treating as new file.")
old_json = {}

# 階層的に変更を抽出する関数
def extract_changes(old_dict, new_dict, path=""):
# 新規追加または変更されたキー
for key in new_dict:
current_path = f"{path}.{key}" if path else key

# キーが存在しない場合は追加
if key not in old_dict:
added_modified_keys[current_path] = new_dict[key]
# 両方が辞書の場合は再帰的に処理
elif isinstance(old_dict[key], dict) and isinstance(
new_dict[key], dict
):
extract_changes(old_dict[key], new_dict[key], current_path)
# 値が変更された場合
elif old_dict[key] != new_dict[key]:
added_modified_keys[current_path] = new_dict[key]

# 削除されたキー
for key in old_dict:
current_path = f"{path}.{key}" if path else key

if key not in new_dict:
deleted_keys.append(current_path)
elif isinstance(old_dict[key], dict) and isinstance(
new_dict[key], dict
):
# 両方が辞書の場合は再帰的に処理
extract_changes(old_dict[key], new_dict[key], current_path)

# 変更を抽出
extract_changes(old_json, current_json)

return added_modified_keys, deleted_keys

except Exception as e:
print(f"Error detecting changes: {e}")
return {}, []


def create_json_from_keys(keys_dict):
"""
キーのパスと値の辞書からネストされたJSONオブジェクトを作成
"""
result = {}

for key_path, value in keys_dict.items():
keys = key_path.split(".")
current = result

# 最後のキー以外をたどってネストされた辞書を作成
for i, k in enumerate(keys[:-1]):
if k not in current:
current[k] = {}
current = current[k]

# 最後のキーに値を設定
current[keys[-1]] = value

return result


def translate_json(json_obj, target_lang, max_retries=3):
"""
JSON構造を保持したまま翻訳する
最大3回まで再試行する
"""
# JSON文字列に変換
json_str = json.dumps(json_obj, ensure_ascii=False, indent=2)

retry_count = 0
while retry_count < max_retries:
try:
# OpenAI APIを使用して翻訳
response = client.chat.completions.create(
model="gpt-4o-mini", # 指定されたモデル
messages=[
{
"role": "system",
"content": f"You are a professional translator. Translate the following JSON content from Japanese to {target_lang}. Keep all keys the same, only translate the values. Maintain the exact same JSON structure.",
},
{
"role": "user",
"content": f"Translate this JSON content to {target_lang}. Keep all keys the same, only translate string values:\n\n```json\n{json_str}\n```",
},
],
temperature=0.1, # 低い温度で一貫性のある翻訳を生成
)

translation_text = response.choices[0].message.content

# JSON文字列から辞書に変換
# translation_textからJSONオブジェクトを抽出する正規表現を使用
import re

json_match = re.search(r"```json\s*([\s\S]*?)\s*```", translation_text)
if json_match:
translation_text = json_match.group(1)

try:
translated_json = json.loads(translation_text)
return translated_json
except json.JSONDecodeError:
print(
f"Error: Failed to parse translated JSON for {target_lang}. Retry {retry_count + 1}/{max_retries}"
)
print(f"Translated content: {translation_text}")
retry_count += 1
time.sleep(1) # 1秒待機してから再試行

except Exception as e:
print(
f"Error translating to {target_lang}: {e}. Retry {retry_count + 1}/{max_retries}"
)
retry_count += 1
time.sleep(1) # 1秒待機してから再試行

print(f"Failed to translate to {target_lang} after {max_retries} attempts.")
return None


def update_locale_file(locale_path, added_modified_json, deleted_keys):
"""
ローカルファイルを更新: 追加・変更されたキーを適用し、削除されたキーを削除
"""
try:
# ローカルファイルを読み込み(存在しない場合は空の辞書)
locale_json = {}
if os.path.exists(locale_path):
with open(locale_path, "r", encoding="utf-8") as f:
locale_json = json.load(f)

# 階層的にキーを追加・更新する関数
def update_nested_dict(target, source):
for k, v in source.items():
if isinstance(v, dict):
# キーが存在しない場合は作成
if k not in target:
target[k] = {}
# 再帰的に処理
update_nested_dict(target[k], v)
else:
# 値を更新
target[k] = v

# 追加・変更されたキーを適用
update_nested_dict(locale_json, added_modified_json)

# 削除されたキーを処理
for key_path in deleted_keys:
keys = key_path.split(".")
current = locale_json
parent_chain = []

# 最後のキー以外をたどる
for i, k in enumerate(keys[:-1]):
if k not in current:
# パスが存在しない場合はスキップ
break
parent_chain.append((current, k))
current = current[k]

# 最後のキーを削除
if keys[-1] in current:
del current[keys[-1]]

# 空の親オブジェクトを削除
for parent, key in reversed(parent_chain):
if parent[key] == {}:
del parent[key]

# 更新されたJSONをファイルに書き込み
os.makedirs(os.path.dirname(locale_path), exist_ok=True)
with open(locale_path, "w", encoding="utf-8") as f:
json.dump(locale_json, f, ensure_ascii=False, indent=2)

return True

except Exception as e:
print(f"Error updating locale file {locale_path}: {e}")
return False


def get_language_name(locale_code):
"""
ロケールコードから言語名を取得
"""
language_map = {
"ar": "Arabic",
"de": "German",
"en": "English",
"es": "Spanish",
"fr": "French",
"hi": "Hindi",
"it": "Italian",
"ko": "Korean",
"pl": "Polish",
"pt": "Portuguese",
"ru": "Russian",
"th": "Thai",
"vi": "Vietnamese",
"zh": "Chinese",
}
return language_map.get(locale_code, f"Language code {locale_code}")


def main():
print("Starting locale files update process...")

# 変更されたキーを取得
added_modified_keys, deleted_keys = get_changed_keys()

if not added_modified_keys and not deleted_keys:
print("No changes to process. Exiting.")
return

print(
f"Found {len(added_modified_keys)} added/modified keys and {len(deleted_keys)} deleted keys."
)

# 変更されたキーからJSONオブジェクトを作成
added_modified_json = create_json_from_keys(added_modified_keys)

# 各ロケールを処理
for locale in SUPPORTED_LOCALES:
if locale == "ja": # 日本語は処理しない
continue

locale_path = os.path.join(locales_dir, locale, "translation.json")
print(f"Processing locale: {locale}")

# 追加・変更されたキーがある場合のみ翻訳処理
if added_modified_keys:
# 言語名を取得
language_name = get_language_name(locale)

# 翻訳を実行
translated_json = translate_json(added_modified_json, language_name)

if translated_json:
# ローカルファイルを更新
success = update_locale_file(locale_path, translated_json, deleted_keys)
if success:
print(f"Successfully updated locale file for {locale}.")
else:
print(f"Failed to update locale file for {locale}.")
else:
print(f"Failed to translate content for {locale}. Skipping update.")
elif deleted_keys: # 削除されたキーのみの処理
# 削除されたキーのみ適用
success = update_locale_file(locale_path, {}, deleted_keys)
if success:
print(f"Successfully deleted keys from locale file for {locale}.")
else:
print(f"Failed to delete keys from locale file for {locale}.")


if __name__ == "__main__":
main()
Loading

0 comments on commit 319ce8b

Please sign in to comment.