feat(nodedex): add user-defined node groups with many-to-many membership #294
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Check Translations | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - "lib/l10n/**" | |
| - "l10n.yaml" | |
| - "scripts/check_translation_coverage.py" | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - "lib/l10n/**" | |
| - "l10n.yaml" | |
| - "scripts/check_translation_coverage.py" | |
| workflow_dispatch: | |
| permissions: read-all | |
| jobs: | |
| validate-translations: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| - name: Create .env for CI | |
| run: cp .env.ci .env | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 | |
| with: | |
| flutter-version: "3.41.6" | |
| channel: "stable" | |
| cache: true | |
| - name: Install dependencies | |
| run: flutter pub get | |
| - name: Validate ARB files (flutter gen-l10n) | |
| run: | | |
| echo "Running flutter gen-l10n to validate ARB files..." | |
| flutter gen-l10n | |
| echo "All ARB files are valid." | |
| - name: Check ARB file integrity | |
| run: | | |
| python3 << 'SCRIPT' | |
| import json | |
| import os | |
| import re | |
| import sys | |
| l10n_dir = "lib/l10n" | |
| template = "app_en.arb" | |
| with open(os.path.join(l10n_dir, template), "r") as f: | |
| template_data = json.load(f) | |
| template_keys = sorted( | |
| k for k in template_data if not k.startswith("@") and k != "@@locale" | |
| ) | |
| print(f"Template ({template}): {len(template_keys)} keys") | |
| errors = [] | |
| warnings = [] | |
| arb_files = sorted( | |
| f for f in os.listdir(l10n_dir) | |
| if f.endswith(".arb") and f != template | |
| ) | |
| for arb_file in arb_files: | |
| path = os.path.join(l10n_dir, arb_file) | |
| try: | |
| with open(path, "r") as f: | |
| locale_data = json.load(f) | |
| except json.JSONDecodeError as e: | |
| errors.append(f"{arb_file}: Invalid JSON - {e}") | |
| continue | |
| locale_keys = sorted( | |
| k for k in locale_data if not k.startswith("@") and k != "@@locale" | |
| ) | |
| # Extra keys not in template are always an error — stale or mistyped keys | |
| extra = set(locale_keys) - set(template_keys) | |
| if extra: | |
| errors.append( | |
| f"{arb_file}: {len(extra)} extra key(s) not in template: " | |
| + ", ".join(sorted(extra)[:10]) | |
| + ("..." if len(extra) > 10 else "") | |
| ) | |
| # @@locale must be present | |
| if "@@locale" not in locale_data: | |
| errors.append(f"{arb_file}: Missing @@locale field") | |
| # Check that parameterized keys preserve placeholders | |
| for key in locale_keys: | |
| if key not in template_data: | |
| continue | |
| locale_val = locale_data[key] | |
| if not isinstance(locale_val, str): | |
| continue | |
| # Skip if value is identical to English (untranslated placeholder) | |
| if locale_val == template_data[key]: | |
| continue | |
| # Get declared placeholders from @key metadata | |
| meta_key = f"@{key}" | |
| if meta_key in template_data and isinstance(template_data[meta_key], dict): | |
| placeholders = template_data[meta_key].get("placeholders", {}) | |
| declared_params = set(placeholders.keys()) | |
| else: | |
| continue | |
| if not declared_params: | |
| continue | |
| # Each declared placeholder must appear in the translation | |
| locale_params = set(re.findall(r"\{(\w+)\}", locale_val)) | |
| missing_params = declared_params - locale_params | |
| if missing_params: | |
| errors.append( | |
| f"{arb_file}: Key '{key}' is missing placeholder(s): " | |
| + ", ".join(sorted(missing_params)) | |
| ) | |
| # Missing keys are an ERROR. Flutter would silently fall back to | |
| # English, but that lets feature commits add keys to en.arb and | |
| # forget every other locale - this is how locales drift out of | |
| # sync. Every key in app_en.arb MUST exist in every locale file, | |
| # even if the value is identical to English (intentional verbatim | |
| # for brand names, units, format templates). Use | |
| # `python3 scripts/backfill_verbatim_keys.py` to add missing | |
| # keys with the English value when they are intentional verbatim. | |
| missing = set(template_keys) - set(locale_keys) | |
| if missing: | |
| errors.append( | |
| f"{arb_file}: {len(missing)} key(s) missing from locale " | |
| f"(must be present in every locale file - run " | |
| f"scripts/backfill_verbatim_keys.py for intentional " | |
| f"verbatim, or translate them): " | |
| + ", ".join(sorted(missing)[:10]) | |
| + ("..." if len(missing) > 10 else "") | |
| ) | |
| else: | |
| print(f" {arb_file}: {len(locale_keys)} keys (complete)") | |
| if warnings: | |
| print(f"\n{'='*60}") | |
| print(f"WARNINGS ({len(warnings)}):") | |
| for w in warnings: | |
| print(f" ⚠️ {w}") | |
| if errors: | |
| print(f"\n{'='*60}") | |
| print(f"ERRORS ({len(errors)}):") | |
| for e in errors: | |
| print(f" ❌ {e}") | |
| sys.exit(1) | |
| print(f"\nAll translation files passed integrity checks.") | |
| SCRIPT | |
| - name: Translation coverage report | |
| if: always() | |
| run: python3 scripts/check_translation_coverage.py | |
| - name: Upload coverage report | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: translation-status | |
| path: TRANSLATION_STATUS.md | |
| - name: Post coverage summary | |
| if: github.event_name == 'pull_request' | |
| run: | | |
| python3 << 'SCRIPT' | |
| import json | |
| import os | |
| l10n_dir = "lib/l10n" | |
| template = "app_en.arb" | |
| with open(os.path.join(l10n_dir, template), "r") as f: | |
| template_data = json.load(f) | |
| template_keys = [ | |
| k for k in template_data if not k.startswith("@") and k != "@@locale" | |
| ] | |
| total = len(template_keys) | |
| arb_files = sorted( | |
| f for f in os.listdir(l10n_dir) | |
| if f.endswith(".arb") and f != template | |
| ) | |
| lines = ["## 🌐 Translation Coverage", ""] | |
| lines.append(f"| Locale | Translated | Coverage |") | |
| lines.append(f"|--------|-----------|----------|") | |
| for arb_file in arb_files: | |
| path = os.path.join(l10n_dir, arb_file) | |
| try: | |
| with open(path, "r") as f: | |
| locale_data = json.load(f) | |
| except json.JSONDecodeError: | |
| lines.append(f"| {arb_file} | ❌ INVALID JSON | — |") | |
| continue | |
| translated = 0 | |
| for key in template_keys: | |
| if key in locale_data and locale_data[key] != template_data[key]: | |
| translated += 1 | |
| pct = (translated / total * 100) if total > 0 else 0 | |
| locale = arb_file.replace("app_", "").replace(".arb", "") | |
| lines.append(f"| {locale} | {translated}/{total} | {pct:.1f}% |") | |
| lines.append("") | |
| lines.append(f"Keys with values identical to English are counted as untranslated.") | |
| lines.append(f"Missing keys fall back to English automatically.") | |
| summary = "\n".join(lines) | |
| summary_path = os.environ.get("GITHUB_STEP_SUMMARY", "") | |
| if summary_path: | |
| with open(summary_path, "a") as f: | |
| f.write(summary + "\n") | |
| print(summary) | |
| SCRIPT |