mirror of
https://github.com/apache/airflow.git
synced 2026-11-04 07:12:48 +00:00
Add --json output to breeze ui check-translation-completeness (#75831)
The checker computed its per-file statistics inside the rich table rendering loop, so the report existed only as terminal output. Extract the aggregation into build_completeness_report() and add a --json flag that prints the same data as a single JSON document on stdout — following the breeze ci verify --json pattern (#73241): narration is suppressed (or sent to stderr with --verbose) so the output can be piped straight to jq. The default output stays byte-identical, exit-code semantics are unchanged (#55327), and the existing --language/--add-missing/ --remove-unused flags compose with --json. closes: #75772 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A18rBgPJ6X4oXAzsecJjnz
This commit is contained in:
1 parent
7bfc8d32db
commit
eb47cc9f15
8 files changed
+532
-117
No files matched your search
@@ -146,6 +146,13 @@ and 100% coverage:
|
||||
breeze ui check-translation-completeness --language <locale>
|
||||
```
|
||||
|
||||
For machine-readable validation, add `--json` and check the language's totals
|
||||
instead — stdout carries only the JSON document:
|
||||
|
||||
```bash
|
||||
breeze ui check-translation-completeness --language <locale> --json
|
||||
```
|
||||
|
||||
Run pre-commit hooks to fix formatting, licenses, and linting issues:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -202,6 +202,12 @@ Remove unused entries for all locales:
|
||||
breeze ui check-translation-completeness --remove-unused
|
||||
```
|
||||
|
||||
Print the report as machine-readable JSON, e.g. for scripts or agents (stdout carries only the JSON document, so it can be piped straight to `jq`):
|
||||
|
||||
```bash
|
||||
breeze ui check-translation-completeness --language <language_code> --json
|
||||
```
|
||||
|
||||
## Locale-specific guidance
|
||||
|
||||
Before translating, read the locale-specific guide if one exists. Those files contain glossary and style choices
|
||||
|
||||
@@ -86,6 +86,9 @@ Example usage:
|
||||
# Fix translations for a specific language
|
||||
breeze ui check-translation-completeness --language de --add-missing --remove-unused
|
||||
|
||||
# Print the report as machine-readable JSON (stdout carries only the JSON document)
|
||||
breeze ui check-translation-completeness --language de --json
|
||||
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<svg class="rich-terminal" viewBox="0 0 1482 416.0" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="rich-terminal" viewBox="0 0 1482 440.4" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Generated with Rich https://www.textualize.io -->
|
||||
<style>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
<defs>
|
||||
<clipPath id="breeze-ui-check-translation-completeness-clip-terminal">
|
||||
<rect x="0" y="0" width="1463.0" height="365.0" />
|
||||
<rect x="0" y="0" width="1463.0" height="389.4" />
|
||||
</clipPath>
|
||||
<clipPath id="breeze-ui-check-translation-completeness-line-0">
|
||||
<rect x="0" y="1.5" width="1464" height="24.65"/>
|
||||
@@ -87,9 +87,12 @@
|
||||
<clipPath id="breeze-ui-check-translation-completeness-line-13">
|
||||
<rect x="0" y="318.7" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="breeze-ui-check-translation-completeness-line-14">
|
||||
<rect x="0" y="343.1" width="1464" height="24.65"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="414" rx="8"/><text class="breeze-ui-check-translation-completeness-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command: ui check-translation-completeness</text>
|
||||
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="438.4" rx="8"/><text class="breeze-ui-check-translation-completeness-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command: ui check-translation-completeness</text>
|
||||
<g transform="translate(26,22)">
|
||||
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
|
||||
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
|
||||
@@ -108,12 +111,13 @@
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="166.4" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-6)">│</text><text class="breeze-ui-check-translation-completeness-r4" x="24.4" y="166.4" textLength="183" clip-path="url(#breeze-ui-check-translation-completeness-line-6)">--language     </text><text class="breeze-ui-check-translation-completeness-r6" x="231.8" y="166.4" textLength="24.4" clip-path="url(#breeze-ui-check-translation-completeness-line-6)">-l</text><text class="breeze-ui-check-translation-completeness-r1" x="280.6" y="166.4" textLength="719.8" clip-path="url(#breeze-ui-check-translation-completeness-line-6)">Show summary for a single language (e.g. en, de, pl, etc.) </text><text class="breeze-ui-check-translation-completeness-r7" x="1000.4" y="166.4" textLength="73.2" clip-path="url(#breeze-ui-check-translation-completeness-line-6)">(TEXT)</text><text class="breeze-ui-check-translation-completeness-r5" x="1451.8" y="166.4" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-6)">│</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-6)">
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="190.8" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-7)">│</text><text class="breeze-ui-check-translation-completeness-r4" x="24.4" y="190.8" textLength="183" clip-path="url(#breeze-ui-check-translation-completeness-line-7)">--add-missing  </text><text class="breeze-ui-check-translation-completeness-r1" x="280.6" y="190.8" textLength="1122.4" clip-path="url(#breeze-ui-check-translation-completeness-line-7)">Add missing translations for all languages except English, prefixed with 'TODO: translate:'.</text><text class="breeze-ui-check-translation-completeness-r5" x="1451.8" y="190.8" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-7)">│</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-7)">
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="215.2" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-8)">│</text><text class="breeze-ui-check-translation-completeness-r4" x="24.4" y="215.2" textLength="183" clip-path="url(#breeze-ui-check-translation-completeness-line-8)">--remove-unused</text><text class="breeze-ui-check-translation-completeness-r1" x="280.6" y="215.2" textLength="915" clip-path="url(#breeze-ui-check-translation-completeness-line-8)">Remove unused translations (keys present in the language but not required).</text><text class="breeze-ui-check-translation-completeness-r5" x="1451.8" y="215.2" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-8)">│</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-8)">
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="239.6" textLength="1464" clip-path="url(#breeze-ui-check-translation-completeness-line-9)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-9)">
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="264" textLength="24.4" clip-path="url(#breeze-ui-check-translation-completeness-line-10)">╭─</text><text class="breeze-ui-check-translation-completeness-r5" x="24.4" y="264" textLength="195.2" clip-path="url(#breeze-ui-check-translation-completeness-line-10)"> Common options </text><text class="breeze-ui-check-translation-completeness-r5" x="219.6" y="264" textLength="1220" clip-path="url(#breeze-ui-check-translation-completeness-line-10)">────────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="breeze-ui-check-translation-completeness-r5" x="1439.6" y="264" textLength="24.4" clip-path="url(#breeze-ui-check-translation-completeness-line-10)">─╮</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="264" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-10)">
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="288.4" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-11)">│</text><text class="breeze-ui-check-translation-completeness-r4" x="24.4" y="288.4" textLength="109.8" clip-path="url(#breeze-ui-check-translation-completeness-line-11)">--verbose</text><text class="breeze-ui-check-translation-completeness-r6" x="158.6" y="288.4" textLength="24.4" clip-path="url(#breeze-ui-check-translation-completeness-line-11)">-v</text><text class="breeze-ui-check-translation-completeness-r1" x="207.4" y="288.4" textLength="585.6" clip-path="url(#breeze-ui-check-translation-completeness-line-11)">Print verbose information about performed steps.</text><text class="breeze-ui-check-translation-completeness-r5" x="1451.8" y="288.4" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-11)">│</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-11)">
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="312.8" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-12)">│</text><text class="breeze-ui-check-translation-completeness-r4" x="24.4" y="312.8" textLength="109.8" clip-path="url(#breeze-ui-check-translation-completeness-line-12)">--dry-run</text><text class="breeze-ui-check-translation-completeness-r6" x="158.6" y="312.8" textLength="24.4" clip-path="url(#breeze-ui-check-translation-completeness-line-12)">-D</text><text class="breeze-ui-check-translation-completeness-r1" x="207.4" y="312.8" textLength="719.8" clip-path="url(#breeze-ui-check-translation-completeness-line-12)">If dry-run is set, commands are only printed, not executed.</text><text class="breeze-ui-check-translation-completeness-r5" x="1451.8" y="312.8" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-12)">│</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-12)">
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="337.2" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-13)">│</text><text class="breeze-ui-check-translation-completeness-r4" x="24.4" y="337.2" textLength="109.8" clip-path="url(#breeze-ui-check-translation-completeness-line-13)">--help   </text><text class="breeze-ui-check-translation-completeness-r6" x="158.6" y="337.2" textLength="24.4" clip-path="url(#breeze-ui-check-translation-completeness-line-13)">-h</text><text class="breeze-ui-check-translation-completeness-r1" x="207.4" y="337.2" textLength="329.4" clip-path="url(#breeze-ui-check-translation-completeness-line-13)">Show this message and exit.</text><text class="breeze-ui-check-translation-completeness-r5" x="1451.8" y="337.2" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-13)">│</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-13)">
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="361.6" textLength="1464" clip-path="url(#breeze-ui-check-translation-completeness-line-14)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-14)">
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="239.6" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-9)">│</text><text class="breeze-ui-check-translation-completeness-r4" x="24.4" y="239.6" textLength="183" clip-path="url(#breeze-ui-check-translation-completeness-line-9)">--json         </text><text class="breeze-ui-check-translation-completeness-r1" x="280.6" y="239.6" textLength="951.6" clip-path="url(#breeze-ui-check-translation-completeness-line-9)">Print the completeness report as machine-readable JSON instead of rich tables.</text><text class="breeze-ui-check-translation-completeness-r5" x="1451.8" y="239.6" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-9)">│</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-9)">
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="264" textLength="1464" clip-path="url(#breeze-ui-check-translation-completeness-line-10)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="264" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-10)">
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="288.4" textLength="24.4" clip-path="url(#breeze-ui-check-translation-completeness-line-11)">╭─</text><text class="breeze-ui-check-translation-completeness-r5" x="24.4" y="288.4" textLength="195.2" clip-path="url(#breeze-ui-check-translation-completeness-line-11)"> Common options </text><text class="breeze-ui-check-translation-completeness-r5" x="219.6" y="288.4" textLength="1220" clip-path="url(#breeze-ui-check-translation-completeness-line-11)">────────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="breeze-ui-check-translation-completeness-r5" x="1439.6" y="288.4" textLength="24.4" clip-path="url(#breeze-ui-check-translation-completeness-line-11)">─╮</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-11)">
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="312.8" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-12)">│</text><text class="breeze-ui-check-translation-completeness-r4" x="24.4" y="312.8" textLength="109.8" clip-path="url(#breeze-ui-check-translation-completeness-line-12)">--verbose</text><text class="breeze-ui-check-translation-completeness-r6" x="158.6" y="312.8" textLength="24.4" clip-path="url(#breeze-ui-check-translation-completeness-line-12)">-v</text><text class="breeze-ui-check-translation-completeness-r1" x="207.4" y="312.8" textLength="585.6" clip-path="url(#breeze-ui-check-translation-completeness-line-12)">Print verbose information about performed steps.</text><text class="breeze-ui-check-translation-completeness-r5" x="1451.8" y="312.8" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-12)">│</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-12)">
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="337.2" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-13)">│</text><text class="breeze-ui-check-translation-completeness-r4" x="24.4" y="337.2" textLength="109.8" clip-path="url(#breeze-ui-check-translation-completeness-line-13)">--dry-run</text><text class="breeze-ui-check-translation-completeness-r6" x="158.6" y="337.2" textLength="24.4" clip-path="url(#breeze-ui-check-translation-completeness-line-13)">-D</text><text class="breeze-ui-check-translation-completeness-r1" x="207.4" y="337.2" textLength="719.8" clip-path="url(#breeze-ui-check-translation-completeness-line-13)">If dry-run is set, commands are only printed, not executed.</text><text class="breeze-ui-check-translation-completeness-r5" x="1451.8" y="337.2" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-13)">│</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-13)">
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="361.6" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-14)">│</text><text class="breeze-ui-check-translation-completeness-r4" x="24.4" y="361.6" textLength="109.8" clip-path="url(#breeze-ui-check-translation-completeness-line-14)">--help   </text><text class="breeze-ui-check-translation-completeness-r6" x="158.6" y="361.6" textLength="24.4" clip-path="url(#breeze-ui-check-translation-completeness-line-14)">-h</text><text class="breeze-ui-check-translation-completeness-r1" x="207.4" y="361.6" textLength="329.4" clip-path="url(#breeze-ui-check-translation-completeness-line-14)">Show this message and exit.</text><text class="breeze-ui-check-translation-completeness-r5" x="1451.8" y="361.6" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-14)">│</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-14)">
|
||||
</text><text class="breeze-ui-check-translation-completeness-r5" x="0" y="386" textLength="1464" clip-path="url(#breeze-ui-check-translation-completeness-line-15)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ui-check-translation-completeness-r1" x="1464" y="386" textLength="12.2" clip-path="url(#breeze-ui-check-translation-completeness-line-15)">
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
@@ -1 +1 @@
|
||||
4d8c7e8b2fe193a4b8e8cf25a429ff7e
|
||||
297713f7f93bba54244a44180842df41
|
||||
@@ -16,10 +16,11 @@
|
||||
# under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
@@ -27,10 +28,12 @@ import click
|
||||
|
||||
from airflow_breeze.commands.common_options import option_dry_run, option_verbose
|
||||
from airflow_breeze.utils.click_utils import BreezeGroup
|
||||
from airflow_breeze.utils.console import console_print, get_console
|
||||
from airflow_breeze.utils.console import console_print, get_console, get_stderr_console
|
||||
from airflow_breeze.utils.docker_command_utils import perform_environment_checks
|
||||
from airflow_breeze.utils.path_utils import AIRFLOW_ROOT_PATH
|
||||
from airflow_breeze.utils.reproduce_ci import SKIP_LOCAL_REPRODUCTION
|
||||
from airflow_breeze.utils.run_utils import assert_prek_installed, run_compile_ui_assets
|
||||
from airflow_breeze.utils.shared_options import get_verbose
|
||||
|
||||
LOCALES_DIR = AIRFLOW_ROOT_PATH / "airflow-core" / "src" / "airflow" / "ui" / "public" / "i18n" / "locales"
|
||||
|
||||
@@ -313,6 +316,15 @@ def print_locale_file_table(locale_files: list[LocaleFiles], language: str | Non
|
||||
console.print(table)
|
||||
|
||||
|
||||
def locale_file_sets_differ(locale_files: list[LocaleFiles], language: str | None = None) -> bool:
|
||||
"""Return True when the compared locales do not all have the same set of translation files."""
|
||||
filtered = (
|
||||
locale_files if language is None else [lf for lf in locale_files if lf.locale in (language, "en")]
|
||||
)
|
||||
file_sets = [set(lf.files) for lf in filtered]
|
||||
return len(file_sets) > 1 and any(file_sets[0] != fs for fs in file_sets[1:])
|
||||
|
||||
|
||||
def print_file_set_differences(locale_files: list[LocaleFiles], language: str | None = None) -> bool:
|
||||
if language and language == "en":
|
||||
return False
|
||||
@@ -321,12 +333,10 @@ def print_file_set_differences(locale_files: list[LocaleFiles], language: str |
|
||||
locale_files if language is None else [lf for lf in locale_files if lf.locale in (language, "en")]
|
||||
)
|
||||
all_file_sets = {lf.locale: set(lf.files) for lf in filtered}
|
||||
file_sets: list[set[str]] = list(all_file_sets.values())
|
||||
found_difference = False
|
||||
en_files = set(all_file_sets["en"])
|
||||
if len(file_sets) > 1 and any(file_sets[0] != fs for fs in file_sets[1:]):
|
||||
found_difference = True
|
||||
found_difference = locale_file_sets_differ(locale_files, language)
|
||||
if found_difference:
|
||||
console.print("[bold red]Error: Locales have different set of translation files![/bold red]")
|
||||
en_files = set(all_file_sets["en"])
|
||||
all_locales_sorted = sorted(all_file_sets.keys())
|
||||
for locale in all_locales_sorted:
|
||||
files_missing = sorted(en_files - all_file_sets[locale])
|
||||
@@ -392,40 +402,34 @@ def count_todos(obj) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def print_translation_progress(locale_files, summary):
|
||||
console = get_console()
|
||||
from rich.table import Table
|
||||
def build_completeness_report(
|
||||
locale_files: list[LocaleFiles], summary: dict[str, LocaleSummary]
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
"""
|
||||
Aggregate per-language, per-file translation statistics.
|
||||
|
||||
tables = defaultdict(lambda: Table(show_lines=True))
|
||||
all_files = set()
|
||||
coverage_per_language = {}
|
||||
Returns a mapping: language -> {"files": {filename: stats}, "totals": stats}. File stats
|
||||
carry the counts shown by the progress tables plus the sorted lists of missing and unused
|
||||
keys. Coverage values are raw floats; renderers round them for display.
|
||||
"""
|
||||
all_files: set[str] = set()
|
||||
for lf in locale_files:
|
||||
all_files.update(lf.files)
|
||||
|
||||
has_todos = False
|
||||
|
||||
report: dict[str, dict[str, Any]] = {}
|
||||
for lf in sorted(locale_files):
|
||||
lang = lf.locale
|
||||
if lang == "en":
|
||||
continue
|
||||
table = tables[lang]
|
||||
table.title = f"Translation Progress: {lang}"
|
||||
table.add_column("File", style="bold cyan")
|
||||
table.add_column("Required (base EN)", style="dim")
|
||||
table.add_column("Required (plural)", style="dim")
|
||||
table.add_column("Total required", style="bold")
|
||||
table.add_column("Translated", style="green")
|
||||
table.add_column("Missing", style="red")
|
||||
table.add_column("Coverage", style="bold")
|
||||
table.add_column("TODOs", style="magenta")
|
||||
table.add_column("Unused", style="yellow")
|
||||
total_required_base = 0
|
||||
total_required_plural = 0
|
||||
total_required = 0
|
||||
total_translated = 0
|
||||
total_missing = 0
|
||||
total_todos = 0
|
||||
total_unused = 0
|
||||
files: dict[str, dict[str, Any]] = {}
|
||||
totals: dict[str, Any] = {
|
||||
"required": 0,
|
||||
"required_base_en": 0,
|
||||
"required_plural": 0,
|
||||
"translated": 0,
|
||||
"missing": 0,
|
||||
"todos": 0,
|
||||
"unused": 0,
|
||||
}
|
||||
for filename in sorted(all_files):
|
||||
en_path = Path(LOCALES_DIR / "en" / filename)
|
||||
file_path = Path(LOCALES_DIR / lang / filename)
|
||||
@@ -440,8 +444,8 @@ def print_translation_progress(locale_files, summary):
|
||||
required_base_en = len(en_keys)
|
||||
required_plural = len(required_keys) - required_base_en
|
||||
total_req = len(required_keys)
|
||||
file_missing = len(diff.missing_keys.get(lang, []))
|
||||
file_unused = len(diff.unused_keys.get(lang, []))
|
||||
missing_keys = sorted(diff.missing_keys.get(lang, []))
|
||||
unused_keys = sorted(diff.unused_keys.get(lang, []))
|
||||
if file_path.exists():
|
||||
with open(file_path) as f:
|
||||
lang_data = json.load(f)
|
||||
@@ -454,54 +458,90 @@ def print_translation_progress(locale_files, summary):
|
||||
file_todos = sum(
|
||||
1 for k in required_keys if k in lang_key_to_value and is_todo_value(lang_key_to_value[k])
|
||||
)
|
||||
if file_todos > 0:
|
||||
has_todos = True
|
||||
else:
|
||||
file_translated = 0
|
||||
file_todos = 0
|
||||
file_coverage = 100 * file_translated / total_req if total_req else 100.0
|
||||
if file_missing > 0:
|
||||
files[filename] = {
|
||||
"required": total_req,
|
||||
"required_base_en": required_base_en,
|
||||
"required_plural": required_plural,
|
||||
"translated": file_translated,
|
||||
"missing": len(missing_keys),
|
||||
"todos": file_todos,
|
||||
"unused": len(unused_keys),
|
||||
"coverage": file_coverage,
|
||||
"missing_keys": missing_keys,
|
||||
"unused_keys": unused_keys,
|
||||
}
|
||||
totals["required"] += total_req
|
||||
totals["required_base_en"] += required_base_en
|
||||
totals["required_plural"] += required_plural
|
||||
totals["translated"] += file_translated
|
||||
totals["missing"] += len(missing_keys)
|
||||
totals["todos"] += file_todos
|
||||
totals["unused"] += len(unused_keys)
|
||||
totals["coverage"] = 100 * totals["translated"] / totals["required"] if totals["required"] else 100.0
|
||||
report[lang] = {"files": files, "totals": totals}
|
||||
return report
|
||||
|
||||
|
||||
def print_translation_progress(locale_files, summary):
|
||||
console = get_console()
|
||||
from rich.table import Table
|
||||
|
||||
report = build_completeness_report(locale_files, summary)
|
||||
has_todos = any(
|
||||
file_stats["todos"] > 0 for lang_data in report.values() for file_stats in lang_data["files"].values()
|
||||
)
|
||||
coverage_per_language = {lang: lang_data["totals"]["coverage"] for lang, lang_data in report.items()}
|
||||
|
||||
for lang, lang_data in report.items():
|
||||
table = Table(show_lines=True)
|
||||
table.title = f"Translation Progress: {lang}"
|
||||
table.add_column("File", style="bold cyan")
|
||||
table.add_column("Required (base EN)", style="dim")
|
||||
table.add_column("Required (plural)", style="dim")
|
||||
table.add_column("Total required", style="bold")
|
||||
table.add_column("Translated", style="green")
|
||||
table.add_column("Missing", style="red")
|
||||
table.add_column("Coverage", style="bold")
|
||||
table.add_column("TODOs", style="magenta")
|
||||
table.add_column("Unused", style="yellow")
|
||||
for filename, file_stats in lang_data["files"].items():
|
||||
if file_stats["missing"] > 0:
|
||||
style = "red"
|
||||
elif file_todos > 0 or file_unused > 0:
|
||||
elif file_stats["todos"] > 0 or file_stats["unused"] > 0:
|
||||
style = "yellow"
|
||||
else:
|
||||
style = "bold green"
|
||||
table.add_row(
|
||||
f"[bold reverse]{filename}[/bold reverse]",
|
||||
str(required_base_en),
|
||||
str(required_plural),
|
||||
str(total_req),
|
||||
str(file_translated),
|
||||
str(file_missing),
|
||||
f"{file_coverage:.1f}%",
|
||||
str(file_todos),
|
||||
str(file_unused),
|
||||
str(file_stats["required_base_en"]),
|
||||
str(file_stats["required_plural"]),
|
||||
str(file_stats["required"]),
|
||||
str(file_stats["translated"]),
|
||||
str(file_stats["missing"]),
|
||||
f"{file_stats['coverage']:.1f}%",
|
||||
str(file_stats["todos"]),
|
||||
str(file_stats["unused"]),
|
||||
style=style,
|
||||
)
|
||||
total_required_base += required_base_en
|
||||
total_required_plural += required_plural
|
||||
total_required += total_req
|
||||
total_translated += file_translated
|
||||
total_missing += file_missing
|
||||
total_todos += file_todos
|
||||
total_unused += file_unused
|
||||
|
||||
total_coverage = 100 * total_translated / total_required if total_required else 100.0
|
||||
coverage_per_language[lang] = total_coverage
|
||||
totals = lang_data["totals"]
|
||||
table.add_row(
|
||||
"All files",
|
||||
str(total_required_base),
|
||||
str(total_required_plural),
|
||||
str(total_required),
|
||||
str(total_translated),
|
||||
str(total_missing),
|
||||
f"{total_coverage:.1f}%",
|
||||
str(total_todos),
|
||||
str(total_unused),
|
||||
style="red" if total_missing > 0 or total_todos > 0 or total_unused > 0 else "bold green",
|
||||
str(totals["required_base_en"]),
|
||||
str(totals["required_plural"]),
|
||||
str(totals["required"]),
|
||||
str(totals["translated"]),
|
||||
str(totals["missing"]),
|
||||
f"{totals['coverage']:.1f}%",
|
||||
str(totals["todos"]),
|
||||
str(totals["unused"]),
|
||||
style="red"
|
||||
if totals["missing"] > 0 or totals["todos"] > 0 or totals["unused"] > 0
|
||||
else "bold green",
|
||||
)
|
||||
|
||||
for _lang, table in tables.items():
|
||||
console.print(table)
|
||||
|
||||
return has_todos, coverage_per_language
|
||||
@@ -618,44 +658,15 @@ def remove_unused_translations(language: str, summary: dict[str, LocaleSummary])
|
||||
console.print(f"[green]Removed {len(unused_keys)} unused translations from {lang_path}[/green]")
|
||||
|
||||
|
||||
@click.group(cls=BreezeGroup, name="ui", help="Tools for UI development and maintenance")
|
||||
def ui_group():
|
||||
pass
|
||||
|
||||
|
||||
@ui_group.command(
|
||||
name="check-translation-completeness",
|
||||
help="Check completeness of UI translations.",
|
||||
)
|
||||
@click.option(
|
||||
"--language",
|
||||
"-l",
|
||||
default=None,
|
||||
help="Show summary for a single language (e.g. en, de, pl, etc.)",
|
||||
)
|
||||
@click.option(
|
||||
"--add-missing",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Add missing translations for all languages except English, prefixed with 'TODO: translate:'.",
|
||||
)
|
||||
@click.option(
|
||||
"--remove-unused",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Remove unused translations (keys present in the language but not required).",
|
||||
)
|
||||
@option_verbose
|
||||
@option_dry_run
|
||||
def check_translation_completeness(
|
||||
language: str | None = None, add_missing: bool = False, remove_unused: bool = False
|
||||
):
|
||||
locale_files = get_locale_files()
|
||||
console = get_console()
|
||||
print_locale_file_table(locale_files, language)
|
||||
found_difference = print_file_set_differences(locale_files, language)
|
||||
summary, missing_counts = compare_keys(locale_files)
|
||||
console.print("\n[bold underline]Summary of differences by language:[/bold underline]", style="cyan")
|
||||
def apply_translation_modifications(
|
||||
locale_files: list[LocaleFiles],
|
||||
summary: dict[str, LocaleSummary],
|
||||
missing_counts: dict[str, dict[str, int]],
|
||||
language: str | None,
|
||||
add_missing: bool,
|
||||
remove_unused: bool,
|
||||
) -> tuple[dict[str, LocaleSummary], dict[str, dict[str, int]]]:
|
||||
"""Run --add-missing / --remove-unused and recompute the summary after each pass."""
|
||||
if add_missing and language != "en":
|
||||
# Loop through all languages except 'en' and add missing translations
|
||||
if language:
|
||||
@@ -688,6 +699,120 @@ def check_translation_completeness(
|
||||
remove_unused_translations(lf.locale, filtered_summary)
|
||||
# After removing, re-run the summary for all languages
|
||||
summary, missing_counts = compare_keys(get_locale_files())
|
||||
return summary, missing_counts
|
||||
|
||||
|
||||
def print_completeness_json(language: str | None, add_missing: bool, remove_unused: bool) -> None:
|
||||
"""Print the completeness report as a single JSON document on stdout.
|
||||
|
||||
Mirrors the data the progress tables show. Console narration (load errors and
|
||||
--add-missing / --remove-unused progress) is kept off stdout so the output can be piped
|
||||
straight to tools like jq: it goes to stderr with --verbose and is suppressed otherwise —
|
||||
the same stdout contract as ``breeze ci verify --json``. If the run aborts, the suppressed
|
||||
narration is replayed to stderr so the error is not silent.
|
||||
"""
|
||||
locale_files = get_locale_files()
|
||||
if language:
|
||||
locales = [lf.locale for lf in locale_files]
|
||||
if language not in locales:
|
||||
get_stderr_console().print(
|
||||
f"[red]Language '{language}' not found among locales: {', '.join(locales)}[/red]"
|
||||
)
|
||||
sys.exit(2)
|
||||
if language == "en":
|
||||
get_stderr_console().print("[bold red]Cannot check completeness for English language![/bold red]")
|
||||
sys.exit(2)
|
||||
buffer = None if get_verbose() else io.StringIO()
|
||||
completed = False
|
||||
try:
|
||||
with contextlib.redirect_stdout(buffer if buffer is not None else sys.stderr):
|
||||
summary, missing_counts = compare_keys(locale_files)
|
||||
summary, _ = apply_translation_modifications(
|
||||
locale_files, summary, missing_counts, language, add_missing, remove_unused
|
||||
)
|
||||
reported_locale_files = [lf for lf in locale_files if language is None or lf.locale == language]
|
||||
report = build_completeness_report(reported_locale_files, summary)
|
||||
completed = True
|
||||
finally:
|
||||
# Replay the suppressed narration so a fatal error inside the computation is not
|
||||
# silent, whether it aborted via sys.exit() or an unexpected exception.
|
||||
if not completed and buffer is not None:
|
||||
sys.stderr.write(buffer.getvalue())
|
||||
languages_payload: dict[str, Any] = {}
|
||||
for lang, lang_data in report.items():
|
||||
languages_payload[lang] = {
|
||||
"files": {
|
||||
filename: {**file_stats, "coverage": round(file_stats["coverage"], 1)}
|
||||
for filename, file_stats in lang_data["files"].items()
|
||||
},
|
||||
"totals": {**lang_data["totals"], "coverage": round(lang_data["totals"]["coverage"], 1)},
|
||||
}
|
||||
has_missing = any(lang_data["totals"]["missing"] > 0 for lang_data in report.values())
|
||||
has_todos = any(lang_data["totals"]["todos"] > 0 for lang_data in report.values())
|
||||
payload = {
|
||||
"languages": languages_payload,
|
||||
"complete": not locale_file_sets_differ(locale_files, language) and not has_missing and not has_todos,
|
||||
}
|
||||
print(json.dumps(payload, indent=2))
|
||||
|
||||
|
||||
@click.group(cls=BreezeGroup, name="ui", help="Tools for UI development and maintenance")
|
||||
def ui_group():
|
||||
pass
|
||||
|
||||
|
||||
@ui_group.command(
|
||||
name="check-translation-completeness",
|
||||
help="Check completeness of UI translations.",
|
||||
)
|
||||
@click.option(
|
||||
"--language",
|
||||
"-l",
|
||||
default=None,
|
||||
help="Show summary for a single language (e.g. en, de, pl, etc.)",
|
||||
)
|
||||
@click.option(
|
||||
"--add-missing",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Add missing translations for all languages except English, prefixed with 'TODO: translate:'.",
|
||||
)
|
||||
@click.option(
|
||||
"--remove-unused",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Remove unused translations (keys present in the language but not required).",
|
||||
)
|
||||
@click.option(
|
||||
"--json",
|
||||
"as_json",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Print the completeness report as machine-readable JSON instead of rich tables.",
|
||||
)
|
||||
@option_verbose
|
||||
@option_dry_run
|
||||
@click.pass_context
|
||||
def check_translation_completeness(
|
||||
ctx: click.Context,
|
||||
language: str | None = None,
|
||||
add_missing: bool = False,
|
||||
remove_unused: bool = False,
|
||||
as_json: bool = False,
|
||||
):
|
||||
if as_json:
|
||||
ctx.meta[SKIP_LOCAL_REPRODUCTION] = True
|
||||
print_completeness_json(language, add_missing, remove_unused)
|
||||
return
|
||||
locale_files = get_locale_files()
|
||||
console = get_console()
|
||||
print_locale_file_table(locale_files, language)
|
||||
found_difference = print_file_set_differences(locale_files, language)
|
||||
summary, missing_counts = compare_keys(locale_files)
|
||||
console.print("\n[bold underline]Summary of differences by language:[/bold underline]", style="cyan")
|
||||
summary, missing_counts = apply_translation_modifications(
|
||||
locale_files, summary, missing_counts, language, add_missing, remove_unused
|
||||
)
|
||||
if language:
|
||||
locales = [lf.locale for lf in locale_files]
|
||||
if language not in locales:
|
||||
|
||||
@@ -32,6 +32,7 @@ UI_PARAMETERS: dict[str, list[dict[str, str | list[str]]]] = {
|
||||
"--language",
|
||||
"--add-missing",
|
||||
"--remove-unused",
|
||||
"--json",
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -17,13 +17,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from airflow_breeze.commands.ui_commands import (
|
||||
LocaleFiles,
|
||||
LocaleKeySet,
|
||||
LocaleSummary,
|
||||
build_completeness_report,
|
||||
check_translation_completeness,
|
||||
compare_keys,
|
||||
expand_plural_keys,
|
||||
flatten_keys,
|
||||
@@ -489,3 +493,268 @@ class TestNaturalSorting:
|
||||
)
|
||||
def test_natural_sort_key_matches_eslint(self, keys, expected):
|
||||
assert sorted(keys, key=natural_sort_key) == expected
|
||||
|
||||
|
||||
def _write_locale(tmp_path, locale: str, filename: str, data: dict) -> None:
|
||||
locale_dir = tmp_path / locale
|
||||
locale_dir.mkdir(exist_ok=True)
|
||||
(locale_dir / filename).write_text(json.dumps(data))
|
||||
|
||||
|
||||
class TestBuildCompletenessReport:
|
||||
def test_report_counts_and_key_lists(self, tmp_path):
|
||||
# en: 3 plain keys + a plural pair with {{count}}; de: one TODO, one missing, one unused
|
||||
_write_locale(
|
||||
tmp_path,
|
||||
"en",
|
||||
"test.json",
|
||||
{"a": "A", "b": "B", "c": "C", "n_one": "1 thing", "n_other": "{{count}} things"},
|
||||
)
|
||||
_write_locale(
|
||||
tmp_path,
|
||||
"de",
|
||||
"test.json",
|
||||
{"a": "A!", "b": "TODO: translate: B", "n_one": "Ding", "n_other": "Dinge", "zz": "extra"},
|
||||
)
|
||||
|
||||
import airflow_breeze.commands.ui_commands as ui_commands
|
||||
|
||||
original_locales_dir = ui_commands.LOCALES_DIR
|
||||
ui_commands.LOCALES_DIR = tmp_path
|
||||
try:
|
||||
locale_files = [
|
||||
LocaleFiles(locale="en", files=["test.json"]),
|
||||
LocaleFiles(locale="de", files=["test.json"]),
|
||||
]
|
||||
summary, _ = compare_keys(locale_files)
|
||||
report = build_completeness_report(locale_files, summary)
|
||||
|
||||
assert "en" not in report
|
||||
stats = report["de"]["files"]["test.json"]
|
||||
assert stats["required"] == 5
|
||||
assert stats["required_base_en"] == 5
|
||||
assert stats["required_plural"] == 0
|
||||
assert stats["translated"] == 3 # a, n_one, n_other; b is a TODO and c is missing
|
||||
assert stats["missing"] == 1
|
||||
assert stats["missing_keys"] == ["c"]
|
||||
assert stats["todos"] == 1
|
||||
assert stats["unused"] == 1
|
||||
assert stats["unused_keys"] == ["zz"]
|
||||
assert stats["coverage"] == pytest.approx(60.0)
|
||||
|
||||
totals = report["de"]["totals"]
|
||||
assert totals["required"] == 5
|
||||
assert totals["translated"] == 3
|
||||
assert totals["missing"] == 1
|
||||
assert totals["todos"] == 1
|
||||
assert totals["unused"] == 1
|
||||
assert totals["coverage"] == pytest.approx(60.0)
|
||||
finally:
|
||||
ui_commands.LOCALES_DIR = original_locales_dir
|
||||
|
||||
def test_report_expands_locale_specific_plural_forms(self, tmp_path):
|
||||
# pl requires _one/_few/_many/_other for a base whose EN value interpolates count
|
||||
_write_locale(tmp_path, "en", "test.json", {"n_one": "1 thing", "n_other": "{{count}} things"})
|
||||
_write_locale(tmp_path, "pl", "test.json", {"n_one": "rzecz", "n_other": "rzeczy"})
|
||||
|
||||
import airflow_breeze.commands.ui_commands as ui_commands
|
||||
|
||||
original_locales_dir = ui_commands.LOCALES_DIR
|
||||
ui_commands.LOCALES_DIR = tmp_path
|
||||
try:
|
||||
locale_files = [
|
||||
LocaleFiles(locale="en", files=["test.json"]),
|
||||
LocaleFiles(locale="pl", files=["test.json"]),
|
||||
]
|
||||
summary, _ = compare_keys(locale_files)
|
||||
report = build_completeness_report(locale_files, summary)
|
||||
|
||||
stats = report["pl"]["files"]["test.json"]
|
||||
assert stats["required_base_en"] == 2
|
||||
assert stats["required_plural"] == 2 # _few and _many on top of the EN base keys
|
||||
assert stats["required"] == 4
|
||||
assert stats["missing_keys"] == ["n_few", "n_many"]
|
||||
finally:
|
||||
ui_commands.LOCALES_DIR = original_locales_dir
|
||||
|
||||
def test_report_skips_files_without_english_source(self, tmp_path):
|
||||
_write_locale(tmp_path, "en", "test.json", {"a": "A"})
|
||||
_write_locale(tmp_path, "de", "test.json", {"a": "A!"})
|
||||
_write_locale(tmp_path, "de", "orphan.json", {"b": "B"})
|
||||
|
||||
import airflow_breeze.commands.ui_commands as ui_commands
|
||||
|
||||
original_locales_dir = ui_commands.LOCALES_DIR
|
||||
ui_commands.LOCALES_DIR = tmp_path
|
||||
try:
|
||||
locale_files = [
|
||||
LocaleFiles(locale="en", files=["test.json"]),
|
||||
LocaleFiles(locale="de", files=["test.json", "orphan.json"]),
|
||||
]
|
||||
summary, _ = compare_keys(locale_files)
|
||||
report = build_completeness_report(locale_files, summary)
|
||||
|
||||
assert list(report["de"]["files"]) == ["test.json"]
|
||||
finally:
|
||||
ui_commands.LOCALES_DIR = original_locales_dir
|
||||
|
||||
|
||||
class TestCheckTranslationCompletenessJson:
|
||||
@pytest.fixture
|
||||
def runner(self):
|
||||
return CliRunner()
|
||||
|
||||
def test_json_prints_a_single_pure_json_document(self, runner, tmp_path):
|
||||
_write_locale(tmp_path, "en", "test.json", {"a": "A", "b": "B"})
|
||||
_write_locale(tmp_path, "de", "test.json", {"a": "A!", "zz": "extra"})
|
||||
|
||||
import airflow_breeze.commands.ui_commands as ui_commands
|
||||
|
||||
original_locales_dir = ui_commands.LOCALES_DIR
|
||||
ui_commands.LOCALES_DIR = tmp_path
|
||||
try:
|
||||
result = runner.invoke(check_translation_completeness, ["--json"], catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
# The whole stdout must parse as JSON — nothing else may be printed around it.
|
||||
payload = json.loads(result.stdout)
|
||||
assert payload["complete"] is False
|
||||
stats = payload["languages"]["de"]["files"]["test.json"]
|
||||
assert stats["missing"] == 1
|
||||
assert stats["missing_keys"] == ["b"]
|
||||
assert stats["unused_keys"] == ["zz"]
|
||||
assert stats["coverage"] == 50.0
|
||||
assert payload["languages"]["de"]["totals"]["missing"] == 1
|
||||
finally:
|
||||
ui_commands.LOCALES_DIR = original_locales_dir
|
||||
|
||||
def test_json_reports_complete_when_nothing_is_missing(self, runner, tmp_path):
|
||||
data = {"a": "A", "b": "B"}
|
||||
_write_locale(tmp_path, "en", "test.json", data)
|
||||
_write_locale(tmp_path, "de", "test.json", {"a": "A!", "b": "B!"})
|
||||
|
||||
import airflow_breeze.commands.ui_commands as ui_commands
|
||||
|
||||
original_locales_dir = ui_commands.LOCALES_DIR
|
||||
ui_commands.LOCALES_DIR = tmp_path
|
||||
try:
|
||||
result = runner.invoke(check_translation_completeness, ["--json"], catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
payload = json.loads(result.stdout)
|
||||
assert payload["complete"] is True
|
||||
assert payload["languages"]["de"]["totals"]["coverage"] == 100.0
|
||||
finally:
|
||||
ui_commands.LOCALES_DIR = original_locales_dir
|
||||
|
||||
def test_json_complete_stays_false_while_todos_remain(self, runner, tmp_path):
|
||||
# The post---add-missing state: nothing missing, nothing unused, only TODO stubs.
|
||||
# The table verdict treats TODOs as incomplete, so the JSON flag must as well.
|
||||
_write_locale(tmp_path, "en", "test.json", {"a": "A", "b": "B"})
|
||||
_write_locale(tmp_path, "de", "test.json", {"a": "TODO: translate: A", "b": "TODO: translate: B"})
|
||||
|
||||
import airflow_breeze.commands.ui_commands as ui_commands
|
||||
|
||||
original_locales_dir = ui_commands.LOCALES_DIR
|
||||
ui_commands.LOCALES_DIR = tmp_path
|
||||
try:
|
||||
result = runner.invoke(check_translation_completeness, ["--json"], catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
payload = json.loads(result.stdout)
|
||||
totals = payload["languages"]["de"]["totals"]
|
||||
assert totals["missing"] == 0
|
||||
assert totals["unused"] == 0
|
||||
assert totals["todos"] == 2
|
||||
assert payload["complete"] is False
|
||||
finally:
|
||||
ui_commands.LOCALES_DIR = original_locales_dir
|
||||
|
||||
def test_json_with_language_filter_reports_only_that_language(self, runner, tmp_path):
|
||||
_write_locale(tmp_path, "en", "test.json", {"a": "A"})
|
||||
_write_locale(tmp_path, "de", "test.json", {"a": "A!"})
|
||||
_write_locale(tmp_path, "pl", "test.json", {"a": "A?"})
|
||||
|
||||
import airflow_breeze.commands.ui_commands as ui_commands
|
||||
|
||||
original_locales_dir = ui_commands.LOCALES_DIR
|
||||
ui_commands.LOCALES_DIR = tmp_path
|
||||
try:
|
||||
result = runner.invoke(
|
||||
check_translation_completeness, ["--json", "--language", "de"], catch_exceptions=False
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
payload = json.loads(result.stdout)
|
||||
assert list(payload["languages"]) == ["de"]
|
||||
finally:
|
||||
ui_commands.LOCALES_DIR = original_locales_dir
|
||||
|
||||
@pytest.mark.parametrize("language", ["xx", "en"])
|
||||
def test_json_exits_2_for_invalid_language_without_printing_json(self, runner, tmp_path, language):
|
||||
_write_locale(tmp_path, "en", "test.json", {"a": "A"})
|
||||
_write_locale(tmp_path, "de", "test.json", {"a": "A!"})
|
||||
|
||||
import airflow_breeze.commands.ui_commands as ui_commands
|
||||
|
||||
original_locales_dir = ui_commands.LOCALES_DIR
|
||||
ui_commands.LOCALES_DIR = tmp_path
|
||||
try:
|
||||
result = runner.invoke(check_translation_completeness, ["--json", "--language", language])
|
||||
assert result.exit_code == 2
|
||||
assert "{" not in result.stdout
|
||||
finally:
|
||||
ui_commands.LOCALES_DIR = original_locales_dir
|
||||
|
||||
def test_json_replays_fatal_error_output_to_stderr(self, runner, tmp_path):
|
||||
# A locale without a PLURAL_SUFFIXES entry aborts the checker; in --json mode the
|
||||
# suppressed narration must be replayed to stderr instead of vanishing with the exit.
|
||||
_write_locale(tmp_path, "en", "test.json", {"a": "A"})
|
||||
_write_locale(tmp_path, "xy", "test.json", {"a": "A!"})
|
||||
|
||||
import airflow_breeze.commands.ui_commands as ui_commands
|
||||
|
||||
original_locales_dir = ui_commands.LOCALES_DIR
|
||||
ui_commands.LOCALES_DIR = tmp_path
|
||||
try:
|
||||
result = runner.invoke(check_translation_completeness, ["--json"])
|
||||
assert result.exit_code == 1
|
||||
stderr_text = re.sub(r"\x1b\[[0-9;]*m", "", result.stderr)
|
||||
assert "No plural suffixes defined for language 'xy'" in stderr_text
|
||||
assert "{" not in result.stdout
|
||||
finally:
|
||||
ui_commands.LOCALES_DIR = original_locales_dir
|
||||
|
||||
def test_json_replays_narration_when_computation_raises(self, runner, tmp_path):
|
||||
# A malformed locale file makes the computation raise (not sys.exit) after
|
||||
# compare_keys() has narrated "Error loading ..."; the narration must surface
|
||||
# on stderr instead of vanishing with the suppression buffer.
|
||||
_write_locale(tmp_path, "en", "test.json", {"a": "A"})
|
||||
locale_dir = tmp_path / "de"
|
||||
locale_dir.mkdir(exist_ok=True)
|
||||
(locale_dir / "test.json").write_text("{not valid json")
|
||||
|
||||
import airflow_breeze.commands.ui_commands as ui_commands
|
||||
|
||||
original_locales_dir = ui_commands.LOCALES_DIR
|
||||
ui_commands.LOCALES_DIR = tmp_path
|
||||
try:
|
||||
result = runner.invoke(check_translation_completeness, ["--json"])
|
||||
assert result.exit_code == 1
|
||||
assert "{" not in result.stdout
|
||||
stderr_text = re.sub(r"\x1b\[[0-9;]*m", "", result.stderr)
|
||||
assert "Error loading" in stderr_text
|
||||
finally:
|
||||
ui_commands.LOCALES_DIR = original_locales_dir
|
||||
|
||||
def test_default_table_mode_still_prints_tables(self, runner, tmp_path):
|
||||
_write_locale(tmp_path, "en", "test.json", {"a": "A", "b": "B"})
|
||||
_write_locale(tmp_path, "de", "test.json", {"a": "A!"})
|
||||
|
||||
import airflow_breeze.commands.ui_commands as ui_commands
|
||||
|
||||
original_locales_dir = ui_commands.LOCALES_DIR
|
||||
ui_commands.LOCALES_DIR = tmp_path
|
||||
try:
|
||||
result = runner.invoke(check_translation_completeness, [], catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert "Translation Progress: de" in result.output
|
||||
assert "Some translations are not complete!" in result.output
|
||||
finally:
|
||||
ui_commands.LOCALES_DIR = original_locales_dir
|
||||
Reference in new issue
Block a user