diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d737b24..e785b783 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Logs severity/filter i18n keys are now localized for Japanese, Russian, Spanish, German, Simplified Chinese, Traditional Chinese, Portuguese, and Korean instead of falling back to English TODO placeholders (closes #2098). - **PR #2136** by @LumenYoung — Stale stream writebacks no longer poison the active session transcript. `cancel_stream()` intentionally clears `active_stream_id` early so the UI can accept a follow-up turn while an old worker is unwinding — but the old worker could still return later from `run_conversation()` and persist its stale result over the newer transcript, causing visible transcript / turn journal / `state.db` to disagree (especially around cancel+retry on compressed continuations). Adds a single-line ownership check `_stream_writeback_is_current(session, stream_id)` (token equality against `session.active_stream_id`) and short-circuits both finalize paths: the success path in `_run_agent_streaming` and the cancel-handler path in `cancel_stream()`. When the stream no longer owns the writeback, both paths log `Skipping stale stream/cancel writeback` and return cleanly without persisting. 89-line regression suite in `tests/test_stale_stream_writeback.py`; companion updates to `tests/test_issue1361_cancel_data_loss.py` and `tests/test_sprint42.py` for the new return-without-persist behavior. ### Added diff --git a/static/i18n.js b/static/i18n.js index 4cdd841c..34576214 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -2865,11 +2865,11 @@ const LOCALES = { logs_no_mtime: '未書き込み', logs_truncated_hint: '大きなログファイルの末尾を表示しています。メモリ使用量を抑えるため、古いデータは省略されました。', logs_copied: 'ログをコピーしました', - logs_severity: 'Severity', // TODO: translate - logs_severity_all: 'All', // TODO: translate - logs_severity_errors: 'Errors', // TODO: translate - logs_severity_warnings: 'Warnings+', // TODO: translate - logs_filter_active: 'shown (filter active)', // TODO: translate + logs_severity: '重大度', + logs_severity_all: 'すべて', + logs_severity_errors: 'エラー', + logs_severity_warnings: '警告+', + logs_filter_active: '表示中(フィルター有効)', // Insights insights_title: '使用状況分析', @@ -3778,11 +3778,11 @@ const LOCALES = { logs_no_mtime: 'not written yet', // TODO: translate logs_truncated_hint: 'Showing the tail of a large log file; older bytes were skipped to keep memory bounded.', // TODO: translate logs_copied: 'Logs copied', // TODO: translate - logs_severity: 'Severity', // TODO: translate - logs_severity_all: 'All', // TODO: translate - logs_severity_errors: 'Errors', // TODO: translate - logs_severity_warnings: 'Warnings+', // TODO: translate - logs_filter_active: 'shown (filter active)', // TODO: translate + logs_severity: 'Уровень', + logs_severity_all: 'Все', + logs_severity_errors: 'Ошибки', + logs_severity_warnings: 'Предупреждения+', + logs_filter_active: 'показано (фильтр активен)', new_conversation: 'Новая беседа', filter_conversations: 'Фильтр бесед...', session_time_unknown: 'Неизвестно', @@ -4820,11 +4820,11 @@ const LOCALES = { logs_no_mtime: 'not written yet', // TODO: translate logs_truncated_hint: 'Showing the tail of a large log file; older bytes were skipped to keep memory bounded.', // TODO: translate logs_copied: 'Logs copied', // TODO: translate - logs_severity: 'Severity', // TODO: translate - logs_severity_all: 'All', // TODO: translate - logs_severity_errors: 'Errors', // TODO: translate - logs_severity_warnings: 'Warnings+', // TODO: translate - logs_filter_active: 'shown (filter active)', // TODO: translate + logs_severity: 'Severidad', + logs_severity_all: 'Todo', + logs_severity_errors: 'Errores', + logs_severity_warnings: 'Advertencias+', + logs_filter_active: 'mostrados (filtro activo)', new_conversation: 'Nueva conversación', filter_conversations: 'Filtrar conversaciones...', session_time_unknown: 'Desconocido', @@ -5845,11 +5845,11 @@ const LOCALES = { logs_no_mtime: 'not written yet', // TODO: translate logs_truncated_hint: 'Showing the tail of a large log file; older bytes were skipped to keep memory bounded.', // TODO: translate logs_copied: 'Logs copied', // TODO: translate - logs_severity: 'Severity', // TODO: translate - logs_severity_all: 'All', // TODO: translate - logs_severity_errors: 'Errors', // TODO: translate - logs_severity_warnings: 'Warnings+', // TODO: translate - logs_filter_active: 'shown (filter active)', // TODO: translate + logs_severity: 'Schweregrad', + logs_severity_all: 'Alle', + logs_severity_errors: 'Fehler', + logs_severity_warnings: 'Warnungen+', + logs_filter_active: 'angezeigt (Filter aktiv)', new_conversation: 'Neuer Chat', filter_conversations: 'Chats filtern...', scheduled_jobs: 'Geplante Aufgaben', @@ -6903,11 +6903,11 @@ const LOCALES = { logs_no_mtime: '尚未写入', logs_truncated_hint: '此处显示的是日志文件的末尾内容。为节省内存,已省略较早的数据。', logs_copied: '日志已复制', - logs_severity: 'Severity', // TODO: translate - logs_severity_all: 'All', // TODO: translate - logs_severity_errors: 'Errors', // TODO: translate - logs_severity_warnings: 'Warnings+', // TODO: translate - logs_filter_active: 'shown (filter active)', // TODO: translate + logs_severity: '严重性', + logs_severity_all: '全部', + logs_severity_errors: '错误', + logs_severity_warnings: '警告+', + logs_filter_active: '已显示(筛选器已启用)', new_conversation: '新建对话', filter_conversations: '筛选对话…', session_time_unknown: '未知', @@ -7933,6 +7933,11 @@ const LOCALES = { kanban_dispatch_auto_blocked: '自動封鎖', kanban_dispatch_timed_out: '逾時', kanban_dispatch_crashed: '崩潰', + logs_severity: '嚴重性', + logs_severity_all: '全部', + logs_severity_errors: '錯誤', + logs_severity_warnings: '警告+', + logs_filter_active: '已顯示(篩選器已啟用)', new_conversation: '新對話', filter_conversations: '篩選對話', scheduled_jobs: '排程任務', @@ -9138,11 +9143,11 @@ const LOCALES = { logs_no_mtime: 'not written yet', // TODO: translate logs_truncated_hint: 'Showing the tail of a large log file; older bytes were skipped to keep memory bounded.', // TODO: translate logs_copied: 'Logs copied', // TODO: translate - logs_severity: 'Severity', // TODO: translate - logs_severity_all: 'All', // TODO: translate - logs_severity_errors: 'Errors', // TODO: translate - logs_severity_warnings: 'Warnings+', // TODO: translate - logs_filter_active: 'shown (filter active)', // TODO: translate + logs_severity: 'Severidade', + logs_severity_all: 'Todos', + logs_severity_errors: 'Erros', + logs_severity_warnings: 'Avisos+', + logs_filter_active: 'exibidos (filtro ativo)', new_conversation: 'Nova conversa', filter_conversations: 'Filtrar conversas...', session_time_unknown: 'Desconhecido', @@ -10144,11 +10149,11 @@ const LOCALES = { logs_no_mtime: 'not written yet', // TODO: translate logs_truncated_hint: 'Showing the tail of a large log file; older bytes were skipped to keep memory bounded.', // TODO: translate logs_copied: 'Logs copied', // TODO: translate - logs_severity: 'Severity', // TODO: translate - logs_severity_all: 'All', // TODO: translate - logs_severity_errors: 'Errors', // TODO: translate - logs_severity_warnings: 'Warnings+', // TODO: translate - logs_filter_active: 'shown (filter active)', // TODO: translate + logs_severity: '심각도', + logs_severity_all: '전체', + logs_severity_errors: '오류', + logs_severity_warnings: '경고+', + logs_filter_active: '표시됨(필터 활성)', new_conversation: '새 대화', filter_conversations: '대화 필터…', session_time_unknown: 'Unknown', diff --git a/tests/test_issue2098_logs_i18n.py b/tests/test_issue2098_logs_i18n.py new file mode 100644 index 00000000..84ee1761 --- /dev/null +++ b/tests/test_issue2098_logs_i18n.py @@ -0,0 +1,119 @@ +import re +from pathlib import Path + + +I18N_PATH = Path(__file__).resolve().parent.parent / "static" / "i18n.js" + + +LOGS_FILTER_KEYS = { + "ja": { + "logs_severity": "重大度", + "logs_severity_all": "すべて", + "logs_severity_errors": "エラー", + "logs_severity_warnings": "警告+", + "logs_filter_active": "表示中(フィルター有効)", + }, + "ru": { + "logs_severity": "Уровень", + "logs_severity_all": "Все", + "logs_severity_errors": "Ошибки", + "logs_severity_warnings": "Предупреждения+", + "logs_filter_active": "показано (фильтр активен)", + }, + "es": { + "logs_severity": "Severidad", + "logs_severity_all": "Todo", + "logs_severity_errors": "Errores", + "logs_severity_warnings": "Advertencias+", + "logs_filter_active": "mostrados (filtro activo)", + }, + "de": { + "logs_severity": "Schweregrad", + "logs_severity_all": "Alle", + "logs_severity_errors": "Fehler", + "logs_severity_warnings": "Warnungen+", + "logs_filter_active": "angezeigt (Filter aktiv)", + }, + "zh": { + "logs_severity": "严重性", + "logs_severity_all": "全部", + "logs_severity_errors": "错误", + "logs_severity_warnings": "警告+", + "logs_filter_active": "已显示(筛选器已启用)", + }, + "zh-Hant": { + "logs_severity": "嚴重性", + "logs_severity_all": "全部", + "logs_severity_errors": "錯誤", + "logs_severity_warnings": "警告+", + "logs_filter_active": "已顯示(篩選器已啟用)", + }, + "pt": { + "logs_severity": "Severidade", + "logs_severity_all": "Todos", + "logs_severity_errors": "Erros", + "logs_severity_warnings": "Avisos+", + "logs_filter_active": "exibidos (filtro ativo)", + }, + "ko": { + "logs_severity": "심각도", + "logs_severity_all": "전체", + "logs_severity_errors": "오류", + "logs_severity_warnings": "경고+", + "logs_filter_active": "표시됨(필터 활성)", + }, +} + + +def _i18n_locale_block(locale: str) -> str: + src = I18N_PATH.read_text(encoding="utf-8") + if "-" in locale: + head = re.compile(rf"^ '{re.escape(locale)}':\s*\{{", re.M) + else: + head = re.compile(rf"^ {re.escape(locale)}:\s*\{{", re.M) + match = head.search(src) + assert match, f"locale {locale!r} not found" + body_start = match.end() + depth = 1 + i = body_start + while i < len(src) and depth > 0: + ch = src[i] + if ch == "/" and i + 1 < len(src) and src[i + 1] == "/": + newline = src.find("\n", i) + i = len(src) if newline < 0 else newline + 1 + continue + if ch in ("'", '"'): + quote = ch + i += 1 + while i < len(src) and src[i] != quote: + i += 2 if src[i] == "\\" else 1 + i += 1 + continue + if ch == "`": + i += 1 + while i < len(src) and src[i] != "`": + i += 2 if src[i] == "\\" else 1 + i += 1 + continue + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + return src[body_start:i] + i += 1 + raise AssertionError(f"locale {locale!r} block never closed") + + +def _string_value(block: str, key: str) -> str: + match = re.search(rf"^\s+{re.escape(key)}:\s+'([^']*)',(?P[^\n]*)$", block, re.M) + assert match, f"{key} missing" + assert "TODO: translate" not in match.group("tail") + return match.group(1) + + +def test_logs_severity_filter_keys_are_translated_for_non_english_locales(): + for locale, expected in LOGS_FILTER_KEYS.items(): + block = _i18n_locale_block(locale) + for key, value in expected.items(): + assert _string_value(block, key) == value