Merge remote-tracking branch 'origin/master' into fix/4768-cron-graceful-degrade
This commit is contained in:
commit
f4e150e105
17 files changed
+1764
-22
No files matched your search
@@ -3,6 +3,12 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.621] — 2026-06-24 — Release WB (Vietnamese UI language)
|
||||
|
||||
### Added
|
||||
|
||||
- **Vietnamese (Tiếng Việt) is now a selectable UI language.** Adds a full `vi` locale to the language picker, including the login page. Thanks @thanhtantran. (#4241)
|
||||
|
||||
## [v0.51.620] — 2026-06-24 — Release WA (fix /api/sessions CPU spike + slowness during streaming)
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -7052,6 +7052,15 @@ _LOGIN_LOCALE = {
|
||||
"invalid_pw": "Nieprawid\u0142owe has\u0142o",
|
||||
"conn_failed": "Po\u0142\u0105czenie nie powiod\u0142o si\u0119",
|
||||
},
|
||||
"vi": {
|
||||
"lang": "vi",
|
||||
"title": "\u0110\u0103ng nh\u1eadp",
|
||||
"subtitle": "Nh\u1eadp m\u1eadt kh\u1ea9u c\u1ee7a b\u1ea1n \u0111\u1ec3 ti\u1ebfp t\u1ee5c",
|
||||
"placeholder": "M\u1eadt kh\u1ea9u",
|
||||
"btn": "\u0110\u0103ng nh\u1eadp",
|
||||
"invalid_pw": "M\u1eadt kh\u1ea9u kh\u00f4ng h\u1ee3p l\u1ec7",
|
||||
"conn_failed": "K\u1ebft n\u1ed1i th\u1ea5t b\u1ea1i",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1615
File diff suppressed because it is too large.
Load diff
@@ -229,7 +229,7 @@ class TestOpenInVsCodeI18n:
|
||||
"""open_in_vscode key must appear exactly once per locale (11 total)."""
|
||||
src = I18N.read_text(encoding="utf-8")
|
||||
count = src.count("open_in_vscode:")
|
||||
assert count == 12, (
|
||||
assert count == 13, (
|
||||
f"Expected 11 open_in_vscode: entries (one per locale), found {count}"
|
||||
)
|
||||
|
||||
@@ -237,7 +237,7 @@ class TestOpenInVsCodeI18n:
|
||||
"""open_in_vscode_failed key must appear exactly once per locale (11 total)."""
|
||||
src = I18N.read_text(encoding="utf-8")
|
||||
count = src.count("open_in_vscode_failed:")
|
||||
assert count == 12, (
|
||||
assert count == 13, (
|
||||
f"Expected 11 open_in_vscode_failed: entries (one per locale), found {count}"
|
||||
)
|
||||
|
||||
|
||||
@@ -305,8 +305,8 @@ class TestAuxiliaryModelsI18n:
|
||||
"""Count of each key should equal the number of locales (12 with Turkish)."""
|
||||
for key in self.REQUIRED_KEYS:
|
||||
count = I18N_JS.count(f"{key}:")
|
||||
assert count == 13, (
|
||||
f"i18n key '{key}' found {count} times — expected 12 (one per locale)"
|
||||
assert count == 14, (
|
||||
f"i18n key '{key}' found {count} times — expected 14 (one per locale)"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ class TestComposerVoiceButtonI18n:
|
||||
"voice_mode_toggle_active",
|
||||
)
|
||||
|
||||
LOCALES = ("en", "fr", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko", "tr", "pl")
|
||||
LOCALES = ("en", "fr", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko", "tr", "pl", "vi")
|
||||
|
||||
def test_legacy_voice_toggle_key_removed(self):
|
||||
"""The old key whose string was 'Voice input' caused the duplicate-
|
||||
@@ -171,7 +171,7 @@ class TestComposerVoiceButtonI18n:
|
||||
class TestVoiceModePreferenceGate:
|
||||
"""boot.js must hide btnVoiceMode by default, surface it via Preferences."""
|
||||
|
||||
LOCALES = ("en", "fr", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko", "tr", "pl")
|
||||
LOCALES = ("en", "fr", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko", "tr", "pl", "vi")
|
||||
|
||||
def test_voice_mode_pref_is_localstorage_backed(self):
|
||||
"""The pref reads from localStorage key 'hermes-voice-mode-button'."""
|
||||
|
||||
@@ -63,8 +63,8 @@ def test_context_indicator_surfaces_cache_hit_rate():
|
||||
def test_cache_usage_labels_are_localized():
|
||||
src = (ROOT / "static" / "i18n.js").read_text()
|
||||
|
||||
assert src.count("usage_cache_hit_detail:") == 13
|
||||
assert src.count("usage_cached_percent:") == 13
|
||||
assert src.count("usage_cache_hit_detail:") == 14
|
||||
assert src.count("usage_cached_percent:") == 14
|
||||
assert "usage_cache_hit_detail: 'Cache: {0}% hit ({1} read / {2} write)'" in src
|
||||
assert "usage_cached_percent: '{0}% cached'" in src
|
||||
|
||||
|
||||
@@ -50,8 +50,8 @@ def test_panels_round_trip_and_hot_apply_hide_suggestions():
|
||||
|
||||
def test_hide_suggestions_i18n_all_locales_and_changelog():
|
||||
js = I18N.read_text(encoding="utf-8")
|
||||
assert js.count("settings_label_hide_suggestions:") == 13
|
||||
assert js.count("settings_desc_hide_suggestions:") == 13
|
||||
assert js.count("settings_label_hide_suggestions:") == 14
|
||||
assert js.count("settings_desc_hide_suggestions:") == 14
|
||||
changelog = CHANGELOG.read_text(encoding="utf-8")
|
||||
assert "#2679" in changelog
|
||||
assert "hide_empty_state_suggestions" in changelog
|
||||
@@ -6,7 +6,7 @@ PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
|
||||
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
|
||||
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
||||
|
||||
LOCALE_COUNT = 13 # en, it, ja, ru, es, de, zh, zh-TW, pt, ko, fr, tr, pl
|
||||
LOCALE_COUNT = 14 # en, it, ja, ru, es, de, zh, zh-TW, pt, ko, fr, tr, pl
|
||||
|
||||
|
||||
def test_help_nav_button_present():
|
||||
|
||||
@@ -5,7 +5,7 @@ BOOT_JS = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
||||
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
|
||||
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
||||
|
||||
LOCALE_COUNT = 13
|
||||
LOCALE_COUNT = 14
|
||||
|
||||
|
||||
def test_raw_audio_active_recording_uses_dedicated_i18n_key():
|
||||
|
||||
@@ -76,8 +76,8 @@ def test_panels_round_trip_and_hot_apply_virtualize_toggle():
|
||||
|
||||
def test_virtualize_toggle_i18n_all_locales():
|
||||
js = I18N.read_text(encoding="utf-8")
|
||||
assert js.count("settings_label_virtualize_transcript:") == 13
|
||||
assert js.count("settings_desc_virtualize_transcript:") == 13
|
||||
assert js.count("settings_label_virtualize_transcript:") == 14
|
||||
assert js.count("settings_desc_virtualize_transcript:") == 14
|
||||
|
||||
|
||||
# ── #4343 force-off-for-everyone migration (load_settings behavior) ──────────
|
||||
|
||||
@@ -80,10 +80,10 @@ def test_panels_has_no_git_branch():
|
||||
|
||||
|
||||
def test_i18n_no_git_key_all_locales():
|
||||
"""settings_update_no_git key appears in all 13 locale blocks in static/i18n.js."""
|
||||
"""settings_update_no_git key appears in all 14 locale blocks in static/i18n.js."""
|
||||
i18n_path = Path(__file__).resolve().parent.parent / 'static' / 'i18n.js'
|
||||
content = i18n_path.read_text()
|
||||
|
||||
# Count occurrences of the new key (should be exactly 13, one per locale)
|
||||
count = content.count("settings_update_no_git")
|
||||
assert count == 13, f"Expected exactly 13 settings_update_no_git keys, found {count}"
|
||||
assert count == 14, f"Expected exactly 14 settings_update_no_git keys, found {count}"
|
||||
@@ -112,5 +112,5 @@ def test_rtl_in_config_defaults_and_writable_keys():
|
||||
def test_rtl_localized_in_all_locales():
|
||||
js = I18N.read_text(encoding="utf-8")
|
||||
# Count occurrences — should match the 11 locale blocks
|
||||
assert js.count("settings_label_rtl:") == 13
|
||||
assert js.count("settings_desc_rtl:") == 13
|
||||
assert js.count("settings_label_rtl:") == 14
|
||||
assert js.count("settings_desc_rtl:") == 14
|
||||
@@ -88,5 +88,5 @@ def test_quota_chip_panels_round_trip():
|
||||
|
||||
def test_quota_chip_localized_in_all_locales():
|
||||
js = I18N.read_text(encoding="utf-8")
|
||||
assert js.count("settings_label_quota_chip:") == 13, "12 locales expected"
|
||||
assert js.count("settings_desc_quota_chip:") == 13, "12 locales expected"
|
||||
assert js.count("settings_label_quota_chip:") == 14, "12 locales expected"
|
||||
assert js.count("settings_desc_quota_chip:") == 14, "12 locales expected"
|
||||
@@ -38,4 +38,4 @@ def test_verdigris_has_no_light_variant():
|
||||
def test_verdigris_i18n_lists_skin_in_all_locales():
|
||||
# There are 12 locales; each should now include verdigris as the trailing skin.
|
||||
# 10 locales use ASCII closing paren, 2 Chinese locales use full-width paren.
|
||||
assert I18N_JS.count("verdigris)") + I18N_JS.count("verdigris)") == 13
|
||||
assert I18N_JS.count("verdigris)") + I18N_JS.count("verdigris)") == 14
|
||||
@@ -0,0 +1,112 @@
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def read(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_vietnamese_locale_block_exists():
|
||||
src = read(REPO / "static" / "i18n.js")
|
||||
assert "\n _lang: 'vi'," in src
|
||||
assert "_label: 'Tiếng Việt'" in src
|
||||
assert "_speech: 'vi-VN'" in src
|
||||
|
||||
|
||||
def extract_locale_block(src: str, locale_key: str) -> str:
|
||||
start_match = re.search(rf"\b{re.escape(locale_key)}\s*:\s*\{{", src)
|
||||
assert start_match, f"{locale_key} locale block not found"
|
||||
|
||||
brace_start = start_match.end() - 1
|
||||
depth = 0
|
||||
in_single = False
|
||||
in_double = False
|
||||
in_backtick = False
|
||||
escape = False
|
||||
|
||||
for i in range(brace_start, len(src)):
|
||||
ch = src[i]
|
||||
|
||||
if escape:
|
||||
escape = False
|
||||
continue
|
||||
|
||||
if in_single:
|
||||
if ch == "\\":
|
||||
escape = True
|
||||
elif ch == "'":
|
||||
in_single = False
|
||||
continue
|
||||
|
||||
if in_double:
|
||||
if ch == "\\":
|
||||
escape = True
|
||||
elif ch == '"':
|
||||
in_double = False
|
||||
continue
|
||||
|
||||
if in_backtick:
|
||||
if ch == "\\":
|
||||
escape = True
|
||||
elif ch == "`":
|
||||
in_backtick = False
|
||||
continue
|
||||
|
||||
if ch == "'":
|
||||
in_single = True
|
||||
continue
|
||||
if ch == '"':
|
||||
in_double = True
|
||||
continue
|
||||
if ch == "`":
|
||||
in_backtick = True
|
||||
continue
|
||||
|
||||
if ch == "{":
|
||||
depth += 1
|
||||
continue
|
||||
if ch == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return src[brace_start + 1 : i]
|
||||
|
||||
raise AssertionError(f"{locale_key} locale block braces are not balanced")
|
||||
|
||||
|
||||
def test_vietnamese_locale_includes_representative_translations():
|
||||
src = read(REPO / "static" / "i18n.js")
|
||||
vi_block = extract_locale_block(src, "vi")
|
||||
expected = [
|
||||
"settings_heading_title: 'Trung tâm điều khiển'",
|
||||
"settings_heading_subtitle: 'Tùy chọn, công cụ hội thoại và điều khiển hệ thống.'",
|
||||
"approval_skip_all: '⚡ Bỏ qua tất cả trong phiên này'",
|
||||
"checkpoint_title: 'Checkpoint'",
|
||||
"composer_send: 'Gửi tin nhắn'",
|
||||
"gateway_restart: 'Khởi động lại'",
|
||||
"wiki_browse: 'Duyệt wiki'",
|
||||
"yolo_pill_title_active: 'Chế độ YOLO đang bật — bấm để tắt'",
|
||||
]
|
||||
for entry in expected:
|
||||
assert entry in vi_block
|
||||
|
||||
|
||||
def test_vietnamese_locale_covers_english_keys():
|
||||
src = read(REPO / "static" / "i18n.js")
|
||||
key_pattern = re.compile(r"^\s{4}([a-zA-Z0-9_]+):", re.MULTILINE)
|
||||
en_keys = set(key_pattern.findall(extract_locale_block(src, "en")))
|
||||
vi_keys = set(key_pattern.findall(extract_locale_block(src, "vi")))
|
||||
|
||||
missing = sorted(en_keys - vi_keys)
|
||||
assert not missing, f"Vietnamese locale missing keys: {missing}"
|
||||
|
||||
|
||||
def test_vietnamese_locale_has_no_duplicate_keys():
|
||||
src = read(REPO / "static" / "i18n.js")
|
||||
key_pattern = re.compile(r"^\s{4}([a-zA-Z0-9_]+):", re.MULTILINE)
|
||||
keys = key_pattern.findall(extract_locale_block(src, "vi"))
|
||||
duplicates = sorted(k for k, count in Counter(keys).items() if count > 1)
|
||||
assert not duplicates, f"Vietnamese locale has duplicate keys: {duplicates}"
|
||||
@@ -45,4 +45,4 @@ def test_zeus_modals_are_not_navy():
|
||||
|
||||
def test_zeus_i18n_lists_skin_in_all_locales():
|
||||
# There are 12 locales; each should include zeus in the skin list.
|
||||
assert I18N_JS.count("/zeus/") == 13
|
||||
assert I18N_JS.count("/zeus/") == 14
|
||||
Reference in new issue
Block a user