fix i18n review comments and locale test robustness
This commit is contained in:
committed by
Nathan Esquenazi
parent
c4efe96725
commit
204dc23c6b
@@ -233,7 +233,7 @@ _LOGIN_LOCALE = {
|
|||||||
"title": "Iniciar sesi\u00f3n",
|
"title": "Iniciar sesi\u00f3n",
|
||||||
"subtitle": "Introduce tu contrase\u00f1a para continuar",
|
"subtitle": "Introduce tu contrase\u00f1a para continuar",
|
||||||
"placeholder": "Contrase\u00f1a",
|
"placeholder": "Contrase\u00f1a",
|
||||||
"btn": "Iniciar sesi\u00f3n",
|
"btn": "Entrar",
|
||||||
"invalid_pw": "Contrase\u00f1a inv\u00e1lida",
|
"invalid_pw": "Contrase\u00f1a inv\u00e1lida",
|
||||||
"conn_failed": "Error de conexi\u00f3n",
|
"conn_failed": "Error de conexi\u00f3n",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -584,7 +584,45 @@ function applyBotName(){
|
|||||||
(async()=>{
|
(async()=>{
|
||||||
// Load send key preference
|
// Load send key preference
|
||||||
let _bootSettings={};
|
let _bootSettings={};
|
||||||
try{const s=await api('/api/settings');_bootSettings=s;window._sendKey=s.send_key||'enter';window._showTokenUsage=!!s.show_token_usage;window._showCliSessions=!!s.show_cli_sessions;window._soundEnabled=!!s.sound_enabled;window._notificationsEnabled=!!s.notifications_enabled;window._botName=s.bot_name||'Hermes';const _theme=s.theme||'dark';document.documentElement.dataset.theme=_theme;localStorage.setItem('hermes-theme',_theme);document.body.classList.toggle('bubble-layout',!!s.bubble_layout);if(typeof setLocale==='function'){const _lang=typeof resolvePreferredLocale==='function'?resolvePreferredLocale(s.language,localStorage.getItem('hermes-lang')):(s.language||localStorage.getItem('hermes-lang')||'en');setLocale(_lang);if(typeof applyLocaleToDOM==='function')applyLocaleToDOM();}applyBotName();}catch(e){window._sendKey='enter';window._showTokenUsage=false;window._showCliSessions=false;window._soundEnabled=false;window._notificationsEnabled=false;window._botName='Hermes';_bootSettings={check_for_updates:false};document.body.classList.remove('bubble-layout');}
|
try{
|
||||||
|
const s=await api('/api/settings');
|
||||||
|
_bootSettings=s;
|
||||||
|
window._sendKey=s.send_key||'enter';
|
||||||
|
window._showTokenUsage=!!s.show_token_usage;
|
||||||
|
window._showCliSessions=!!s.show_cli_sessions;
|
||||||
|
window._soundEnabled=!!s.sound_enabled;
|
||||||
|
window._notificationsEnabled=!!s.notifications_enabled;
|
||||||
|
window._botName=s.bot_name||'Hermes';
|
||||||
|
const _theme=s.theme||'dark';
|
||||||
|
document.documentElement.dataset.theme=_theme;
|
||||||
|
localStorage.setItem('hermes-theme',_theme);
|
||||||
|
document.body.classList.toggle('bubble-layout',!!s.bubble_layout);
|
||||||
|
if(typeof setLocale==='function'){
|
||||||
|
const _lang=typeof resolvePreferredLocale==='function'
|
||||||
|
? resolvePreferredLocale(s.language, localStorage.getItem('hermes-lang'))
|
||||||
|
: (s.language || localStorage.getItem('hermes-lang') || 'en');
|
||||||
|
setLocale(_lang);
|
||||||
|
if(typeof applyLocaleToDOM==='function')applyLocaleToDOM();
|
||||||
|
}
|
||||||
|
applyBotName();
|
||||||
|
}catch(e){
|
||||||
|
window._sendKey='enter';
|
||||||
|
window._showTokenUsage=false;
|
||||||
|
window._showCliSessions=false;
|
||||||
|
window._soundEnabled=false;
|
||||||
|
window._notificationsEnabled=false;
|
||||||
|
window._botName='Hermes';
|
||||||
|
_bootSettings={check_for_updates:false};
|
||||||
|
document.body.classList.remove('bubble-layout');
|
||||||
|
if(typeof setLocale==='function'){
|
||||||
|
const _lang=typeof resolvePreferredLocale==='function'
|
||||||
|
? resolvePreferredLocale(null, localStorage.getItem('hermes-lang'))
|
||||||
|
: (localStorage.getItem('hermes-lang') || 'en');
|
||||||
|
setLocale(_lang);
|
||||||
|
if(typeof applyLocaleToDOM==='function')applyLocaleToDOM();
|
||||||
|
}
|
||||||
|
applyBotName();
|
||||||
|
}
|
||||||
// Non-blocking update check (fire-and-forget, once per tab session)
|
// Non-blocking update check (fire-and-forget, once per tab session)
|
||||||
// ?test_updates=1 in URL forces banner display for testing (bypasses sessionStorage guards)
|
// ?test_updates=1 in URL forces banner display for testing (bypasses sessionStorage guards)
|
||||||
const _testUpdates=new URLSearchParams(location.search).get('test_updates')==='1';
|
const _testUpdates=new URLSearchParams(location.search).get('test_updates')==='1';
|
||||||
|
|||||||
@@ -379,8 +379,10 @@ const LOCALES = {
|
|||||||
profile_gateway_stopped: 'Gateway stopped',
|
profile_gateway_stopped: 'Gateway stopped',
|
||||||
profile_active: 'ACTIVE',
|
profile_active: 'ACTIVE',
|
||||||
profile_no_configuration: 'No configuration',
|
profile_no_configuration: 'No configuration',
|
||||||
|
profile_skill_count: (count) => `${count} skill${count === 1 ? '' : 's'}`,
|
||||||
profile_use: 'Use',
|
profile_use: 'Use',
|
||||||
profile_switch_title: 'Switch to this profile',
|
profile_switch_title: 'Switch to this profile',
|
||||||
|
profile_delete_title: 'Delete this profile',
|
||||||
manage_profiles: 'Manage profiles',
|
manage_profiles: 'Manage profiles',
|
||||||
profiles_load_failed: 'Failed to load profiles',
|
profiles_load_failed: 'Failed to load profiles',
|
||||||
profiles_busy_switch: 'Cannot switch profiles while agent is running',
|
profiles_busy_switch: 'Cannot switch profiles while agent is running',
|
||||||
@@ -771,8 +773,10 @@ const LOCALES = {
|
|||||||
profile_gateway_stopped: 'Gateway stopped',
|
profile_gateway_stopped: 'Gateway stopped',
|
||||||
profile_active: 'ACTIVE',
|
profile_active: 'ACTIVE',
|
||||||
profile_no_configuration: 'No configuration',
|
profile_no_configuration: 'No configuration',
|
||||||
|
profile_skill_count: (count) => `${count} habilidad${count === 1 ? '' : 'es'}`,
|
||||||
profile_use: 'Use',
|
profile_use: 'Use',
|
||||||
profile_switch_title: 'Switch to this profile',
|
profile_switch_title: 'Switch to this profile',
|
||||||
|
profile_delete_title: 'Eliminar este perfil',
|
||||||
manage_profiles: 'Manage profiles',
|
manage_profiles: 'Manage profiles',
|
||||||
profiles_load_failed: 'Failed to load profiles',
|
profiles_load_failed: 'Failed to load profiles',
|
||||||
profiles_busy_switch: 'Cannot switch profiles while agent is running',
|
profiles_busy_switch: 'Cannot switch profiles while agent is running',
|
||||||
@@ -1358,8 +1362,10 @@ const LOCALES = {
|
|||||||
profile_gateway_stopped: '网关已停止',
|
profile_gateway_stopped: '网关已停止',
|
||||||
profile_active: '当前',
|
profile_active: '当前',
|
||||||
profile_no_configuration: '无配置',
|
profile_no_configuration: '无配置',
|
||||||
|
profile_skill_count: (count) => `${count} 个技能`,
|
||||||
profile_use: '使用',
|
profile_use: '使用',
|
||||||
profile_switch_title: '切换到此配置档',
|
profile_switch_title: '切换到此配置档',
|
||||||
|
profile_delete_title: '删除此配置档',
|
||||||
manage_profiles: '管理配置档',
|
manage_profiles: '管理配置档',
|
||||||
profiles_load_failed: '加载配置档失败',
|
profiles_load_failed: '加载配置档失败',
|
||||||
profiles_busy_switch: 'Agent 运行中,无法切换配置档',
|
profiles_busy_switch: 'Agent 运行中,无法切换配置档',
|
||||||
|
|||||||
@@ -808,7 +808,7 @@ async function loadProfilesPanel() {
|
|||||||
const meta = [];
|
const meta = [];
|
||||||
if (p.model) meta.push(p.model.split('/').pop());
|
if (p.model) meta.push(p.model.split('/').pop());
|
||||||
if (p.provider) meta.push(p.provider);
|
if (p.provider) meta.push(p.provider);
|
||||||
if (p.skill_count) meta.push(p.skill_count + ' skill' + (p.skill_count !== 1 ? 's' : ''));
|
if (p.skill_count) meta.push(t('profile_skill_count', p.skill_count));
|
||||||
if (p.has_env) meta.push(t('profile_api_keys_configured'));
|
if (p.has_env) meta.push(t('profile_api_keys_configured'));
|
||||||
const gwDot = p.gateway_running
|
const gwDot = p.gateway_running
|
||||||
? `<span class="profile-opt-badge running" title="${esc(t('profile_gateway_running'))}"></span>`
|
? `<span class="profile-opt-badge running" title="${esc(t('profile_gateway_running'))}"></span>`
|
||||||
@@ -823,7 +823,7 @@ async function loadProfilesPanel() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="profile-card-actions">
|
<div class="profile-card-actions">
|
||||||
${!isActive ? `<button class="ws-action-btn" onclick="switchToProfile('${esc(p.name)}')" title="${esc(t('profile_switch_title'))}">${esc(t('profile_use'))}</button>` : ''}
|
${!isActive ? `<button class="ws-action-btn" onclick="switchToProfile('${esc(p.name)}')" title="${esc(t('profile_switch_title'))}">${esc(t('profile_use'))}</button>` : ''}
|
||||||
${!p.is_default ? `<button class="ws-action-btn danger" onclick="deleteProfile('${esc(p.name)}')" title="Delete this profile">${li('x',12)}</button>` : ''}
|
${!p.is_default ? `<button class="ws-action-btn danger" onclick="deleteProfile('${esc(p.name)}')" title="${esc(t('profile_delete_title'))}">${li('x',12)}</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
panel.appendChild(card);
|
panel.appendChild(card);
|
||||||
@@ -844,7 +844,7 @@ function renderProfileDropdown(data) {
|
|||||||
opt.className = 'profile-opt' + (p.name === active ? ' active' : '');
|
opt.className = 'profile-opt' + (p.name === active ? ' active' : '');
|
||||||
const meta = [];
|
const meta = [];
|
||||||
if (p.model) meta.push(p.model.split('/').pop());
|
if (p.model) meta.push(p.model.split('/').pop());
|
||||||
if (p.skill_count) meta.push(p.skill_count + ' skills');
|
if (p.skill_count) meta.push(t('profile_skill_count', p.skill_count));
|
||||||
const gwDot = `<span class="profile-opt-badge ${p.gateway_running ? 'running' : 'stopped'}"></span>`;
|
const gwDot = `<span class="profile-opt-badge ${p.gateway_running ? 'running' : 'stopped'}"></span>`;
|
||||||
const checkmark = p.name === active ? ' <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--link)" stroke-width="3" style="vertical-align:-1px"><polyline points="20 6 9 17 4 12"/></svg>' : '';
|
const checkmark = p.name === active ? ' <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--link)" stroke-width="3" style="vertical-align:-1px"><polyline points="20 6 9 17 4 12"/></svg>' : '';
|
||||||
opt.innerHTML = `<div class="profile-opt-name">${gwDot}${esc(p.name)}${p.is_default ? ' <span style="opacity:.5;font-weight:400">(default)</span>' : ''}${checkmark}</div>` +
|
opt.innerHTML = `<div class="profile-opt-name">${gwDot}${esc(p.name)}${p.is_default ? ' <span style="opacity:.5;font-weight:400">(default)</span>' : ''}${checkmark}</div>` +
|
||||||
@@ -1000,7 +1000,10 @@ async function submitProfileCreate() {
|
|||||||
toggleProfileForm();
|
toggleProfileForm();
|
||||||
await loadProfilesPanel();
|
await loadProfilesPanel();
|
||||||
showToast(t('profile_created', name));
|
showToast(t('profile_created', name));
|
||||||
} catch (e) { errEl.textContent = e.message || 'Create failed'; errEl.style.display = ''; }
|
} catch (e) {
|
||||||
|
errEl.textContent = e.message || t('create_failed');
|
||||||
|
errEl.style.display = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteProfile(name) {
|
async function deleteProfile(name) {
|
||||||
|
|||||||
@@ -10,6 +10,66 @@ def read(path: Path) -> str:
|
|||||||
return path.read_text(encoding="utf-8")
|
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 test_chinese_locale_block_exists():
|
def test_chinese_locale_block_exists():
|
||||||
src = read(REPO / "static" / "i18n.js")
|
src = read(REPO / "static" / "i18n.js")
|
||||||
assert "\n zh: {" in src
|
assert "\n zh: {" in src
|
||||||
@@ -35,17 +95,9 @@ def test_chinese_locale_includes_representative_translations():
|
|||||||
|
|
||||||
def test_chinese_locale_covers_english_keys():
|
def test_chinese_locale_covers_english_keys():
|
||||||
src = read(REPO / "static" / "i18n.js")
|
src = read(REPO / "static" / "i18n.js")
|
||||||
en_match = re.search(r"\n en: \{([\s\S]*?)\n \},\n\n es: \{", src)
|
|
||||||
zh_match = re.search(
|
|
||||||
r"\n zh: \{([\s\S]*?)\n \},\n\n // Traditional Chinese \(zh-Hant\)",
|
|
||||||
src,
|
|
||||||
)
|
|
||||||
assert en_match, "English locale block not found"
|
|
||||||
assert zh_match, "Chinese locale block not found"
|
|
||||||
|
|
||||||
key_pattern = re.compile(r"^\s{4}([a-zA-Z0-9_]+):", re.MULTILINE)
|
key_pattern = re.compile(r"^\s{4}([a-zA-Z0-9_]+):", re.MULTILINE)
|
||||||
en_keys = set(key_pattern.findall(en_match.group(1)))
|
en_keys = set(key_pattern.findall(extract_locale_block(src, "en")))
|
||||||
zh_keys = set(key_pattern.findall(zh_match.group(1)))
|
zh_keys = set(key_pattern.findall(extract_locale_block(src, "zh")))
|
||||||
|
|
||||||
missing = sorted(en_keys - zh_keys)
|
missing = sorted(en_keys - zh_keys)
|
||||||
assert not missing, f"Chinese locale missing keys: {missing}"
|
assert not missing, f"Chinese locale missing keys: {missing}"
|
||||||
@@ -53,13 +105,7 @@ def test_chinese_locale_covers_english_keys():
|
|||||||
|
|
||||||
def test_chinese_locale_has_no_duplicate_keys():
|
def test_chinese_locale_has_no_duplicate_keys():
|
||||||
src = read(REPO / "static" / "i18n.js")
|
src = read(REPO / "static" / "i18n.js")
|
||||||
zh_match = re.search(
|
|
||||||
r"\n zh: \{([\s\S]*?)\n \},\n\n // Traditional Chinese \(zh-Hant\)",
|
|
||||||
src,
|
|
||||||
)
|
|
||||||
assert zh_match, "Chinese locale block not found"
|
|
||||||
|
|
||||||
key_pattern = re.compile(r"^\s{4}([a-zA-Z0-9_]+):", re.MULTILINE)
|
key_pattern = re.compile(r"^\s{4}([a-zA-Z0-9_]+):", re.MULTILINE)
|
||||||
keys = key_pattern.findall(zh_match.group(1))
|
keys = key_pattern.findall(extract_locale_block(src, "zh"))
|
||||||
duplicates = sorted(k for k, count in Counter(keys).items() if count > 1)
|
duplicates = sorted(k for k, count in Counter(keys).items() if count > 1)
|
||||||
assert not duplicates, f"Chinese locale has duplicate keys: {duplicates}"
|
assert not duplicates, f"Chinese locale has duplicate keys: {duplicates}"
|
||||||
|
|||||||
@@ -39,6 +39,180 @@ def _run_i18n_case(script_expr: str) -> dict:
|
|||||||
return json.loads(proc.stdout)
|
return json.loads(proc.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_call_arglists(src: str, fn_name: str) -> list[str]:
|
||||||
|
token = f"{fn_name}("
|
||||||
|
out = []
|
||||||
|
search_from = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
start = src.find(token, search_from)
|
||||||
|
if start < 0:
|
||||||
|
return out
|
||||||
|
|
||||||
|
i = start + len(token)
|
||||||
|
depth = 1
|
||||||
|
in_single = False
|
||||||
|
in_double = False
|
||||||
|
in_backtick = False
|
||||||
|
escape = False
|
||||||
|
|
||||||
|
while i < len(src):
|
||||||
|
ch = src[i]
|
||||||
|
|
||||||
|
if escape:
|
||||||
|
escape = False
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_single:
|
||||||
|
if ch == "\\":
|
||||||
|
escape = True
|
||||||
|
elif ch == "'":
|
||||||
|
in_single = False
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_double:
|
||||||
|
if ch == "\\":
|
||||||
|
escape = True
|
||||||
|
elif ch == '"':
|
||||||
|
in_double = False
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_backtick:
|
||||||
|
if ch == "\\":
|
||||||
|
escape = True
|
||||||
|
elif ch == "`":
|
||||||
|
in_backtick = False
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ch == "'":
|
||||||
|
in_single = True
|
||||||
|
elif ch == '"':
|
||||||
|
in_double = True
|
||||||
|
elif ch == "`":
|
||||||
|
in_backtick = True
|
||||||
|
elif ch == "(":
|
||||||
|
depth += 1
|
||||||
|
elif ch == ")":
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
out.append(src[start + len(token) : i])
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
search_from = start + len(token)
|
||||||
|
|
||||||
|
|
||||||
|
def _split_top_level_args(arg_src: str) -> list[str]:
|
||||||
|
args = []
|
||||||
|
cur = []
|
||||||
|
paren = 0
|
||||||
|
brace = 0
|
||||||
|
bracket = 0
|
||||||
|
in_single = False
|
||||||
|
in_double = False
|
||||||
|
in_backtick = False
|
||||||
|
escape = False
|
||||||
|
|
||||||
|
for ch in arg_src:
|
||||||
|
if escape:
|
||||||
|
cur.append(ch)
|
||||||
|
escape = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_single:
|
||||||
|
cur.append(ch)
|
||||||
|
if ch == "\\":
|
||||||
|
escape = True
|
||||||
|
elif ch == "'":
|
||||||
|
in_single = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_double:
|
||||||
|
cur.append(ch)
|
||||||
|
if ch == "\\":
|
||||||
|
escape = True
|
||||||
|
elif ch == '"':
|
||||||
|
in_double = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_backtick:
|
||||||
|
cur.append(ch)
|
||||||
|
if ch == "\\":
|
||||||
|
escape = True
|
||||||
|
elif ch == "`":
|
||||||
|
in_backtick = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ch == "'":
|
||||||
|
in_single = True
|
||||||
|
cur.append(ch)
|
||||||
|
continue
|
||||||
|
if ch == '"':
|
||||||
|
in_double = True
|
||||||
|
cur.append(ch)
|
||||||
|
continue
|
||||||
|
if ch == "`":
|
||||||
|
in_backtick = True
|
||||||
|
cur.append(ch)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ch == "(":
|
||||||
|
paren += 1
|
||||||
|
cur.append(ch)
|
||||||
|
continue
|
||||||
|
if ch == ")":
|
||||||
|
paren -= 1
|
||||||
|
cur.append(ch)
|
||||||
|
continue
|
||||||
|
if ch == "{":
|
||||||
|
brace += 1
|
||||||
|
cur.append(ch)
|
||||||
|
continue
|
||||||
|
if ch == "}":
|
||||||
|
brace -= 1
|
||||||
|
cur.append(ch)
|
||||||
|
continue
|
||||||
|
if ch == "[":
|
||||||
|
bracket += 1
|
||||||
|
cur.append(ch)
|
||||||
|
continue
|
||||||
|
if ch == "]":
|
||||||
|
bracket -= 1
|
||||||
|
cur.append(ch)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ch == "," and paren == 0 and brace == 0 and bracket == 0:
|
||||||
|
args.append("".join(cur).strip())
|
||||||
|
cur = []
|
||||||
|
continue
|
||||||
|
|
||||||
|
cur.append(ch)
|
||||||
|
|
||||||
|
if cur:
|
||||||
|
args.append("".join(cur).strip())
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def _has_precedence_call(src: str, first_arg: str) -> bool:
|
||||||
|
expected_second = {
|
||||||
|
"localStorage.getItem('hermes-lang')",
|
||||||
|
'localStorage.getItem("hermes-lang")',
|
||||||
|
}
|
||||||
|
for arg_src in _extract_call_arglists(src, "resolvePreferredLocale"):
|
||||||
|
args = _split_top_level_args(arg_src)
|
||||||
|
if len(args) < 2:
|
||||||
|
continue
|
||||||
|
first = re.sub(r"\s+", "", args[0])
|
||||||
|
second = re.sub(r"\s+", "", args[1])
|
||||||
|
if first == first_arg and second in expected_second:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def test_i18n_exposes_locale_resolvers():
|
def test_i18n_exposes_locale_resolvers():
|
||||||
assert "function resolveLocale(" in I18N_JS
|
assert "function resolveLocale(" in I18N_JS
|
||||||
assert "function resolvePreferredLocale(" in I18N_JS
|
assert "function resolvePreferredLocale(" in I18N_JS
|
||||||
@@ -84,5 +258,5 @@ def test_set_locale_normalizes_alias_and_persists_canonical_key():
|
|||||||
|
|
||||||
|
|
||||||
def test_boot_and_settings_panel_use_shared_locale_precedence():
|
def test_boot_and_settings_panel_use_shared_locale_precedence():
|
||||||
assert re.search(r"resolvePreferredLocale\(s\.language\s*,\s*localStorage\.getItem\('hermes-lang'\)\)", BOOT_JS)
|
assert _has_precedence_call(BOOT_JS, "s.language")
|
||||||
assert re.search(r"resolvePreferredLocale\(settings\.language\s*,\s*localStorage\.getItem\('hermes-lang'\)\)", PANELS_JS)
|
assert _has_precedence_call(PANELS_JS, "settings.language")
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ def test_login_page_uses_simplified_chinese_for_zh_cn_alias():
|
|||||||
assert "\u767b\u5f55" in html
|
assert "\u767b\u5f55" in html
|
||||||
assert "\u8f93\u5165\u5bc6\u7801\u7ee7\u7eed\u4f7f\u7528" in html
|
assert "\u8f93\u5165\u5bc6\u7801\u7ee7\u7eed\u4f7f\u7528" in html
|
||||||
finally:
|
finally:
|
||||||
post("/api/settings", {"language": prev_lang})
|
restored, restore_status = post("/api/settings", {"language": prev_lang})
|
||||||
|
assert restore_status == 200
|
||||||
|
assert restored.get("language") == prev_lang
|
||||||
|
|
||||||
|
|
||||||
def test_login_page_uses_traditional_chinese_for_zh_hant():
|
def test_login_page_uses_traditional_chinese_for_zh_hant():
|
||||||
@@ -61,4 +63,6 @@ def test_login_page_uses_traditional_chinese_for_zh_hant():
|
|||||||
assert "\u8f38\u5165\u5bc6\u78bc\u7e7c\u7e8c\u4f7f\u7528" in html
|
assert "\u8f38\u5165\u5bc6\u78bc\u7e7c\u7e8c\u4f7f\u7528" in html
|
||||||
assert "\u5bc6\u78bc\u932f\u8aa4" in html
|
assert "\u5bc6\u78bc\u932f\u8aa4" in html
|
||||||
finally:
|
finally:
|
||||||
post("/api/settings", {"language": prev_lang})
|
restored, restore_status = post("/api/settings", {"language": prev_lang})
|
||||||
|
assert restore_status == 200
|
||||||
|
assert restored.get("language") == prev_lang
|
||||||
|
|||||||
Reference in New Issue
Block a user