feat(i18n): add Turkish (tr) locale support
Add a complete Turkish locale to the WebUI and login page so users can select Türkçe in Settings, with speech recognition via tr-TR. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
af1d26abd0
commit
c77936ff81
11 files changed
+1386
-14
No files matched your search
@@ -2561,6 +2561,15 @@ _LOGIN_LOCALE = {
|
||||
"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",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1217
File diff suppressed because it is too large.
Load diff
@@ -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:
|
||||
|
||||
@@ -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-
|
||||
@@ -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")
|
||||
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'."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
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")
|
||||
assert "\n tr: {" in src
|
||||
assert "_lang: 'tr'" in src
|
||||
assert "_label: 'Türkçe'" in src
|
||||
assert "_speech: 'tr-TR'" in src
|
||||
|
||||
|
||||
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 == []
|
||||
Reference in new issue
Block a user