This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-4-test by this push:
new ae835335730 [v3-4-test] Fix breeze translation key sorting to match
the UI ESLint rule (#75065) (#75192)
ae835335730 is described below
commit ae83533573048085c4d08afe00249adb68328251
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Oct 20 14:31:34 2026 +0530
[v3-4-test] Fix breeze translation key sorting to match the UI ESLint rule
(#75065) (#75192)
(cherry picked from commit bd29fed01b684acffcbe569ae137eddc753f5a80)
Co-authored-by: CA <[email protected]>
---
.../src/airflow_breeze/commands/ui_commands.py | 65 +++++++++++++---------
dev/breeze/tests/test_ui_commands.py | 28 +++++++---
2 files changed, 60 insertions(+), 33 deletions(-)
diff --git a/dev/breeze/src/airflow_breeze/commands/ui_commands.py
b/dev/breeze/src/airflow_breeze/commands/ui_commands.py
index df906c2f7fd..a7820963031 100644
--- a/dev/breeze/src/airflow_breeze/commands/ui_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/ui_commands.py
@@ -35,37 +35,50 @@ from airflow_breeze.utils.run_utils import
assert_prek_installed, run_compile_ui
LOCALES_DIR = AIRFLOW_ROOT_PATH / "airflow-core" / "src" / "airflow" / "ui" /
"public" / "i18n" / "locales"
-def natural_sort_key(text: str) -> tuple:
- """
- Create a sort key that matches eslint-plugin-jsonc behavior with natural:
true.
+# natural-compare's sort order of ASCII: NUL-"," keep their ASCII order, then
"." "/",
+# the remaining punctuation, "-" (just before digits), digits, uppercase,
lowercase.
+# A character's position in this string is its natural-compare sort code.
+_NATURAL_CHAR_ORDER = (
+ "".join(chr(code) for code in range(32)) # control characters
+ + " !\"#$%&'()*+,"
+ + "./"
+ + ":;<=>?@"
+ + "[\\]^_`"
+ + "{|}~\x7f"
+ + "-"
+ + "0123456789"
+ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ + "abcdefghijklmnopqrstuvwxyz"
+)
+_CHAR_CODES = {char: code for code, char in enumerate(_NATURAL_CHAR_ORDER)}
- This mimics JavaScript's localeCompare with numeric: true, which:
- 1. Does case-insensitive comparison character-by-character
- 2. Uses original case as tiebreaker when characters are equal (ignoring
case)
- 3. Handles numbers naturally (2 < 10)
- For each character position, we create a tuple: (lowercase_char,
original_char)
- This ensures case-insensitive primary sort with case-sensitive tiebreaker.
- """
+def _get_char_code(char: str) -> int:
+ """Return natural-compare's sort code for a character; non-ASCII by code
point."""
+ return _CHAR_CODES.get(char, ord(char))
- def char_key(c: str) -> tuple:
- if c.isdigit():
- return 0, int(c), c # Numbers sort before letters
- return 1, c.lower(), c # Lowercase for primary, original for
tiebreaker
- # Split on numbers to handle natural number ordering
- parts: list[tuple[int, tuple[tuple[Any, ...], ...] | int]] = []
- for part in re.split(r"(\d+)", text):
- if not part:
- continue
- if part.isdigit():
- # For numeric parts, use integer value
- parts.append((0, int(part)))
- else:
- # For text parts, convert each character
- parts.append((1, tuple(char_key(c) for c in part)))
+def natural_sort_key(text: str) -> tuple:
+ """Create a sort key matching eslint-plugin-jsonc with natural: true.
- return tuple(parts)
+ natural-compare, the comparator behind the UI's ``jsonc/sort-keys`` rule,
compares
+ digit runs numerically and other characters by ``_get_char_code``. Each
token also
+ carries a bucket so numbers sort between characters below "0" and above
"9".
+ """
+ tokens: list[tuple] = []
+ for part in re.split(r"([0-9]+)", text):
+ if part.isascii() and part.isdigit():
+ digits = part.lstrip("0")
+ # natural-compare treats a leading "0" as a plain character and
only the
+ # rest of the run as a number.
+ tokens.extend([(0, _get_char_code("0"))] * (len(part) -
len(digits)))
+ if digits:
+ tokens.append((1, int(digits)))
+ else:
+ for char in part:
+ code = _get_char_code(char)
+ tokens.append((0, code) if code < 66 else (2, code))
+ return tuple(tokens)
MOST_COMMON_PLURAL_SUFFIXES = ["_one", "_other"]
diff --git a/dev/breeze/tests/test_ui_commands.py
b/dev/breeze/tests/test_ui_commands.py
index 0bb68978a93..1aab1edd0de 100644
--- a/dev/breeze/tests/test_ui_commands.py
+++ b/dev/breeze/tests/test_ui_commands.py
@@ -29,6 +29,7 @@ from airflow_breeze.commands.ui_commands import (
flatten_keys,
get_locale_files,
get_plural_base,
+ natural_sort_key,
)
@@ -429,7 +430,7 @@ class TestRemoveUnusedTranslations:
class TestNaturalSorting:
def test_natural_sort_matches_eslint(self, tmp_path):
- """Test that keys are sorted like eslint-plugin-jsonc with natural:
true (case-insensitive with case-sensitive tiebreaker)."""
+ """Test that keys are sorted like eslint-plugin-jsonc with natural:
true."""
from airflow_breeze.commands.ui_commands import
add_missing_translations
en_dir = tmp_path / "en"
@@ -464,14 +465,27 @@ class TestNaturalSorting:
)
add_missing_translations("de", {"test.json": summary})
- # Check that keys are sorted using natural sort (case-insensitive
with case-sensitive tiebreaker)
+ # Check that keys are sorted like eslint-plugin-jsonc with
natural: true
de_data_updated = json.loads((de_dir / "test.json").read_text())
keys = list(de_data_updated.keys())
- # Expected order matches eslint-plugin-jsonc with natural: true:
- # - "apple" < "asset_few" < "assetEvent_few" (case-insensitive:
apple < asset < assetevent)
- # - "parseDuration" < "parsedAt" (case-insensitive equal at
"parse", then D < d at position 5)
- # - "Zebra" at the end (case-insensitive: z comes last)
- assert keys == ["apple", "asset_few", "assetEvent_few",
"parseDuration", "parsedAt", "Zebra"]
+ # natural-compare sorts uppercase before lowercase, so "Zebra"
comes first.
+ assert keys == ["Zebra", "apple", "asset_few", "assetEvent_few",
"parseDuration", "parsedAt"]
finally:
ui_commands.LOCALES_DIR = original_locales_dir
+
+ @pytest.mark.parametrize(
+ ("keys", "expected"),
+ [
+ # Real key pairs from the UI locales where a case-insensitive sort
+ # disagrees with natural-compare's case-sensitive ordering.
+ (["logicalDate", "logLevel"], ["logLevel", "logicalDate"]),
+ (["selected", "selectLanguage"], ["selectLanguage", "selected"]),
+ (["modes", "modeTooltip"], ["modeTooltip", "modes"]),
+ (["sortedAscending", "sortMultiColumnHint"],
["sortMultiColumnHint", "sortedAscending"]),
+ # Digit runs sort numerically, except runs starting with "0".
+ (["a2", "a10", "a02"], ["a02", "a2", "a10"]),
+ ],
+ )
+ def test_natural_sort_key_matches_eslint(self, keys, expected):
+ assert sorted(keys, key=natural_sort_key) == expected