Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2529,6 +2529,15 @@ def submit_pending(session_key: str, approval: dict) -> None:
"invalid_pw": "\ube44\ubc00\ubc88\ud638\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
"conn_failed": "\uc5f0\uacb0 \uc2e4\ud328",
},
"tr": {
"lang": "tr-TR",
"title": "Oturum a\u00e7",
"subtitle": "Devam etmek i\u00e7in \u015fifrenizi girin",
"placeholder": "\u015eifre",
"btn": "Oturum a\u00e7",
"invalid_pw": "Ge\u00e7ersiz \u015fifre",
"conn_failed": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z",
},
}


Expand Down
1,222 changes: 1,222 additions & 0 deletions static/i18n.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion tests/test_1560_password_env_var_no_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ def test_panels_js_uses_locked_placeholder_i18n_key():
# (en/es/de/zh/zh-Hant/ru/ja/fr/pt). The repo currently ships 9 locales but
# substitutes 'ko' for 'fr' — we test what the repo actually has, not what the
# issue body lists, so a future addition of fr won't fail the suite either.
EXPECTED_LOCALES = ("en", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko")
EXPECTED_LOCALES = ("en", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko", "tr")


def _locale_block(locale_key: str) -> str:
Expand Down
4 changes: 2 additions & 2 deletions tests/test_issue1488_composer_voice_buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class TestComposerVoiceButtonI18n:
"voice_mode_toggle_active",
)

LOCALES = ("en", "fr", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko")
LOCALES = ("en", "fr", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko", "tr")

def test_legacy_voice_toggle_key_removed(self):
"""The old key whose string was 'Voice input' caused the duplicate-
Expand Down Expand Up @@ -171,7 +171,7 @@ def test_english_voice_mode_label_is_voice_mode(self):
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")
LOCALES = ("en", "fr", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko", "tr")

def test_voice_mode_pref_is_localstorage_backed(self):
"""The pref reads from localStorage key 'hermes-voice-mode-button'."""
Expand Down
4 changes: 2 additions & 2 deletions tests/test_issue2419_cache_usage_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:") == 11
assert src.count("usage_cached_percent:") == 11
assert src.count("usage_cache_hit_detail:") == 12
assert src.count("usage_cached_percent:") == 12
assert "usage_cache_hit_detail: 'Cache: {0}% hit ({1} read / {2} write)'" in src
assert "usage_cached_percent: '{0}% cached'" in src

Expand Down
2 changes: 1 addition & 1 deletion tests/test_issue2462_theme_i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def test_theme_command_help_mentions_current_theme_and_skin_values():
"system/dark/light",
"default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast",
)
for locale in ("en", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko", "fr"):
for locale in ("en", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko", "fr", "tr"):
value = _literal_value(_locale_block(locale), "cmd_theme")
for fragment in required_fragments:
assert fragment in value, f"{locale} cmd_theme missing {fragment!r}: {value!r}"
Expand Down
4 changes: 2 additions & 2 deletions tests/test_issue2679_hide_suggestions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:") == 11
assert js.count("settings_desc_hide_suggestions:") == 11
assert js.count("settings_label_hide_suggestions:") == 12
assert js.count("settings_desc_hide_suggestions:") == 12
changelog = CHANGELOG.read_text(encoding="utf-8")
assert "#2679" in changelog
assert "hide_empty_state_suggestions" in changelog
4 changes: 2 additions & 2 deletions tests/test_pr1721_rtl_salvage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:") == 11
assert js.count("settings_desc_rtl:") == 11
assert js.count("settings_label_rtl:") == 12
assert js.count("settings_desc_rtl:") == 12
4 changes: 2 additions & 2 deletions tests/test_quota_chip_settings_toggle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:") == 11, "11 locales expected"
assert js.count("settings_desc_quota_chip:") == 11, "11 locales expected"
assert js.count("settings_label_quota_chip:") == 12, "12 locales expected"
assert js.count("settings_desc_quota_chip:") == 12, "12 locales expected"
4 changes: 2 additions & 2 deletions tests/test_sidebar_tab_visibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ def test_i18n_coverage():
"""Label and description keys must exist in all locales with matching counts."""
label_count = I18N_JS.count("settings_label_tab_visibility")
desc_count = I18N_JS.count("settings_desc_tab_visibility")
assert label_count >= 11, f"Expected ≥11 locales, found {label_count}"
assert desc_count >= 11, f"Expected ≥11 locales, found {desc_count}"
assert label_count >= 12, f"Expected ≥12 locales, found {label_count}"
assert desc_count >= 12, f"Expected ≥12 locales, found {desc_count}"
assert label_count == desc_count, \
f"Label ({label_count}) and desc ({desc_count}) counts must match"

Expand Down
155 changes: 155 additions & 0 deletions tests/test_turkish_locale.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
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 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"

start = start_match.end() - 1
depth = 0
in_single = False
in_double = False
in_backtick = False
escape = False

for i in range(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[start + 1 : i]

raise AssertionError(f"{locale_key} locale block braces are not balanced")


def locale_keys(src: str, locale_key: str) -> list[str]:
key_pattern = re.compile(r"^\s*([a-zA-Z0-9_]+)\s*:", re.MULTILINE)
return key_pattern.findall(extract_locale_block(src, locale_key))


def test_turkish_locale_block_exists():
src = read(REPO / "static" / "i18n.js")
tr_block = extract_locale_block(src, "tr")
assert tr_block
assert "_lang: 'tr'" in tr_block
assert "_label: 'Türkçe'" in tr_block
assert "_speech: 'tr-TR'" in tr_block


def test_turkish_locale_includes_representative_translations():
src = read(REPO / "static" / "i18n.js")
tr_block = extract_locale_block(src, "tr")
expected = [
"settings_title: 'Ayarlar'",
"settings_label_language: 'Dil'",
"login_title: 'Oturum aç'",
"approval_heading: 'Onay gerekli'",
"tab_chat: 'Sohbet'",
"tab_tasks: 'Görevler'",
"tab_profiles: 'Agent profilleri'",
"empty_title: 'Hangi konuda yardımcı olabilirim?'",
"onboarding_title: 'Hermes Web Kullanıcı Arayüzüne Hoş Geldiniz'",
]
for entry in expected:
assert entry in tr_block


def test_turkish_settings_detail_descriptions_are_translated():
src = read(REPO / "static" / "i18n.js")
tr_block = extract_locale_block(src, "tr")
expected = [
"settings_desc_workspace_panel_open: 'Etkinleştirildiğinde, çalışma alanı / dosya tarayıcı paneli her yeni oturumda otomatik olarak açılır. Yine de istediğiniz zaman manuel olarak kapatabilirsiniz.'",
"settings_desc_notifications: 'Uygulama arka plandayken bir yanıt tamamlandığında bir sistem bildirimi gösterin.'",
"settings_desc_token_usage: 'Her Asistan yanıtının altında giriş/çıkış jeton sayılarını gösterir. /usage ile de değiştirilebilir.'",
"settings_desc_sidebar_density: 'Oturum listesinin sol kenar çubuğunda ne kadar meta veri göstereceğini kontrol eder.'",
"settings_desc_auto_title_refresh: 'Oturum başlıklarını en son konuşmaya göre otomatik olarak yeniden oluşturarak konuşma ilerledikçe başlıkların alakalı kalmasını sağlar. LLM başlık oluşturma modeli yapılandırması gerektirir.'",
"settings_desc_external_sessions: 'Oturum listesinde CLI, Telegram, Discord, Slack ve diğer kanallardan gelen konuşmaları gösterin. İçe aktarmak ve devam etmek için tıklayın.'",
"settings_desc_sync_insights: 'WebUI belirteci kullanımını state.db\\'ye yansıtır, böylece hermes /insights tarayıcı oturum verilerini içerir. Varsayılan olarak kapalıdır.'",
"settings_desc_check_updates: 'WebUI veya Agent\\'ın daha yeni sürümleri mevcut olduğunda bir banner gösterin. Periyodik olarak bir arka plan git getirme işlemi çalıştırır.'",
"settings_desc_bot_name: 'Yalnızca varsayılan profil için kullanılır. Diğer profiller kendi profil adlarını kullanır.'",
"settings_desc_password: 'Ayarlamak veya değiştirmek için yeni bir şifre girin. Geçerli ayarı korumak için boş bırakın.'",
]
for entry in expected:
assert entry in tr_block


def test_turkish_locale_matches_english_key_coverage():
src = read(REPO / "static" / "i18n.js")
en_keys = set(locale_keys(src, "en"))
tr_keys = set(locale_keys(src, "tr"))
assert sorted(en_keys - tr_keys) == []
assert sorted(tr_keys - en_keys) == []


def test_turkish_locale_has_no_duplicate_keys():
src = read(REPO / "static" / "i18n.js")
keys = locale_keys(src, "tr")
duplicates = sorted(k for k, count in Counter(keys).items() if count > 1)
assert not duplicates, f"Turkish locale has duplicate keys: {duplicates}"


def test_turkish_locale_keys_use_standard_indentation():
src = read(REPO / "static" / "i18n.js")
tr_block = extract_locale_block(src, "tr")
badly_indented = [
line.strip()
for line in tr_block.splitlines()
if re.match(r"^\s{1,3}[a-zA-Z0-9_]+\s*:", line)
]
assert badly_indented == []


def test_turkish_locale_has_no_double_escaped_unicode_sequences():
"""JSON-style double escapes (\\\\u2026) render literal backslash-u in the UI."""
src = read(REPO / "static" / "i18n.js")
tr_block = extract_locale_block(src, "tr")
for bad in ("\\\\u2026", "\\\\u2192", "\\\\u2713"):
assert bad not in tr_block, f"Turkish locale must not contain {bad!r}"
Loading