diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 6a3cd83ae6d..63534427cd9 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -3,17 +3,26 @@ on: pull_request: branches: - master + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: flutter: name: Flutter -- verify, test runs-on: macOS-latest + # Allow push so Generate locales can commit and push ARB changes to the PR branch. + permissions: + contents: write steps: - name: Checkout main repo id: checkout-main-repo uses: actions/checkout@v3 with: path: main-repo + fetch-depth: 0 - name: Checkout the target 'master' branch of 'flutter-app-secrets' id: checkout-secrets uses: actions/checkout@v3 @@ -69,12 +78,62 @@ jobs: run: | cd main-repo melos run 4mat + - name: Set l10n baseline ref (last successful run or origin/base) + id: l10n-baseline + if: github.event_name == 'pull_request' + run: | + # For PRs: use last successful CI run's commit as baseline when available; else origin/base_ref. + BRANCH="${{ github.head_ref }}" + RUN_ID="${{ github.run_id }}" + BASE_DEFAULT="origin/${{ github.base_ref }}" + if [ -z "$BRANCH" ]; then + echo "base_ref=$BASE_DEFAULT" >> $GITHUB_OUTPUT + exit 0 + fi + LAST_SHA=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/actions/workflows/CI.yaml/runs?branch=${BRANCH}&status=success&per_page=10" \ + | jq -r --arg run_id "$RUN_ID" '.workflow_runs[] | select(.id != ($run_id | tonumber)) | .head_sha' | head -1) + if [ -n "$LAST_SHA" ] && [ "$LAST_SHA" != "null" ]; then + echo "base_ref=$LAST_SHA" >> $GITHUB_OUTPUT + else + echo "base_ref=$BASE_DEFAULT" >> $GITHUB_OUTPUT + fi + - name: Checkout PR branch (for locale commit/push) + if: github.event_name == 'pull_request' + run: | + cd main-repo + git fetch origin ${{ github.head_ref }} + git checkout ${{ github.head_ref }} + - name: Set git committer from PR HEAD + if: github.event_name == 'pull_request' + run: | + cd main-repo + git config --local user.name "$(git log -1 --format='%cn')" + git config --local user.email "$(git log -1 --format='%ce')" - name: Generate locales id: generate-locales + env: + BASE_REF: ${{ steps.l10n-baseline.outputs.base_ref }} + run: | + cd main-repo + if [ "${{ github.event_name }}" = "pull_request" ]; then + bash ./scripts/auto_translate_locales.sh --commit + else + bash ./scripts/auto_translate_locales.sh + fi + - name: Re-trigger CI for new commit (so status is reported on pushed commit) + if: github.event_name == 'pull_request' run: | cd main-repo - # Run locale generation with verification - ./scripts/generate_locales.sh + HEAD_SHA=$(git rev-parse HEAD) + if [ "$HEAD_SHA" != "${{ github.sha }}" ]; then + echo "Locales were pushed; triggering CI for new commit so status is reported on HEAD." + curl -s -X POST \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/${{ github.repository }}/actions/workflows/CI.yaml/dispatches" \ + -d "{\"ref\":\"refs/heads/${{ github.head_ref }}\"}" + fi - name: Generate code id: generate-code run: | diff --git a/docs/auto-translate-missing-strings-flow.md b/docs/auto-translate-missing-strings-flow.md new file mode 100644 index 00000000000..0474ae5d7f4 --- /dev/null +++ b/docs/auto-translate-missing-strings-flow.md @@ -0,0 +1,68 @@ +# Auto-translate l10n strings (missing + changed English) + +This repo auto-fills ARB translations using a Dart script + a small shell wrapper. + +## Files + +- **Source locale**: `lib/l10n/app_en.arb` +- **Target locales**: `lib/l10n/app_.arb` (all non-`en` locales present) +- **Missing-keys input**: `untranslated_messages.txt` (generated by `flutter gen-l10n`, JSON map `{"it":["key1",...], ...}`) +- **Translator**: `tools/translate_missing.dart` +- **Wrapper** (shared by CI + pre-push): `scripts/auto_translate_locales.sh` +- **Verifier**: `scripts/generate_locales.sh` + +## What gets translated + +`tools/translate_missing.dart` translates the union of: + +- **Missing keys** per locale (from `untranslated_messages.txt`) +- **Keys whose English changed** in `app_en.arb` vs a base Git ref (`BASE_REF` / `--base-ref`) + +### Overwrite rules + +- **Modified English keys**: re-translated for all locales (overwrites existing values). +- **Newly added English keys**: translated **only if** that locale does not already contain the key (so manually-added translations in the same PR are preserved). + +## Common flow (CI + pre-push) + +The shared wrapper is `scripts/auto_translate_locales.sh`: + +1. Run `flutter gen-l10n` (creates/refreshes `untranslated_messages.txt`) +2. Decide if translation is needed: + - `untranslated_messages.txt` is non-empty, **or** + - `lib/l10n/app_en.arb` differs vs `BASE_REF` (or auto base ref) +3. Run `dart run tools/translate_missing.dart` (optionally `--commit`) +4. Re-run `flutter gen-l10n` +5. Run `./scripts/generate_locales.sh` to verify completeness + +## Usage + +### Local (pre-push) + +`scripts/pre_push.sh` calls: + +- `./scripts/auto_translate_locales.sh --fail-if-generated --base-ref=auto` + +Meaning: + +- If translations were generated, the push is blocked so you can **review + commit** the ARB changes. + +### CI (PRs) + +CI runs (with `BASE_REF` set to last successful run’s commit or origin/base): + +- `./scripts/auto_translate_locales.sh --commit` + +Meaning: + +- Missing and/or changed-English translations are generated, committed, and pushed **only when allowed by `tools/translate_missing.dart` branch guards**. + +When the job pushes a commit, GitHub does not start a new run for that commit, so the PR can show “Expected — Waiting for status” on the latest commit. After Generate locales, the workflow **re-triggers itself** via `workflow_dispatch` on the same branch when it pushed. That second run (verify-only, no `--commit`) reports status to the new commit so the required “Flutter -- verify, test” check passes. + +## Configuration + +- **OpenAI key**: `OPENAI_API_KEY` (required for actual translation; `--dry-run` does not require it) +- **Base ref for “changed English” detection**: + - env: `BASE_REF` (preferred in CI), or + - wrapper: `--base-ref=` / `--base-ref=auto` + diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 4b974f0e8ef..d3654ea7ae3 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -1,13 +1,15 @@ { "@@locale": "ar", + "add_new_key_for_auto_translate": "نص جديد، يجب ترجمته تلقائيًا", + "add_new_key_for_auto_translate2": "نص جديد، يجب ترجمته تلقائيًا 2", "all_networks_item": "كل الشبكات", "app_language_description": "سيتم عرض التطبيق باللغة المختارة", - "app_language_title": "لغة التطبيق", - "article_menu_report_article": "الإبلاغ عن المقالة", + "app_language_title": "تحديث لغة التطبيق 1", + "article_menu_report_article": "تقرير تحديث المقال 2", "article_page_from_author": "من {name}", "article_page_in_topic": "في {name}", - "article_preview_title": "معاينة المقالة", - "auth_identity_io": "Identity.io", + "article_preview_title": "تحديث دفع معاينة المقالة 3", + "auth_identity_io": "تحديث Identity.io 4 دفع متزامن", "auth_link_new_device_description": "لمتابعة استخدام التطبيق، يرجى ربط هذا الجهاز بحسابك", "auth_link_new_device_link_button": "ربط الجهاز", "auth_link_new_device_title": "تسجيل الدخول من جهاز جديد", @@ -58,10 +60,10 @@ "bsc_required_dialog_action_button": "ابدأ", "bsc_required_dialog_description": "حول محتواك إلى قيمة حقيقية من خلال المجتمعات المميزة بالرموز المميزة وإيرادات الإعلانات والمقايضات. متاح لجميع المستخدمين.", "bsc_required_dialog_title": "تحقيق الربح من المحتوى متاح الآن", - "button_add": "إضافة", - "button_apply": "تطبيق", - "button_approve": "موافقة", - "button_approve_all": "موافقة على الكل", + "button_add": "أضف تغيير 5", + "button_apply": "تطبيق التغيير 6", + "button_approve": "وافق على تغيير 7 دفع", + "button_approve_all": "وافق على جميع التغييرات 8 متزامنة", "button_back": "رجوع", "button_back_to_security": "العودة إلى الأمان", "button_block": "حظر", @@ -204,7 +206,6 @@ "chat_money_sent_title": "أرسلت", "chat_new_message_button": "رسالة جديدة", "chat_privacy_cant_send_message": "لا يمكنك إرسال رسالة لهذا المستخدم بسبب إعدادات الخصوصية الخاصة به.", - "chat_profile_share_button": "اكتب رسالة", "chat_profile_share_modal_title": "شارك الملف الشخصي", "chat_profile_view_button": "عرض الملف الشخصي", "chat_read": "مقروء", @@ -292,10 +293,6 @@ "contact_wallet_is_private_title": "محفظة خاصة", "contact_wallet_not_found_description": "لا يملك محفظة في شبكة {network}", "contact_wallet_not_found_title": "لم يتم العثور على المحفظة", - "contacts_allow_pop_up_action": "السماح بالوصول إلى جهات الاتصال", - "contacts_allow_pop_up_desc": "زامن جهات الاتصال الخاصة بك، شاهد من هم موجودون بالفعل على Ice، وأرسل واستقبل مدفوعات Ice من أي من جهات اتصالك.", - "contacts_allow_pop_up_title": "Ice أفضل مع الأصدقاء", - "contacts_select_title": "اختر جهة اتصال", "content_language_description": "سيتم عرض المحتوى باللغة المختارة", "content_language_title": "لغات المحتوى", "core_all": "الكل", @@ -318,7 +315,6 @@ "create_post_external_content": "إنشاء منشور", "create_post_modal_placeholder": "ما الذي يحدث؟", "create_post_modal_title": "منشور جديد", - "create_video_edit_cover": "تعديل الغلاف", "create_video_input_placeholder": "أضف وصفاً (اختياري)", "create_video_new_video": "فيديو جديد", "creator_token_is_live": "توكن المنشئ نشط", @@ -581,8 +577,6 @@ "identity_key_name_description": "اعتبر اسم مفتاح هويتك كمعرف فريد لحسابك. ستحتاجه لتسجيل الدخول واستعادة حسابك، لذا احفظه جيداً ولا تنساه", "settings_identity_key_name_description": "اسم مفتاح الهوية هو معرف فريد لحسابك ومطلوب لتسجيل الدخول واستعادة الحساب", "identity_key_name_usage": "استخدمه لتسجيل الدخول على أي تطبيق مؤمن بواسطة", - "identity_not_found_desc": "لم نجد هذا المفتاح في قاعدة بياناتنا. تحقق من صحة البيانات المدخلة", - "identity_not_found_title": "اسم مفتاح الهوية غير موجود", "invite_friends_button": "ادعُ صديقًا", "invite_friends_earnings_title": "الأرباح", "invite_friends_page_subtitle": "لكل شخص تدعوه، ستحصل على نسبة من الإيرادات التي يحققها", @@ -610,8 +604,6 @@ "nickname_not_owned_suffix": "(! غير مملوك)", "no_connection_page_description": "يرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى", "no_connection_page_title": "لا يوجد اتصال", - "no_local_passkey_creds_desc": "تحقق من ملكية هذا الحساب بمسح رمز الاستجابة السريعة في الخطوة التالية باستخدام الجهاز الذي تم حفظ مفتاحك عليه.", - "no_local_passkey_creds_title": "يتطلب التحقق من مفتاح الهوية", "notification_article_loading": "تحميل المقالة...", "notification_article_published": "نشر المقالة", "notification_delete_loading": "حذف...", @@ -747,8 +739,6 @@ "profile_articles_empty_list": "{username} ليس لديه أي مقالات", "profile_articles_empty_list_current_user": "ليس لديك أي مقالات", "profile_bio": "نبذة", - "profile_bookmarks": "الإشارات المرجعية", - "profile_bookmarks_desc": "منشوراتك المحفوظة", "profile_create_new_account": "إضافة حساب", "profile_log_in_existing_account": "تسجيل الدخول إلى حساب موجود", "profile_create_a_new_account": "إنشاء حساب جديد", @@ -911,7 +901,6 @@ "sign_up_passkey_advantage_2_title": "يعمل على جميع أجهزتك", "sign_up_passkey_advantage_3_description": "توفر مفاتيح المرور حماية متقدمة ضد التصيد الاحتيالي", "sign_up_passkey_advantage_3_title": "حماية أقوى للحساب", - "sign_up_passkey_identity_key_name_taken": "اسم مفتاح الهوية محجوز", "sign_up_passkey_title": "التسجيل باستخدام مفتاح المرور", "sign_up_password_description": "اختر كلمة مرور قوية لإنشاء حساب", "sign_up_password_title": "تسجيل", @@ -1210,8 +1199,8 @@ "who_can_reply_settings_title_post": "من يمكنه الرد على هذا المنشور", "who_can_reply_settings_title_video": "من يمكنه الرد على هذا الفيديو", "who_can_reply_settings_verified_accounts": "الحسابات المُوثقة", - "write_a_message": "اكتب رسالة...", "token_unable_to_fetch_market_data_title": "تعذّر جلب بيانات السوق", "token_unable_to_fetch_market_data_description": "نقوم بجلب معلومات السوق لهذا الرمز، يرجى المحاولة مرة أخرى لاحقاً", + "write_a_message": "اكتب رسالة...", "your_position_card_title": "أيتمكش" } diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 4346ddadd02..e8d7f7bfb7e 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1,13 +1,15 @@ { "@@locale": "bg", + "add_new_key_for_auto_translate": "Нов текст, който трябва да бъде автоматично преведен", + "add_new_key_for_auto_translate2": "Нов текст, който трябва да бъде автоматично преведен 2", "all_networks_item": "Всички мрежи", "app_language_description": "Приложението ще се показва на избрания език", - "app_language_title": "Език на приложението", - "article_menu_report_article": "Докладване на статия", + "app_language_title": "Актуализация на езика на приложението 1", + "article_menu_report_article": "Докладвай актуализация на статия 2", "article_page_from_author": "от {name}", "article_page_in_topic": "в {name}", - "article_preview_title": "Преглед на статията", - "auth_identity_io": "Identity.io", + "article_preview_title": "Актуализация на предварителния преглед на статия 3 пусни", + "auth_identity_io": "Identity.io актуализация 4 едновременни известия", "auth_link_new_device_description": "За да продължите да използвате приложението, моля свържете това устройство с вашия акаунт", "auth_link_new_device_link_button": "Свързване на устройство", "auth_link_new_device_title": "Вход от ново устройство", @@ -58,10 +60,10 @@ "bsc_required_dialog_action_button": "Започнете", "bsc_required_dialog_description": "Превърнете съдържанието си в реална стойност чрез токенизирани общности, приходи от реклами и суапове. Отворено за всички потребители.", "bsc_required_dialog_title": "Монетизацията на създателите е НА ЖИВО", - "button_add": "Добави", - "button_apply": "Приложи", - "button_approve": "Одобри", - "button_approve_all": "Одобри всички", + "button_add": "Добави промяна 5", + "button_apply": "Приложи промяна 6", + "button_approve": "Одобрете промяна 7 push", + "button_approve_all": "Одобрете всички промени 8 едновременни", "button_back": "Назад", "button_back_to_security": "Обратно към сигурността", "button_block": "Блокирай", @@ -1197,8 +1199,8 @@ "who_can_reply_settings_title_post": "Кой може да отговаря на тази публикация", "who_can_reply_settings_title_video": "Кой може да отговаря на това видео", "who_can_reply_settings_verified_accounts": "Верифицирани акаунти", - "write_a_message": "Напишете съобщение...", "token_unable_to_fetch_market_data_title": "Не могат да се извлекат пазарни данни", "token_unable_to_fetch_market_data_description": "Извличаме пазарна информация за този токен, моля опитайте отново по-късно", + "write_a_message": "Напишете съобщение...", "your_position_card_title": "Вашата позиция" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 33762720998..e95d9212d38 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1,13 +1,15 @@ { "@@locale": "de", + "add_new_key_for_auto_translate": "Neuer Text, der automatisch übersetzt werden sollte", + "add_new_key_for_auto_translate2": "Neuer Text, der automatisch übersetzt werden sollte 2", "all_networks_item": "Alle Netzwerke", "app_language_description": "Die App wird dir in der ausgewählten Sprache angezeigt", - "app_language_title": "App-Sprache", - "article_menu_report_article": "Artikel melden", + "app_language_title": "App-Sprachaktualisierung 1", + "article_menu_report_article": "Artikelaktualisierung 2 melden", "article_page_from_author": "von {name}", "article_page_in_topic": "in {name}", - "article_preview_title": "Artikelvorschau", - "auth_identity_io": "Identity.io", + "article_preview_title": "Artikelvorschau-Update 3 Push", + "auth_identity_io": "Identity.io Update 4 gleichzeitige Push", "auth_link_new_device_description": "Um die App weiter zu verwenden, verknüpfe dieses Gerät mit deinem Konto", "auth_link_new_device_link_button": "Gerät verknüpfen", "auth_link_new_device_title": "Anmeldung auf neuem Gerät", @@ -58,10 +60,10 @@ "bsc_required_dialog_action_button": "Loslegen", "bsc_required_dialog_description": "Verwandeln Sie Ihre Inhalte in echten Wert durch tokenisierte Communities, Werbeeinnahmen und Swaps. Offen für alle Nutzer.", "bsc_required_dialog_title": "Creator Monetarisierung ist LIVE", - "button_add": "Hinzufügen", - "button_apply": "Anwenden", - "button_approve": "Genehmigen", - "button_approve_all": "Alle genehmigen", + "button_add": "Änderung 5 hinzufügen", + "button_apply": "Änderung 6 anwenden", + "button_approve": "Änderung 7 Push genehmigen", + "button_approve_all": "Alle Änderungen 8 gleichzeitig genehmigen", "button_back": "Zurück", "button_back_to_security": "Zurück zu Sicherheit", "button_block": "Blockieren", @@ -1197,8 +1199,8 @@ "who_can_reply_settings_title_post": "Wer kann auf diesen Beitrag antworten", "who_can_reply_settings_title_video": "Wer kann auf dieses Video antworten", "who_can_reply_settings_verified_accounts": "Verifizierte Konten", - "write_a_message": "Nachricht schreiben …", "token_unable_to_fetch_market_data_title": "Marktdaten konnten nicht abgerufen werden", "token_unable_to_fetch_market_data_description": "Wir rufen Marktinformationen für diesen Token ab, bitte versuche es später erneut", + "write_a_message": "Nachricht schreiben …", "your_position_card_title": "Deine Position" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 31ef2708a57..1927b6c8806 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,13 +1,15 @@ { "@@locale": "en", + "add_new_key_for_auto_translate": "New text, which should be automatically translated", + "add_new_key_for_auto_translate2": "New text, which should be automatically translated 2", "all_networks_item": "All networks", "app_language_description": "You'll be shown the app in the selected language", - "app_language_title": "App language", - "article_menu_report_article": "Report article", + "app_language_title": "App language update 1", + "article_menu_report_article": "Report article update 2", "article_page_from_author": "from {name}", "article_page_in_topic": "in {name}", - "article_preview_title": "Article preview", - "auth_identity_io": "Identity.io", + "article_preview_title": "Article preview update 3 push", + "auth_identity_io": "Identity.io update 4 concurrent push", "auth_link_new_device_description": "To continue using the app, please link this device to your account", "auth_link_new_device_link_button": "Link device", "auth_link_new_device_title": "New device login", @@ -58,10 +60,10 @@ "bsc_required_dialog_action_button": "Get started", "bsc_required_dialog_description": "Turn your content into real value through tokenized communities, ad revenue, and swaps. Open to all users.", "bsc_required_dialog_title": "Creator Monetization is LIVE", - "button_add": "Add", - "button_apply": "Apply", - "button_approve": "Approve", - "button_approve_all": "Approve all", + "button_add": "Add change 5", + "button_apply": "Apply change 6", + "button_approve": "Approve change 7 push", + "button_approve_all": "Approve all change 8 concurrent", "button_back": "Back", "button_back_to_security": "Back to Security", "button_block": "Block", @@ -1002,7 +1004,7 @@ "two_fa_edit_phone_button": "Update phone", "two_fa_edit_phone_new_phone_description": "Enter your new phone number for verification", "two_fa_edit_phone_options_description": "To update your phone number, please verify your identity using one of the 2FA options below", - "two_fa_edit_phone_success": "Verification by phone was successfully changed", + "two_fa_edit_phone_success": "EN translation changed", "two_fa_edit_phone_title": "Update phone number", "two_fa_email": "Email code", "two_fa_failure_authenticator_description": "To set up an Authenticator app, please first link an email address or phone number to your account", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index cfb19f844f0..5b0c12d7d6f 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,13 +1,15 @@ { "@@locale": "es", + "add_new_key_for_auto_translate": "Nuevo texto, que debería ser traducido automáticamente", + "add_new_key_for_auto_translate2": "Nuevo texto, que debería ser traducido automáticamente 2", "all_networks_item": "Todas las redes", "app_language_description": "La aplicación se mostrará en el idioma seleccionado", - "app_language_title": "Idioma de la aplicación", - "article_menu_report_article": "Reportar artículo", + "app_language_title": "Actualización del idioma de la aplicación 1", + "article_menu_report_article": "Informar actualización del artículo 2", "article_page_from_author": "de {name}", "article_page_in_topic": "en {name}", - "article_preview_title": "Vista previa del artículo", - "auth_identity_io": "Identity.io", + "article_preview_title": "Actualización de la vista previa del artículo 3 push", + "auth_identity_io": "Actualización de Identity.io 4 push concurrentes", "auth_link_new_device_description": "Para continuar usando la aplicación, vincula este dispositivo a tu cuenta", "auth_link_new_device_link_button": "Vincular dispositivo", "auth_link_new_device_title": "Inicio de sesión en nuevo dispositivo", @@ -58,10 +60,10 @@ "bsc_required_dialog_action_button": "Comenzar", "bsc_required_dialog_description": "Convierte tu contenido en valor real a través de comunidades tokenizadas, ingresos por publicidad e intercambios. Abierto a todos los usuarios.", "bsc_required_dialog_title": "La Monetización de Creadores está EN VIVO", - "button_add": "Agregar", - "button_apply": "Aplicar", - "button_approve": "Aprobar", - "button_approve_all": "Aprobar todo", + "button_add": "Agregar cambio 5", + "button_apply": "Aplicar cambio 6", + "button_approve": "Aprobar cambio 7 push", + "button_approve_all": "Aprobar todos los cambios 8 concurrentes", "button_back": "Atrás", "button_back_to_security": "Volver a Seguridad", "button_block": "Bloquear", @@ -1197,8 +1199,8 @@ "who_can_reply_settings_title_post": "Quién puede responder a esta publicación", "who_can_reply_settings_title_video": "Quién puede responder a este video", "who_can_reply_settings_verified_accounts": "Cuentas verificadas", - "write_a_message": "Escribe un mensaje...", "token_unable_to_fetch_market_data_title": "No se pudieron obtener los datos del mercado", "token_unable_to_fetch_market_data_description": "Estamos obteniendo información del mercado para este token, inténtalo de nuevo más tarde", + "write_a_message": "Escribe un mensaje...", "your_position_card_title": "Tu posición" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index f5fe63a09fc..0ecd031bac6 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,13 +1,15 @@ { "@@locale": "fr", + "add_new_key_for_auto_translate": "Nouveau texte, qui devrait être traduit automatiquement", + "add_new_key_for_auto_translate2": "Nouveau texte, qui devrait être automatiquement traduit 2", "all_networks_item": "Tous les réseaux", "app_language_description": "L'application s'affichera dans la langue sélectionnée", - "app_language_title": "Langue de l'application", - "article_menu_report_article": "Signaler l'article", + "app_language_title": "Mise à jour de la langue de l'application 1", + "article_menu_report_article": "Signaler la mise à jour de l'article 2", "article_page_from_author": "de {name}", "article_page_in_topic": "dans {name}", - "article_preview_title": "Aperçu de l'article", - "auth_identity_io": "Identity.io", + "article_preview_title": "Mise à jour de l'aperçu de l'article 3 push", + "auth_identity_io": "Mise à jour d'Identity.io 4 push simultanés", "auth_link_new_device_description": "Pour continuer à utiliser l'application, veuillez lier cet appareil à votre compte", "auth_link_new_device_link_button": "Lier l'appareil", "auth_link_new_device_title": "Connexion sur un nouvel appareil", @@ -58,10 +60,10 @@ "bsc_required_dialog_action_button": "Commencer", "bsc_required_dialog_description": "Transformez votre contenu en valeur réelle grâce aux communautés tokenisées, aux revenus publicitaires et aux échanges. Ouvert à tous les utilisateurs.", "bsc_required_dialog_title": "La Monétisation des Créateurs est EN DIRECT", - "button_add": "Ajouter", - "button_apply": "Appliquer", - "button_approve": "Approuver", - "button_approve_all": "Tout approuver", + "button_add": "Ajouter le changement 5", + "button_apply": "Appliquer le changement 6", + "button_approve": "Approuver le changement 7 push", + "button_approve_all": "Approuver tous les changements 8 concurrents", "button_back": "Retour", "button_back_to_security": "Retour à la sécurité", "button_block": "Bloquer", @@ -1197,8 +1199,8 @@ "who_can_reply_settings_title_post": "Qui peut répondre à cette publication", "who_can_reply_settings_title_video": "Qui peut répondre à cette vidéo", "who_can_reply_settings_verified_accounts": "Comptes vérifiés", - "write_a_message": "Écrire un message...", "token_unable_to_fetch_market_data_title": "Impossible de récupérer les données du marché", "token_unable_to_fetch_market_data_description": "Nous récupérons les informations du marché pour ce jeton, veuillez réessayer plus tard", + "write_a_message": "Écrire un message...", "your_position_card_title": "Votre position" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index e504bae60d3..4f6a2293054 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1,13 +1,15 @@ { "@@locale": "it", + "add_new_key_for_auto_translate": "Nuovo testo, che dovrebbe essere tradotto automaticamente", + "add_new_key_for_auto_translate2": "Nuovo testo, che dovrebbe essere tradotto automaticamente 2", "all_networks_item": "Tutte le reti", "app_language_description": "L'app ti sarà mostrata nella lingua selezionata", - "app_language_title": "Lingua dell'app", - "article_menu_report_article": "Segnala articolo", + "app_language_title": "Aggiornamento lingua app 1", + "article_menu_report_article": "Segnala aggiornamento articolo 2", "article_page_from_author": "da {name}", "article_page_in_topic": "in {name}", - "article_preview_title": "Anteprima articolo", - "auth_identity_io": "Identity.io", + "article_preview_title": "Aggiornamento anteprima articolo 3 push", + "auth_identity_io": "Aggiornamento di Identity.io 4 push concorrenti", "auth_link_new_device_description": "Per continuare a utilizzare l'app, collega questo dispositivo al tuo account", "auth_link_new_device_link_button": "Collega dispositivo", "auth_link_new_device_title": "Accesso nuovo dispositivo", @@ -58,10 +60,10 @@ "bsc_required_dialog_action_button": "Inizia", "bsc_required_dialog_description": "Trasforma i tuoi contenuti in valore reale attraverso comunità tokenizzate, entrate pubblicitarie e swap. Aperto a tutti gli utenti.", "bsc_required_dialog_title": "La Monetizzazione dei Creator è IN DIRETTA", - "button_add": "Aggiungi", - "button_apply": "Applica", - "button_approve": "Approva", - "button_approve_all": "Approva tutti", + "button_add": "Aggiungi modifica 5", + "button_apply": "Applica modifica 6", + "button_approve": "Approva la modifica 7 push", + "button_approve_all": "Approva tutte le modifiche 8 concorrenti", "button_back": "Indietro", "button_back_to_security": "Torna alla Sicurezza", "button_block": "Blocca", @@ -1197,8 +1199,8 @@ "who_can_reply_settings_title_post": "Chi può rispondere a questo post", "who_can_reply_settings_title_video": "Chi può rispondere a questo video", "who_can_reply_settings_verified_accounts": "Account verificati", - "write_a_message": "Scrivi un messaggio...", "token_unable_to_fetch_market_data_title": "Impossibile recuperare i dati di mercato", "token_unable_to_fetch_market_data_description": "Stiamo recuperando le informazioni di mercato per questo token, riprova più tardi", + "write_a_message": "Scrivi un messaggio...", "your_position_card_title": "La tua posizione" } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index f873d157bef..4ecea51a9c2 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -1,13 +1,15 @@ { "@@locale": "ko", + "add_new_key_for_auto_translate": "새로운 텍스트, 자동으로 번역되어야 합니다.", + "add_new_key_for_auto_translate2": "새로운 텍스트, 자동으로 번역되어야 합니다 2", "all_networks_item": "모든 네트워크", "app_language_description": "선택한 언어로 앱이 표시됩니다", - "app_language_title": "앱 언어", - "article_menu_report_article": "기사 신고", + "app_language_title": "앱 언어 업데이트 1", + "article_menu_report_article": "기사 업데이트 2 보고", "article_page_from_author": "{name} 작성", "article_page_in_topic": "{name} 주제", - "article_preview_title": "기사 미리보기", - "auth_identity_io": "Identity.io", + "article_preview_title": "기사 미리보기 업데이트 3 푸시", + "auth_identity_io": "Identity.io 업데이트 4 동시 푸시", "auth_link_new_device_description": "앱을 계속 사용하려면 이 기기를 계정에 연결해 주세요", "auth_link_new_device_link_button": "기기 연결", "auth_link_new_device_title": "새 기기 로그인", @@ -58,10 +60,10 @@ "bsc_required_dialog_action_button": "시작하기", "bsc_required_dialog_description": "토큰화 커뮤니티, 광고 수익, 스왑을 통해 콘텐츠를 실질적 가치로 전환하세요. 모든 사용자에게 공개됩니다.", "bsc_required_dialog_title": "크리에이터 수익화 라이브", - "button_add": "추가", - "button_apply": "적용", - "button_approve": "승인", - "button_approve_all": "모두 승인", + "button_add": "변경 사항 5 추가", + "button_apply": "변경 사항 6 적용", + "button_approve": "변경 7 푸시 승인", + "button_approve_all": "모든 변경 사항 8개 동시 승인", "button_back": "뒤로", "button_back_to_security": "보안으로 돌아가기", "button_block": "차단", @@ -207,8 +209,8 @@ "chat_profile_share_modal_title": "프로필 공유", "chat_profile_view_button": "프로필 보기", "chat_read": "읽음", - "chat_read_all": "모두 읽음", "chat_unread": "읽지 않음으로 표시", + "chat_read_all": "모두 읽음", "chat_search_empty": "사용자, 채팅, 그룹, 채널 검색...", "chat_search_no_community_empty": "사용자, 채팅 검색...", "chat_title": "채팅", @@ -1197,8 +1199,8 @@ "who_can_reply_settings_title_post": "이 게시물에 답글을 달 수 있는 사용자", "who_can_reply_settings_title_video": "이 동영상에 답글을 달 수 있는 사용자", "who_can_reply_settings_verified_accounts": "인증된 계정", - "write_a_message": "메시지를 입력하세요...", "token_unable_to_fetch_market_data_title": "시장 데이터를 가져올 수 없습니다", "token_unable_to_fetch_market_data_description": "이 토큰의 시장 정보를 가져오는 중입니다. 나중에 다시 시도해 주세요", + "write_a_message": "메시지를 입력하세요...", "your_position_card_title": "내 포지션" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 5e04f1e1c80..1515d121e0b 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1,13 +1,15 @@ { "@@locale": "pl", + "add_new_key_for_auto_translate": "Manually added translation which should not be Overriden", + "add_new_key_for_auto_translate2": "Nowy tekst, który powinien być automatycznie przetłumaczony 2", "all_networks_item": "Wszystkie sieci", "app_language_description": "Aplikacja będzie wyświetlana w wybranym języku", - "app_language_title": "Język aplikacji", - "article_menu_report_article": "Zgłoś artykuł", + "app_language_title": "Aktualizacja języka aplikacji 1", + "article_menu_report_article": "Zgłoś aktualizację artykułu 2", "article_page_from_author": "od {name}", "article_page_in_topic": "w {name}", - "article_preview_title": "Podgląd artykułu", - "auth_identity_io": "Identity.io", + "article_preview_title": "Aktualizacja podglądu artykułu 3 push", + "auth_identity_io": "Aktualizacja Identity.io 4 równoczesne powiadomienia push", "auth_link_new_device_description": "Aby kontynuować korzystanie z aplikacji, połącz to urządzenie z kontem", "auth_link_new_device_link_button": "Połącz urządzenie", "auth_link_new_device_title": "Logowanie z nowego urządzenia", @@ -58,10 +60,10 @@ "bsc_required_dialog_action_button": "Zacznij", "bsc_required_dialog_description": "Zamień swoją treść na prawdziwą wartość poprzez tokenizowane społeczności, przychody z reklam i swapy. Otwarte dla wszystkich użytkowników.", "bsc_required_dialog_title": "Monetyzacja Twórców jest AKTYWNA", - "button_add": "Dodaj", - "button_apply": "Zastosuj", - "button_approve": "Zatwierdź", - "button_approve_all": "Zatwierdź wszystkie", + "button_add": "Dodaj zmianę 5", + "button_apply": "Zastosuj zmianę 6", + "button_approve": "Zatwierdź zmianę 7 push", + "button_approve_all": "Zatwierdź wszystkie zmiany 8 jednoczesnych", "button_back": "Wstecz", "button_back_to_security": "Powrót do zabezpieczeń", "button_block": "Zablokuj", @@ -783,7 +785,7 @@ "profile_token_buy": "Kup", "profile_tokenized_communities": "Tokenizowane społeczności", "profile_videos": "Wideo", - "profile_videos_empty_list": "{username} nie ma wideo", + "profile_videos_empty_list": "{username} nie ma żadnych filmów", "profile_videos_empty_list_current_user": "Nie masz wideo", "profile_website": "Strona internetowa", "protect_account_button": "Chroń konto", @@ -1002,7 +1004,7 @@ "two_fa_edit_phone_button": "Zaktualizuj telefon", "two_fa_edit_phone_new_phone_description": "Wprowadź swój nowy numer telefonu do weryfikacji", "two_fa_edit_phone_options_description": "Aby zaktualizować swój numer telefonu, proszę zweryfikować swoją tożsamość, używając jednej z opcji 2FA poniżej", - "two_fa_edit_phone_success": "Weryfikacja przez telefon została pomyślnie zmieniona", + "two_fa_edit_phone_success": "Ręcznie zmienione tłumaczenie", "two_fa_edit_phone_title": "Zaktualizuj numer telefonu", "two_fa_email": "Kod e-mail", "two_fa_failure_authenticator_description": "Aby skonfigurować aplikację Authenticator, najpierw połącz adres e-mail lub numer telefonu z kontem", @@ -1197,8 +1199,8 @@ "who_can_reply_settings_title_post": "Kto może odpowiadać na ten post", "who_can_reply_settings_title_video": "Kto może odpowiadać na to wideo", "who_can_reply_settings_verified_accounts": "Zweryfikowane konta", - "write_a_message": "Napisz wiadomość...", "token_unable_to_fetch_market_data_title": "Nie udało się pobrać danych rynkowych", "token_unable_to_fetch_market_data_description": "Pobieramy informacje rynkowe dla tego tokena, spróbuj ponownie później", + "write_a_message": "Napisz wiadomość...", "your_position_card_title": "Twoja pozycja" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 0837f874437..96f2331e279 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1,13 +1,15 @@ { "@@locale": "pt", + "add_new_key_for_auto_translate": "Novo texto, que deve ser traduzido automaticamente", + "add_new_key_for_auto_translate2": "Novo texto, que deve ser traduzido automaticamente 2", "all_networks_item": "Todas as redes", "app_language_description": "O aplicativo será exibido no idioma selecionado", - "app_language_title": "Idioma do aplicativo", - "article_menu_report_article": "Reportar artigo", + "app_language_title": "Atualização de idioma do aplicativo 1", + "article_menu_report_article": "Relatar atualização do artigo 2", "article_page_from_author": "de {name}", "article_page_in_topic": "em {name}", - "article_preview_title": "Pré-visualização do artigo", - "auth_identity_io": "Identity.io", + "article_preview_title": "Atualização de pré-visualização do artigo 3 push", + "auth_identity_io": "Atualização do Identity.io 4 push simultâneos", "auth_link_new_device_description": "Para continuar usando o aplicativo, vincule este dispositivo à sua conta", "auth_link_new_device_link_button": "Vincular dispositivo", "auth_link_new_device_title": "Login em novo dispositivo", @@ -58,10 +60,10 @@ "bsc_required_dialog_action_button": "Começar", "bsc_required_dialog_description": "Transforme seu conteúdo em valor real por meio de comunidades tokenizadas, receita de anúncios e trocas. Aberto a todos os usuários.", "bsc_required_dialog_title": "Monetização de criadores AO VIVO", - "button_add": "Adicionar", - "button_apply": "Aplicar", - "button_approve": "Aprovar", - "button_approve_all": "Aprovar tudo", + "button_add": "Adicionar alteração 5", + "button_apply": "Aplicar alteração 6", + "button_approve": "Aprovar alteração 7 push", + "button_approve_all": "Aprovar todas as mudanças 8 concorrentes", "button_back": "Voltar", "button_back_to_security": "Voltar à Segurança", "button_block": "Bloquear", @@ -207,8 +209,8 @@ "chat_profile_share_modal_title": "Compartilhar perfil", "chat_profile_view_button": "Ver perfil", "chat_read": "Lido", - "chat_read_all": "Marcar tudo como lido", "chat_unread": "Marcar como não lido", + "chat_read_all": "Marcar tudo como lido", "chat_search_empty": "Pesquise aqui por usuários, chats, grupos e canais...", "chat_search_no_community_empty": "Pesquise aqui por usuários, chats...", "chat_title": "Chats", @@ -1197,8 +1199,8 @@ "who_can_reply_settings_title_post": "Quem pode responder a esta publicação", "who_can_reply_settings_title_video": "Quem pode responder a este vídeo", "who_can_reply_settings_verified_accounts": "Contas verificadas", - "write_a_message": "Escreva uma mensagem...", "token_unable_to_fetch_market_data_title": "Não foi possível obter dados de mercado", "token_unable_to_fetch_market_data_description": "Estamos a obter informações de mercado para este token, tente novamente mais tarde", + "write_a_message": "Escreva uma mensagem...", "your_position_card_title": "A sua posição" } diff --git a/lib/l10n/app_ro.arb b/lib/l10n/app_ro.arb index 65e6a67ffd0..0973fe39919 100644 --- a/lib/l10n/app_ro.arb +++ b/lib/l10n/app_ro.arb @@ -1,13 +1,15 @@ { "@@locale": "ro", + "add_new_key_for_auto_translate": "Text nou, care ar trebui să fie tradus automat", + "add_new_key_for_auto_translate2": "Text nou, care ar trebui să fie tradus automat 2", "all_networks_item": "Toate rețelele", "app_language_description": "Aplicația va fi afișată în limba selectată", - "app_language_title": "Limba aplicației", - "article_menu_report_article": "Raportează articolul", + "app_language_title": "Actualizare limbă aplicație 1", + "article_menu_report_article": "Raportați actualizarea articolului 2", "article_page_from_author": "de la {name}", "article_page_in_topic": "în {name}", - "article_preview_title": "Previzualizare articol", - "auth_identity_io": "Identity.io", + "article_preview_title": "Actualizare previzualizare articol 3 push", + "auth_identity_io": "Actualizare Identity.io 4 push-uri concurente", "auth_link_new_device_description": "Pentru a continua utilizarea aplicației, vă rugăm să conectați acest dispozitiv la contul dvs.", "auth_link_new_device_link_button": "Conectează dispozitivul", "auth_link_new_device_title": "Conectare din dispozitiv nou", @@ -58,10 +60,10 @@ "bsc_required_dialog_action_button": "Începe", "bsc_required_dialog_description": "Transformă-ți conținutul în valoare reală prin comunități tokenizate, venituri din publicitate și swap-uri. Deschis pentru toți utilizatorii.", "bsc_required_dialog_title": "Monetizarea Creatorilor este LIVE", - "button_add": "Adaugă", - "button_apply": "Aplică", - "button_approve": "Aprobă", - "button_approve_all": "Aprobă toate", + "button_add": "Adaugă modificare 5", + "button_apply": "Aplică schimbarea 6", + "button_approve": "Aprobă schimbarea 7 push", + "button_approve_all": "Aprobă toate modificările 8 concurente", "button_back": "Înapoi", "button_back_to_security": "Înapoi la Securitate", "button_block": "Blochează", @@ -1197,8 +1199,8 @@ "who_can_reply_settings_title_post": "Cine poate răspunde la această postare", "who_can_reply_settings_title_video": "Cine poate răspunde la acest video", "who_can_reply_settings_verified_accounts": "Conturi verificate", - "write_a_message": "Scrie un mesaj...", "token_unable_to_fetch_market_data_title": "Nu s-au putut obține datele de piață", "token_unable_to_fetch_market_data_description": "Obținem informații de piață pentru acest token, te rugăm să încerci din nou mai târziu", + "write_a_message": "Scrie un mesaj...", "your_position_card_title": "Poziția ta" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index e52e4c868e9..2200f3ca427 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1,13 +1,15 @@ { "@@locale": "ru", + "add_new_key_for_auto_translate": "Новый текст, который должен быть автоматически переведен", + "add_new_key_for_auto_translate2": "Текст введенный вручную", "all_networks_item": "Все сети", "app_language_description": "Приложение будет отображаться на выбранном языке", - "app_language_title": "Язык приложения", - "article_menu_report_article": "Пожаловаться на статью", + "app_language_title": "Обновление языка приложения 1", + "article_menu_report_article": "Сообщить об обновлении статьи 2", "article_page_from_author": "от {name}", "article_page_in_topic": "в {name}", - "article_preview_title": "Предпросмотр статьи", - "auth_identity_io": "Identity.io", + "article_preview_title": "Обновление 3 пуша предварительного просмотра статьи", + "auth_identity_io": "Identity.io обновление 4 одновременных пушей", "auth_link_new_device_description": "Чтобы продолжить использование приложения, подключите это устройство к вашему аккаунту", "auth_link_new_device_link_button": "Подключить устройство", "auth_link_new_device_title": "Вход с нового устройства", @@ -58,10 +60,10 @@ "bsc_required_dialog_action_button": "Начать", "bsc_required_dialog_description": "Превратите свой контент в реальную ценность через токенизированные сообщества, доходы от рекламы и свопы. Доступно для всех пользователей.", "bsc_required_dialog_title": "Монетизация для создателей ЗАПУЩЕНА", - "button_add": "Добавить", - "button_apply": "Применить", - "button_approve": "Одобрить", - "button_approve_all": "Одобрить все", + "button_add": "Добавить изменение 5", + "button_apply": "Применить изменение 6", + "button_approve": "Одобрить изменение 7 пуш", + "button_approve_all": "Одобрить все изменения 8 одновременно", "button_back": "Назад", "button_back_to_security": "Вернуться к безопасности", "button_block": "Заблокировать", @@ -1196,9 +1198,9 @@ "who_can_reply_settings_title_article": "Кто может отвечать на эту статью", "who_can_reply_settings_title_post": "Кто может отвечать на этот пост", "who_can_reply_settings_title_video": "Кто может отвечать на это видео", - "who_can_reply_settings_verified_accounts": "Проверенные аккаунты", - "write_a_message": "Написать сообщение...", + "who_can_reply_settings_verified_accounts": "Измененный текст", "token_unable_to_fetch_market_data_title": "Не удалось загрузить рыночные данные", "token_unable_to_fetch_market_data_description": "Мы загружаем рыночную информацию для этого токена, попробуйте позже", + "write_a_message": "Написать сообщение...", "your_position_card_title": "Ваша позиция" } diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 9ac52fab6dc..cc1e031dbba 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -1,13 +1,15 @@ { "@@locale": "tr", + "add_new_key_for_auto_translate": "Yeni metin, otomatik olarak çevrilmelidir.", + "add_new_key_for_auto_translate2": "Yeni metin, otomatik olarak çevrilmelidir 2", "all_networks_item": "Tüm ağlar", "app_language_description": "Uygulama seçilen dilde gösterilecek", - "app_language_title": "Uygulama dili", - "article_menu_report_article": "Makaleyi bildir", + "app_language_title": "Uygulama dil güncellemesi 1", + "article_menu_report_article": "Makaleyi güncelle 2", "article_page_from_author": "{name} tarafından", "article_page_in_topic": "{name} içinde", - "article_preview_title": "Makale önizlemesi", - "auth_identity_io": "Identity.io", + "article_preview_title": "Makaleye önizleme güncellemesi 3 bildirim", + "auth_identity_io": "Identity.io güncellemesi 4 eşzamanlı itme", "auth_link_new_device_description": "Uygulamayı kullanmaya devam etmek için lütfen bu cihazı hesabınıza bağlayın", "auth_link_new_device_link_button": "Cihazı bağla", "auth_link_new_device_title": "Yeni cihaz girişi", @@ -58,10 +60,10 @@ "bsc_required_dialog_action_button": "Başlayın", "bsc_required_dialog_description": "İçeriğinizi tokenize edilmiş topluluklar, reklam gelirleri ve takaslar aracılığıyla gerçek değere dönüştürün. Tüm kullanıcılara açıktır.", "bsc_required_dialog_title": "İçerik Üreticisi Monetizasyonu CANLI", - "button_add": "Ekle", - "button_apply": "Uygula", - "button_approve": "Onayla", - "button_approve_all": "Tümünü onayla", + "button_add": "Değişiklik 5 ekle", + "button_apply": "Değişikliği uygula 6", + "button_approve": "Değişikliği onayla 7 itme", + "button_approve_all": "Tüm değişiklikleri onayla 8 eşzamanlı", "button_back": "Geri", "button_back_to_security": "Güvenliğe dön", "button_block": "Engelle", @@ -1197,8 +1199,8 @@ "who_can_reply_settings_title_post": "Bu gönderiye kim yanıtlayabilir", "who_can_reply_settings_title_video": "Bu videoya kim yanıtlayabilir", "who_can_reply_settings_verified_accounts": "Doğrulanmış hesaplar", - "write_a_message": "Mesaj yazın...", "token_unable_to_fetch_market_data_title": "Piyasa verileri alınamadı", "token_unable_to_fetch_market_data_description": "Bu token için piyasa bilgilerini getiriyoruz, lütfen daha sonra tekrar deneyin", + "write_a_message": "Mesaj yazın...", "your_position_card_title": "Pozisyonun" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f35a765d9a6..7d81de63a66 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,13 +1,15 @@ { "@@locale": "zh", + "add_new_key_for_auto_translate": "新文本,应该自动翻译", + "add_new_key_for_auto_translate2": "新文本,应该自动翻译 2", "all_networks_item": "所有网络", "app_language_description": "应用将以所选语言显示", - "app_language_title": "应用语言", - "article_menu_report_article": "举报文章", + "app_language_title": "应用语言更新 1", + "article_menu_report_article": "报告文章更新 2", "article_page_from_author": "来自 {name}", "article_page_in_topic": "在 {name}", - "article_preview_title": "文章预览", - "auth_identity_io": "Identity.io", + "article_preview_title": "文章预览更新 3 推送", + "auth_identity_io": "Identity.io 更新 4 个并发推送", "auth_link_new_device_description": "要继续使用应用,请将此设备链接到您的账户", "auth_link_new_device_link_button": "链接设备", "auth_link_new_device_title": "新设备登录", @@ -58,10 +60,10 @@ "bsc_required_dialog_action_button": "开始", "bsc_required_dialog_description": "通过代币化社区、广告收入和交换,将您的内容转化为真正的价值。向所有用户开放。", "bsc_required_dialog_title": "创作者变现功能已上线", - "button_add": "添加", - "button_apply": "应用", - "button_approve": "批准", - "button_approve_all": "全部批准", + "button_add": "添加更改 5", + "button_apply": "应用更改 6", + "button_approve": "批准更改 7 推送", + "button_approve_all": "批准所有更改 8 并发", "button_back": "返回", "button_back_to_security": "返回安全设置", "button_block": "屏蔽", @@ -1197,8 +1199,8 @@ "who_can_reply_settings_title_post": "谁可以回复此帖子", "who_can_reply_settings_title_video": "谁可以回复此视频", "who_can_reply_settings_verified_accounts": "经过验证的账户", - "write_a_message": "写一条消息...", "token_unable_to_fetch_market_data_title": "无法获取市场数据", "token_unable_to_fetch_market_data_description": "我们正在获取此代币的市场信息,请稍后再试", + "write_a_message": "写一条消息...", "your_position_card_title": "持仓" } diff --git a/scripts/auto_translate_locales.sh b/scripts/auto_translate_locales.sh new file mode 100644 index 00000000000..1f337503439 --- /dev/null +++ b/scripts/auto_translate_locales.sh @@ -0,0 +1,99 @@ +#!/bin/bash +set -euo pipefail + +source "$(dirname "$0")/utils.sh" + +usage() { + cat <<'EOF' +Usage: + scripts/auto_translate_locales.sh [--commit] [--fail-if-generated] [--base-ref=|--base-ref=auto] + +Behavior: + - Runs flutter gen-l10n + - If untranslated_messages.txt is non-empty OR app_en.arb changed vs base ref, runs tools/translate_missing.dart + - Re-runs flutter gen-l10n + - Runs scripts/generate_locales.sh verification + +Options: + --commit Pass --commit to translate_missing.dart (CI use). + --fail-if-generated Exit 1 if translations were generated (pre-push use). + --base-ref= Git ref to compare for app_en.arb changes (default: env BASE_REF, else auto). + --base-ref=auto Auto-pick origin/master or origin/main if available. +EOF +} + +DO_COMMIT=0 +FAIL_IF_GENERATED=0 +BASE_REF_ARG="" + +for arg in "$@"; do + case "$arg" in + --commit) DO_COMMIT=1 ;; + --fail-if-generated) FAIL_IF_GENERATED=1 ;; + --base-ref=*) BASE_REF_ARG="${arg#--base-ref=}" ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown arg: $arg" >&2; usage; exit 2 ;; + esac +done + +UNTRANSLATED_FILE="untranslated_messages.txt" +NEED_TRANSLATIONS=0 +L10N_STATUS_BEFORE="$(git status --porcelain lib/l10n 2>/dev/null || true)" + +# Resolve BASE_REF early (so translate_missing always gets it in one run) +BASE_REF="${BASE_REF_ARG:-${BASE_REF:-}}" +if [ -z "$BASE_REF" ] || [ "$BASE_REF" = "auto" ]; then + if git show "origin/master:lib/l10n/app_en.arb" >/dev/null 2>&1; then + BASE_REF="origin/master" + elif git show "origin/main:lib/l10n/app_en.arb" >/dev/null 2>&1; then + BASE_REF="origin/main" + else + BASE_REF="" + fi +fi + +# 1) Generate l10n (produces untranslated_messages.txt) +use_asdf flutter gen-l10n + +if [ -f "$UNTRANSLATED_FILE" ] && [ -s "$UNTRANSLATED_FILE" ] && [ "$(jq 'length' "$UNTRANSLATED_FILE" 2>/dev/null)" != "0" ]; then + NEED_TRANSLATIONS=1 +fi + +if [ "$NEED_TRANSLATIONS" -eq 0 ]; then + if [ -n "$BASE_REF" ] && ! git diff --quiet "$BASE_REF" -- lib/l10n/app_en.arb 2>/dev/null; then + NEED_TRANSLATIONS=1 + fi +fi + +GENERATED=0 +if [ "$NEED_TRANSLATIONS" -eq 1 ]; then + TRANSLATE_ARGS=() + if [ -n "$BASE_REF" ]; then + TRANSLATE_ARGS+=(--base-ref="$BASE_REF") + fi + + if [ "$DO_COMMIT" -eq 1 ]; then + dart run tools/translate_missing.dart --commit "${TRANSLATE_ARGS[@]}" + else + dart run tools/translate_missing.dart "${TRANSLATE_ARGS[@]}" + fi + + # Re-run gen-l10n after filling so untranslated_messages.txt is refreshed. + use_asdf flutter gen-l10n +fi + +# 4) Verify +./scripts/generate_locales.sh + +# Only fail pre-push when this script actually produced new l10n changes. +L10N_STATUS_AFTER="$(git status --porcelain lib/l10n 2>/dev/null || true)" +if [ "$L10N_STATUS_AFTER" != "$L10N_STATUS_BEFORE" ]; then + GENERATED=1 +fi + +if [ "$GENERATED" -eq 1 ] && [ "$FAIL_IF_GENERATED" -eq 1 ]; then + exit 1 +fi + +exit 0 + diff --git a/scripts/generate_locales.sh b/scripts/generate_locales.sh index 83cd5d9916a..bdf844d7828 100755 --- a/scripts/generate_locales.sh +++ b/scripts/generate_locales.sh @@ -40,7 +40,12 @@ if [ ! -s "$UNTRANSLATED_FILE" ] || [ "$(jq 'length' "$UNTRANSLATED_FILE" 2>/dev else echo -e "${YELLOW}⚠️ Some translations are incomplete${NC}" echo "Check $UNTRANSLATED_FILE for details" - exit 1 + # On CI (e.g. GitHub Actions), fail so the pipeline catches missing translations. + # Locally, do not fail so developers can run gen-l10n without blocking. + if [ -n "${GITHUB_ACTIONS:-}" ] && [ "$GITHUB_ACTIONS" = "true" ]; then + exit 1 + fi + exit 0 fi exit 0 diff --git a/scripts/pre_push.sh b/scripts/pre_push.sh index 6786331605d..8441d51d408 100755 --- a/scripts/pre_push.sh +++ b/scripts/pre_push.sh @@ -39,11 +39,13 @@ printf '%s\n' "${avar}" # Generate Locales printf "\e[33;1m%s\e[0m\n" '=== Generate Locales ===' -scripts/generate_locales.sh +bash ./scripts/auto_translate_locales.sh --fail-if-generated --base-ref=auto if [ $? -ne 0 ]; then - printf "\e[31;1m%s\e[0m\n" '=== Generate locales error ===' + printf "\e[31;1m%s\e[0m\n" '=== Translations were generated ===' + printf "\e[31;1m%s\e[0m\n" 'Please review and commit the ARB changes, then push again.' exit 1 fi + printf "\e[33;1m%s\e[0m\n" 'Finished running Generate Locales' printf '%s\n' "${avar}" diff --git a/tools/translate_missing.dart b/tools/translate_missing.dart new file mode 100644 index 00000000000..1a7146e7d96 --- /dev/null +++ b/tools/translate_missing.dart @@ -0,0 +1,719 @@ +// SPDX-License-Identifier: ice License 1.0 +// +// Translates missing ARB strings using OpenAI. Run from repo root after: +// flutter gen-l10n +// Requires OPENAI_API_KEY in the environment. +// +// Usage: dart run tools/translate_missing.dart [options] +// --dry-run Print what would be translated without calling the API or writing files. +// --commit Commit and push ARB changes (use only on CI, on a feature branch). Without this, no git operations run (local default). +// --base-ref=REF (Optional fallback) Compare app_en.arb with REF (e.g. origin/master) and re-translate keys whose English changed in all locales. +// By default, the script will detect the latest successful l10n auto-commit on the branch and use it as a baseline. +// Can also set BASE_REF env. + +import 'dart:convert'; +import 'dart:io'; + +const _arbDir = 'lib/l10n'; +const _templateArb = 'app_en.arb'; +const _untranslatedFile = 'untranslated_messages.txt'; +const _openAiEndpoint = 'https://api.openai.com/v1/chat/completions'; +const _model = 'gpt-4o-mini'; + +/// Locale code -> full language name for the OpenAI prompt. +const _localeToLanguage = { + 'ar': 'Arabic', + 'bg': 'Bulgarian', + 'de': 'German', + 'es': 'Spanish', + 'fr': 'French', + 'it': 'Italian', + 'ko': 'Korean', + 'pl': 'Polish', + 'pt': 'Portuguese', + 'ro': 'Romanian', + 'ru': 'Russian', + 'tr': 'Turkish', + 'zh': 'Chinese', +}; + +void main(List args) async { + final dryRun = args.contains('--dry-run'); + final doCommit = args.contains('--commit'); + final providedBaseRef = _parseBaseRef(args); + + final cwd = Directory.current.path; + final untranslatedPath = '$cwd/$_untranslatedFile'; + final arbDirPath = '$cwd/$_arbDir'; + + // Baseline for "changed English keys" detection: + // 1) Last l10n auto-commit on this branch (so we don't re-translate already-synced keys). + // 2) Then --base-ref / BASE_REF: + // - Pre-push: not set or auto → wrapper passes origin/master; script falls back to _autoPickBaseRef (origin/master | origin/main). + // - CI: set to last successful run's commit SHA (when available), else origin/base_ref. + // 3) Finally auto-pick origin/master or origin/main if no ref provided. + final currentBranch = await _getCurrentBranch(); + final lastL10nCommitSha = (currentBranch != null) + ? await _findLastL10nCommitSha(cwd: cwd, branch: currentBranch) + : null; + final baseRef = lastL10nCommitSha ?? + providedBaseRef ?? + await _autoPickBaseRef(cwd); + + var untranslated = >{}; + if (File(untranslatedPath).existsSync()) { + final content = await File(untranslatedPath).readAsString(); + untranslated = _parseUntranslated(content); + } else if (baseRef == null) { + stderr.writeln('$_untranslatedFile not found. Run "flutter gen-l10n" first.'); + exit(1); + } + + final templatePath = '$arbDirPath/$_templateArb'; + if (!File(templatePath).existsSync()) { + stderr.writeln('Template $_templateArb not found at $templatePath'); + exit(1); + } + + final templateArb = _parseArb(await File(templatePath).readAsString()); + + // Keys in app_en.arb whose source text (or @key metadata) changed vs base ref — need re-translation in all locales. + var changedAddedKeys = {}; + var changedModifiedKeys = {}; + if (baseRef != null) { + final changed = await _getChangedEnKeysDetailed(cwd, baseRef, templateArb); + changedAddedKeys = changed.added; + changedModifiedKeys = changed.modified; + final totalChanged = changedAddedKeys.length + changedModifiedKeys.length; + if (totalChanged > 0) { + stdout.writeln( + 'Keys with changed English (vs $baseRef): $totalChanged ' + '(added: ${changedAddedKeys.length}, modified: ${changedModifiedKeys.length})', + ); + } + } + + // All target locales: from untranslated_messages + existing app_XX.arb (except en). + final allLocales = _allTargetLocales(arbDirPath, untranslated.keys.toList()); + + // Merge: per locale, translate missing + changed keys (deduplicated). + // - Modified English keys: always re-translate (overwrites locale values) + // - Added English keys: translate only if the locale doesn't already have the key + final toTranslate = await _mergeKeysToTranslate( + arbDirPath, + untranslated, + changedAddedKeys, + changedModifiedKeys, + allLocales, + baseRef: baseRef, + cwd: cwd, + ); + + if (toTranslate.isEmpty) { + stdout.writeln('No translations to generate.'); + exit(0); + } + + final totalKeys = toTranslate.values.fold(0, (s, keys) => s + keys.length); + stdout.writeln('Keys to translate: $totalKeys across ${toTranslate.length} locale(s).'); + + final apiKey = await _resolveOpenAiApiKey(cwd); + if (!dryRun && (apiKey == null || apiKey.isEmpty)) { + stderr.writeln( + 'OPENAI_API_KEY is not set in the environment and could not be found in .env or .app.env.\n' + 'Set it via:\n' + ' export OPENAI_API_KEY=... # shell / local\n' + 'or add it to your env files and re-run configure_env.sh.', + ); + exit(1); + } + + if (dryRun) { + for (final e in toTranslate.entries) { + stdout.writeln(' ${e.key}: ${e.value.length} keys'); + } + exit(0); + } + + var translated = 0; + for (final localeEntry in toTranslate.entries) { + final locale = localeEntry.key; + final keysToTranslate = localeEntry.value; + final language = _localeToLanguage[locale] ?? locale; + final targetPath = '$arbDirPath/app_$locale.arb'; + var targetArb = {}; + if (File(targetPath).existsSync()) { + targetArb = _parseArb(await File(targetPath).readAsString()); + } else { + targetArb['@@locale'] = locale; + } + + for (final key in keysToTranslate) { + final source = templateArb[key] as String?; + if (source == null) continue; + + final translatedText = await _translate(apiKey!, source, language); + if (translatedText == null) { + stderr.writeln('Failed to translate: $key for $locale'); + continue; + } + + targetArb[key] = translatedText; + final metaKey = '@$key'; + if (templateArb.containsKey(metaKey)) { + targetArb[metaKey] = templateArb[metaKey]; + } + translated++; + stdout.writeln(' $locale: $key'); + } + + final merged = _mergeArbInTemplateOrder(templateArb, targetArb, locale); + await _writeArb(targetPath, merged); + } + + if (translated == 0) { + stdout.writeln('No strings were translated.'); + exit(0); + } + + if (!doCommit) { + stdout.writeln( + 'Done. Translated $translated string(s). Not committing (use --commit on CI to commit and push).', + ); + exit(0); + } + + final branch = await _getCurrentBranch(); + if (branch == null) { + stderr.writeln('Could not determine current git branch.'); + exit(1); + } + if (_isProtectedBranch(branch)) { + stderr.writeln( + 'Commit is only allowed on feature branches, not on $branch.\n' + 'Refusing to commit/push to avoid modifying master or release branches.', + ); + exit(1); + } + + final hasChanges = await _hasGitChanges(); + if (!hasChanges) { + stdout.writeln('No ARB changes detected after translation.'); + exit(0); + } + + try { + await _gitCommitAndPush(); + } catch (e, st) { + stderr + ..writeln('Git commit/push failed: $e') + ..writeln(st); + exit(1); + } + + stdout.writeln('Translations committed and pushed to $branch.'); + exit(0); +} + +String? _parseBaseRef(List args) { + for (final arg in args) { + if (arg.startsWith('--base-ref=')) { + final ref = arg.substring('--base-ref='.length).trim(); + if (ref.isNotEmpty) return ref; + } + } + return Platform.environment['BASE_REF']; +} + +const _l10nCommitMessage = 'chore(l10n): auto-translate missing strings'; + +/// Finds the latest l10n auto-commit SHA on the given branch. +/// Returns `null` if no such commit is found or if we can't access history. +Future _findLastL10nCommitSha({ + required String cwd, + required String branch, +}) async { + // Best-effort: ensure we have enough history for `git log`. + try { + await Process.run( + 'git', + ['fetch', '--unshallow', 'origin', branch], + runInShell: true, + workingDirectory: cwd, + ); + } catch (_) { + // Ignore fetch failures; we'll still try `git log` with whatever history exists. + } + + final candidateRefs = ['origin/$branch', branch]; + for (final ref in candidateRefs) { + final verify = await Process.run( + 'git', + ['rev-parse', '--verify', ref], + runInShell: true, + workingDirectory: cwd, + ); + if (verify.exitCode != 0) continue; + + final logResult = await Process.run( + 'git', + [ + 'log', + '-n', + '1', + '--format=%H', + '--fixed-strings', + '--grep', + _l10nCommitMessage, + ref, + ], + runInShell: true, + workingDirectory: cwd, + ); + if (logResult.exitCode != 0) continue; + final sha = (logResult.stdout as String).trim(); + if (sha.isNotEmpty) return sha; + } + return null; +} + +/// Picks a base ref to compare `app_en.arb` against when we can't find a prior l10n commit. +Future _autoPickBaseRef(String cwd) async { + final candidates = ['origin/master', 'origin/main']; + for (final ref in candidates) { + final result = await Process.run( + 'git', + ['show', '$ref:$_arbDir/$_templateArb'], + runInShell: true, + workingDirectory: cwd, + ); + if (result.exitCode == 0) return ref; + } + return null; +} + +/// Parses untranslated_messages.txt (JSON: {"locale": ["key1", ...], ...}). +Map> _parseUntranslated(String content) { + final trimmed = content.trim(); + if (trimmed.isEmpty) return {}; + try { + final map = jsonDecode(trimmed) as Map; + return map.map((locale, value) { + final list = value as List; + return MapEntry(locale, list.map((e) => e as String).toList()); + }); + } catch (_) { + return {}; + } +} + +/// Returns message keys in current app_en.arb that are added vs modified (vs [baseRef]). +Future<({Set added, Set modified})> _getChangedEnKeysDetailed( + String cwd, + String baseRef, + Map currentArb, +) async { + final result = await Process.run( + 'git', + ['show', '$baseRef:$_arbDir/$_templateArb'], + runInShell: true, + workingDirectory: cwd, + ); + if (result.exitCode != 0) { + return (added: {}, modified: {}); + } + final oldContent = result.stdout as String; + if (oldContent.trim().isEmpty) { + return ( + added: currentArb.keys.where((k) => !k.startsWith('@') && k != '@@locale').toSet(), + modified: {}, + ); + } + Map oldArb; + try { + oldArb = _parseArb(oldContent); + } catch (_) { + return (added: {}, modified: {}); + } + final added = {}; + final modified = {}; + for (final key in currentArb.keys) { + if (key.startsWith('@') || key == '@@locale') continue; + final currentVal = currentArb[key]; + final oldVal = oldArb[key]; + if (oldVal == null) { + added.add(key); + continue; + } + final metaKey = '@$key'; + final valChanged = currentVal != oldVal; + // jsonDecode produces Map/List instances; default `==` for Map compares identity, + // so we need deep equality for metadata objects (placeholders, descriptions, etc.). + final metaChanged = !_deepEquals(currentArb[metaKey], oldArb[metaKey]); + if (valChanged || metaChanged) modified.add(key); + } + return (added: added, modified: modified); +} + +/// All target locale codes: from [fromUntranslated] plus any app_XX.arb present (except en). +List _allTargetLocales(String arbDirPath, List fromUntranslated) { + final set = fromUntranslated.toSet(); + final dir = Directory(arbDirPath); + if (!dir.existsSync()) return set.toList()..sort(); + for (final e in dir.listSync()) { + if (e is! File) continue; + final name = e.uri.pathSegments.last; + if (!name.startsWith('app_') || !name.endsWith('.arb')) continue; + final locale = name.substring(4, name.length - 4); + if (locale != 'en') set.add(locale); + } + return set.toList()..sort(); +} + +/// Merge untranslated (per locale) with changed keys. Returns map locale -> list of keys to translate. +Future>> _mergeKeysToTranslate( + String arbDirPath, + Map> untranslated, + Set changedAddedKeys, + Set changedModifiedKeys, + List allLocales, { + required String? baseRef, + required String cwd, +}) async { + final out = >{}; + + for (final locale in allLocales) { + final existingKeys = {}; + final targetPath = '$arbDirPath/app_$locale.arb'; + Map? currentLocaleArb; + if (File(targetPath).existsSync()) { + try { + currentLocaleArb = _parseArb(File(targetPath).readAsStringSync()); + for (final k in currentLocaleArb.keys) { + if (k.startsWith('@') || k == '@@locale') continue; + existingKeys.add(k); + } + } catch (_) { + // If a locale ARB can't be parsed, fall back to translating based on untranslated + modified keys. + } + } + + final set = (untranslated[locale] ?? []).toSet(); + + // For modified-English keys, don't keep re-translating if the locale value was + // already updated in this branch. We treat "needs translation" as: + // - key missing in current locale, or + // - current locale value still matches baseRef's value (i.e., not updated yet), or + // - baseRef doesn't have the locale/key (new locale/key in branch). + if (changedModifiedKeys.isNotEmpty) { + if (baseRef == null || currentLocaleArb == null) { + set.addAll(changedModifiedKeys); + } else { + final baseLocaleArb = await _tryLoadArbFromGit( + cwd: cwd, + ref: baseRef, + path: '$_arbDir/app_$locale.arb', + ); + for (final key in changedModifiedKeys) { + final currentVal = currentLocaleArb[key]; + if (currentVal == null) { + set.add(key); + continue; + } + final baseVal = baseLocaleArb?[key]; + if (baseVal == null) { + set.add(key); + continue; + } + if (currentVal == baseVal) { + set.add(key); + } + } + } + } + + for (final key in changedAddedKeys) { + if (!existingKeys.contains(key)) set.add(key); + } + if (set.isEmpty) continue; + out[locale] = set.toList()..sort(); + } + return out; +} + +/// Parses ARB JSON; preserves key order via LinkedHashMap from jsonDecode. +Map _parseArb(String content) { + return jsonDecode(content) as Map; +} + +bool _deepEquals(Object? a, Object? b) { + if (identical(a, b)) return true; + if (a == null || b == null) return a == b; + + if (a is Map && b is Map) { + if (a.length != b.length) return false; + for (final key in a.keys) { + if (!b.containsKey(key)) return false; + if (!_deepEquals(a[key], b[key])) return false; + } + return true; + } + + if (a is List && b is List) { + if (a.length != b.length) return false; + for (var i = 0; i < a.length; i++) { + if (!_deepEquals(a[i], b[i])) return false; + } + return true; + } + + return a == b; +} + +Future?> _tryLoadArbFromGit({ + required String cwd, + required String ref, + required String path, +}) async { + final result = await Process.run( + 'git', + ['show', '$ref:$path'], + runInShell: true, + workingDirectory: cwd, + ); + if (result.exitCode != 0) return null; + final content = (result.stdout as String).trim(); + if (content.isEmpty) return null; + try { + return _parseArb(content); + } catch (_) { + return null; + } +} + +/// Replaces placeholders with tokens for the API; returns (modified string, ordered list of placeholders). +(List, String) _replacePlaceholdersForTranslation(String text) { + final placeholders = []; + var result = text; + + // {name}, {amount}, etc. + result = result.replaceAllMapped(RegExp(r'\{(\w+)\}'), (m) { + final placeholder = m.group(0)!; + final index = placeholders.indexOf(placeholder); + if (index >= 0) return '__PH_${index}__'; + placeholders.add(placeholder); + return '__PH_${placeholders.length - 1}__'; + }); + + // [[:link]]...[[/:link]] + result = result.replaceAllMapped(RegExp(r'\[\[:link\]\](.*?)\[\[\/:link\]\]', dotAll: true), (m) { + final full = m.group(0)!; + final index = placeholders.indexOf(full); + if (index >= 0) return '__PH_${index}__'; + placeholders.add(full); + return '__PH_${placeholders.length - 1}__'; + }); + + return (placeholders, result); +} + +String _restorePlaceholders(String text, List placeholders) { + var result = text; + for (var i = 0; i < placeholders.length; i++) { + result = result.replaceAll('__PH_${i}__', placeholders[i]); + } + return result; +} + +Future _translate(String apiKey, String source, String targetLanguage) async { + final (placeholders, textForApi) = _replacePlaceholdersForTranslation(source); + final prompt = 'Translate the following English app string to $targetLanguage. ' + 'Keep any placeholders like __PH_0__, __PH_1__ exactly as-is (do not translate them). ' + 'Reply with only the translated string, no quotes or explanation.\n\n$textForApi'; + + final body = { + 'model': _model, + 'messages': [ + {'role': 'user', 'content': prompt}, + ], + 'temperature': 0.3, + }; + + final client = HttpClient(); + try { + final request = await client.postUrl(Uri.parse(_openAiEndpoint)); + request.headers.set('Content-Type', 'application/json'); + request.headers.set('Authorization', 'Bearer $apiKey'); + request.write(jsonEncode(body)); + final response = await request.close(); + final responseBody = await response.transform(utf8.decoder).join(); + + if (response.statusCode != 200) { + stderr.writeln('OpenAI API error ${response.statusCode}: $responseBody'); + return null; + } + + final data = jsonDecode(responseBody) as Map; + final choices = data['choices'] as List?; + final content = choices?.isNotEmpty ?? false + ? (choices!.first as Map)['message'] as Map? + : null; + final translated = content?['content'] as String?; + if (translated == null || translated.isEmpty) return null; + + var trimmed = translated.trim(); + if (trimmed.length >= 2 && + (trimmed.startsWith('"') && trimmed.endsWith('"') || + trimmed.startsWith("'") && trimmed.endsWith("'"))) { + trimmed = trimmed.substring(1, trimmed.length - 1); + } + return _restorePlaceholders(trimmed, placeholders); + } catch (e, st) { + stderr + ..writeln('Translation request failed: $e') + ..writeln(st); + return null; + } finally { + client.close(); + } +} + +/// Merges target ARB into template key order; uses template for @@locale and metadata keys. +Map _mergeArbInTemplateOrder( + Map template, + Map target, + String targetLocale, +) { + final merged = {}; + for (final key in template.keys) { + if (key == '@@locale') { + merged[key] = targetLocale; + } else if (key.startsWith('@')) { + merged[key] = template[key]; + } else { + merged[key] = target[key] ?? template[key]; + } + } + return merged; +} + +Future _writeArb(String path, Map arb) async { + const encoder = JsonEncoder.withIndent(' '); + await File(path).writeAsString('${encoder.convert(arb)}\n'); +} + +Future _resolveOpenAiApiKey(String cwd) async { + final fromEnv = Platform.environment['OPENAI_API_KEY']; + if (fromEnv != null && fromEnv.isNotEmpty) return fromEnv; + + final envFile = File('$cwd/.env'); + if (envFile.existsSync()) { + final key = _readKeyFromEnvFile(envFile, 'OPENAI_API_KEY'); + if (key != null && key.isNotEmpty) return key; + } + + final appEnvFile = File('$cwd/.app.env'); + if (appEnvFile.existsSync()) { + final key = _readKeyFromEnvFile(appEnvFile, 'OPENAI_API_KEY'); + if (key != null && key.isNotEmpty) return key; + } + + return null; +} + +String? _readKeyFromEnvFile(File file, String key) { + final prefix = '$key='; + final lines = file.readAsLinesSync(); + for (final line in lines) { + final trimmed = line.trim(); + if (trimmed.startsWith('#') || trimmed.isEmpty) continue; + if (trimmed.startsWith(prefix)) { + return trimmed.substring(prefix.length); + } + } + return null; +} + +Future _getCurrentBranch() async { + final result = await Process.run( + 'git', + ['rev-parse', '--abbrev-ref', 'HEAD'], + runInShell: true, + ); + if (result.exitCode != 0) return null; + final branch = (result.stdout as String).trim(); + // Detached HEAD (e.g. CI pull_request checkout): use GitHub Actions branch when available. + if (branch == 'HEAD') { + final headRef = Platform.environment['GITHUB_HEAD_REF']; + if (headRef != null && headRef.isNotEmpty) return headRef; + return null; + } + return branch; +} + +bool _isProtectedBranch(String branch) { + if (branch == 'master') return true; + if (branch == 'release_candidate') return true; + if (branch.startsWith('release/')) return true; + return false; +} + +Future _hasGitChanges() async { + final result = await Process.run( + 'git', + ['status', '--porcelain', 'lib/l10n'], + runInShell: true, + ); + if (result.exitCode != 0) { + stderr.writeln('git status failed: ${result.stderr}'); + return false; + } + return (result.stdout as String).trim().isNotEmpty; +} + +Future _gitCommitAndPush() async { + const filesPattern = 'lib/l10n'; + + final addResult = await Process.run( + 'git', + ['add', filesPattern], + runInShell: true, + ); + if (addResult.exitCode != 0) { + throw Exception('git add failed: ${addResult.stderr}'); + } + + final commitResult = await Process.run( + 'git', + [ + 'commit', + '-m', + 'chore(l10n): auto-translate missing strings', + ], + runInShell: true, + ); + + if (commitResult.exitCode != 0 && (commitResult.stderr as String).contains('nothing to commit')) { + return; + } + if (commitResult.exitCode != 0) { + throw Exception('git commit failed: ${commitResult.stderr}'); + } + + // In detached HEAD (e.g. CI), push explicitly to the target branch. + final branch = await _getCurrentBranch(); + if (branch == null || branch.isEmpty) { + throw Exception( + 'Could not determine branch for push (detached HEAD?). ' + 'On CI, set GITHUB_HEAD_REF or run from a branch.', + ); + } + final pushResult = await Process.run( + 'git', + ['push', 'origin', 'HEAD:refs/heads/$branch'], + runInShell: true, + ); + if (pushResult.exitCode != 0) { + throw Exception('git push failed: ${pushResult.stderr}'); + } +}