diff --git a/.kiro/specs/i18n-externalize-remaining-backend-logs/design.md b/.kiro/specs/i18n-externalize-remaining-backend-logs/design.md new file mode 100644 index 00000000..9adec698 --- /dev/null +++ b/.kiro/specs/i18n-externalize-remaining-backend-logs/design.md @@ -0,0 +1,286 @@ +# Design Document + +## Overview + +**Purpose**: Replace the last nine hard-coded Chinese log/print strings in three backend modules (`backend/app/api/graph.py`, `backend/app/services/oasis_profile_generator.py`, `backend/app/utils/retry.py`) with calls to the existing `t("log..", **kwargs)` helper, and add the corresponding entries to `locales/en.json` and `locales/zh.json`. The result is locale-correct backend logs with zero behavioural drift. + +**Users**: Backend operators reading logs in English deployments; existing Chinese-locale operators (preserved verbatim). + +**Impact**: Removes the last sources of Chinese-text leakage in backend logs under the `en` locale, completing the i18n coverage started by ticket #6. + +### Goals + +- Replace the nine f-string arguments listed in ticket #24 with `t("log..", **kwargs)` calls. +- Add eleven new locale entries (3 in `log.graph_api`, 2 in `log.profile_generator`, 4 in new `log.retry`) to both `locales/en.json` and `locales/zh.json` with key parity. +- Preserve all interpolated values, all log levels, all control flow, and all `print(...)` console banners. + +### Non-Goals + +- Translating other Chinese strings in the same files (docstrings, comments, `update_task` messages, `progress_callback` messages, `logger.warning` retry messages) — out of scope for ticket #24. +- Modifying the `t()` helper, the locale resolution logic, or the locale dictionary structure (other than adding the listed keys). +- Frontend `vue-i18n` translation work or schema changes to `locales/{en,zh}.json`. +- Adding test infrastructure, the `run_audit.sh` script, or any new dev dependency. + +## Boundary Commitments + +### This Spec Owns + +- The string-literal contents of nine specific `logger.{info,error}` and `print(...)` call sites (exact `file:line` listed in Requirement 1). +- Eleven new translation entries in `locales/en.json` and `locales/zh.json`. +- The new `log.retry` sub-namespace under the existing top-level `log` key. + +### Out of Boundary + +- Other Chinese strings in the three modified files. +- Any change to public API contracts, log levels, or response payloads. +- Any change to the `t()` helper or the per-thread / per-request locale resolution logic. +- Frontend `zh.json` entries beyond the ones this spec must add for backend parity (i.e., none — frontend keys are untouched). + +### Allowed Dependencies + +- `backend/app/utils/locale.py` (`t`) — already in use, just import it where needed. +- The existing locale dictionaries `locales/{en,zh}.json` — extend, don't re-organise. +- `get_logger` from `backend/app/utils/logger.py` — already imported by `retry.py`. + +### Revalidation Triggers + +- Renaming `t()` or moving it to a different module. +- Changing the placeholder syntax in `t()` from `{name}` to anything else. +- Restructuring `locales/en.json` / `zh.json` (e.g., flattening `log..m###` into a flat key tree). + +## Architecture + +### Existing Architecture Analysis + +This spec extends a pattern already established by ticket #6 (`i18n-externalize-backend-logs`). The convention is: + +1. Source-code call sites use `t("log..m###", placeholder=value, …)` instead of `f"…{value}…"`. +2. Each `t()` key has matching entries in `locales/en.json` (English copy) and `locales/zh.json` (verbatim original Chinese). +3. Placeholders use `{name}` (replaced via `str.replace` inside `t()`). +4. The locale is resolved per request (`Accept-Language`) or per thread (`set_locale`); `'zh'` is the default fallback; missing keys return the key string and emit a deduped warning. + +The constraint: only the nine listed call sites change. No new architecture, no new component, no new integration point. + +### Architecture Pattern & Boundary Map + +The change is a **pure string-externalisation extension** of the existing localisation pattern. No new components, no new flows, no new dependencies. The only structural addition is a new `log.retry` sub-namespace inside the existing top-level `log` key in the locale dictionaries. + +```mermaid +flowchart LR + A[graph.py:385/494/513
build_logger.{info,error}] -->|t("log.graph_api.mNNN", ...)| L[t() helper
backend/app/utils/locale.py] + B[oasis_profile_generator.py:945/1001
print(...)] -->|t("log.profile_generator.mNNN", ...)| L + C[retry.py:55/108/179/227
logger.error] -->|t("log.retry.mNNN", ...)| L + L --> EN[locales/en.json
log.graph_api.m027-m029
log.profile_generator.m024-m025
log.retry.m001-m004] + L --> ZH[locales/zh.json
same key paths
verbatim Chinese values] +``` + +### Technology Stack + +| Layer | Choice / Version | Role in Feature | Notes | +|-------|------------------|-----------------|-------| +| Backend / Services | Python ≥3.11 | Source-language change site | No version change | +| Backend / Services | `backend/app/utils/locale.py` (project-internal) | Provides `t(key, **kwargs)` | Reused as-is | +| Data / Storage | `locales/en.json`, `locales/zh.json` | Adds 11 new key/value pairs | Flat JSON, UTF-8 | +| Infrastructure / Runtime | Flask 3.0 / asyncio | Locale resolution context | No runtime change | + +## File Structure Plan + +### Modified Files + +- `backend/app/api/graph.py` — Replace the f-string argument of three `build_logger.{info,error}` calls (lines 385, 494, 513) with `t("log.graph_api.", **kwargs)`. No new imports (already imports `t` on line 21). +- `backend/app/services/oasis_profile_generator.py` — Replace the f-string argument of two `print(...)` calls (lines 945, 1001) with `t("log.profile_generator.", **kwargs)`. No new imports (already imports `t` on line 23). +- `backend/app/utils/retry.py` — Add `from .locale import t` (or `from ..utils.locale import t`, matching the project's existing relative-import style). Replace the f-string argument of four `logger.error` calls (lines 55, 108, 179, 227) with `t("log.retry.", **kwargs)`. +- `locales/en.json` — Append three keys to `log.graph_api`, two to `log.profile_generator`, and a new `log.retry` sub-namespace with four keys. +- `locales/zh.json` — Mirror the same key paths with verbatim original Chinese strings. + +No new files. No deleted files. + +## Requirements Traceability + +| Requirement | Summary | Components | Interfaces | Flows | +|-------------|---------|------------|------------|-------| +| 1.1 | Replace `graph.py` log strings via `t()` | `graph.py` build-task closure | `t("log.graph_api.", ...)` | Build pipeline log emission | +| 1.2 | Replace `oasis_profile_generator.py` banner prints via `t()` | `OasisProfileGenerator.generate_profiles_parallel` | `t("log.profile_generator.", ...)` | Profile-generation banner | +| 1.3 | Replace `retry.py` errors via `t()` (new `log.retry` namespace) | `retry_with_backoff`, `retry_with_backoff_async`, `RetryableAPIClient` | `t("log.retry.", ...)` | Retry-failure path | +| 1.4 | Preserve interpolated values via kwargs | All three modules | `t(key, name=value, ...)` with `{name}` placeholders | All log emission | +| 1.5 | Zero CJK in the listed lines after change | Same as 1.1–1.3 | n/a | n/a | +| 2.1, 2.2 | Add 11 new keys to `en.json` and `zh.json` | Locale dictionaries | JSON file edits | n/a | +| 2.3 | Use next available `m###` slot per namespace | Locale dictionaries | n/a | n/a | +| 2.4 | Structural parity across both files | Locale dictionaries | Verification script | n/a | +| 2.5 | No new top-level keys; no existing keys touched | Locale dictionaries | n/a | n/a | +| 3.1 | Graph build pipeline behaves identically | `graph.py` build-task closure | n/a | Build pipeline | +| 3.2 | Profile generator continues to print exactly two banners | `oasis_profile_generator.py` | n/a | Banner emission | +| 3.3 | Retry semantics unchanged (raise, sleep, level, position) | `retry.py` | n/a | Retry path | +| 3.4 | HTTP responses unchanged | All API endpoints | n/a | n/a | +| 4.1, 4.2, 4.3, 4.4 | Locale resolution works in all contexts | `t()` helper (unchanged) | n/a | n/a | +| 5.1 | CJK regex audit on the nine lines passes | Verification procedure | `grep -P "[一-鿿]"` | n/a | +| 5.2 | Key-parity audit passes | Verification procedure | Python `json.load` walk | n/a | +| 5.3 | Placeholder-integrity audit passes | Verification procedure | Python regex check | n/a | +| 5.4 | Only stock tooling | Verification procedure | `grep`, `python3` | n/a | +| 5.5 | `pytest` continues to pass | Backend test suite | `uv run python -m pytest` | n/a | + +## Components and Interfaces + +| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies (P0/P1) | Contracts | +|-----------|--------------|--------|--------------|--------------------------|-----------| +| `graph.py` build-task closure | Backend / API | Log graph-build start/complete/fail in active locale | 1.1, 1.4, 1.5, 3.1 | `t()` (P0), `build_logger` (P0) | Behaviour-only | +| OASIS banner prints | Backend / Services | Print banner around parallel profile generation | 1.2, 1.4, 1.5, 3.2 | `t()` (P0) | Console-output | +| Retry error logs | Backend / Utils | Log final-failure errors after retry exhaustion | 1.3, 1.4, 1.5, 3.3 | `t()` (P0), `logger` (P0) | Behaviour-only | +| Locale dictionaries | Backend / Data | Provide en/zh strings for new keys | 2.1–2.5 | JSON parse (P0) | Data | + +### Backend / Services + +#### `graph.py` build-task closure + +| Field | Detail | +|-------|--------| +| Intent | Emit "build started", "build completed", "build failed" log records using `t()` | +| Requirements | 1.1, 1.4, 1.5, 3.1 | + +**Responsibilities & Constraints** + +- Replace three f-string log arguments only. +- Do not change log level, log handler, control flow, or surrounding `task_manager.update_task(...)` calls. + +**Dependencies** + +- Inbound: called from `task_manager.run_task` (P0) +- Outbound: `t()` (P0), `build_logger.{info,error}` (P0) + +**Contracts**: Service [ ] / API [ ] / Event [ ] / Batch [ ] / State [ ] ← (none — purely behavioural) + +**Key Mapping** + +| Line | Existing source | New key | EN translation | ZH translation | +|------|-----------------|---------|----------------|----------------| +| 385 | `f"[{task_id}] 开始构建图谱..."` | `log.graph_api.m027` | `[{task_id}] Starting graph build...` | `[{task_id}] 开始构建图谱...` | +| 494 | `f"[{task_id}] 图谱构建完成: graph_id={graph_id}, 节点={node_count}, 边={edge_count}"` | `log.graph_api.m028` | `[{task_id}] Graph build completed: graph_id={graph_id}, nodes={node_count}, edges={edge_count}` | `[{task_id}] 图谱构建完成: graph_id={graph_id}, 节点={node_count}, 边={edge_count}` | +| 513 | `f"[{task_id}] 图谱构建失败: {str(e)}"` | `log.graph_api.m029` | `[{task_id}] Graph build failed: {e}` | `[{task_id}] 图谱构建失败: {e}` | + +**Implementation Notes** + +- `t` is already imported at `graph.py:21`. +- Use `e=str(e)` to maintain the existing exception-string semantics. + +#### OASIS banner prints (`oasis_profile_generator.py`) + +| Field | Detail | +|-------|--------| +| Intent | Wrap the two banner-print arguments in `t()` while leaving the surrounding `'='*60` separator prints intact | +| Requirements | 1.2, 1.4, 1.5, 3.2 | + +**Responsibilities & Constraints** + +- Replace only the *content* line of each banner (the line at 945 and the line at 1001). The two `'='*60` separator prints around them (lines 944/946 and 1000/1002) contain only ASCII and stay verbatim. +- Do not remove either `print(...)` call. +- Do not modify the existing `logger.info(t("log.profile_generator.m017", …))` at line 943. + +**Key Mapping** + +| Line | Existing source | New key | EN translation | ZH translation | +|------|-----------------|---------|----------------|----------------| +| 945 | `f"开始生成Agent人设 - 共 {total} 个实体,并行数: {parallel_count}"` | `log.profile_generator.m024` | `Starting agent profile generation — {total} entities, parallelism: {parallel_count}` | `开始生成Agent人设 - 共 {total} 个实体,并行数: {parallel_count}` | +| 1001 | `f"人设生成完成!共生成 {len([p for p in profiles if p])} 个Agent"` | `log.profile_generator.m025` | `Profile generation complete — generated {count} agents` | `人设生成完成!共生成 {count} 个Agent` | + +**Implementation Notes** + +- The expression `len([p for p in profiles if p])` becomes a kwarg: `count=len([p for p in profiles if p])`. This is a single name, easier for the locale dictionaries. +- `t` is already imported at `oasis_profile_generator.py:23`. + +#### Retry error logs (`retry.py`) + +| Field | Detail | +|-------|--------| +| Intent | Localise the four "final-failure" `logger.error` strings; introduce `log.retry` sub-namespace | +| Requirements | 1.3, 1.4, 1.5, 3.3, 4.1–4.4 | + +**Responsibilities & Constraints** + +- Add `from ..utils.locale import t` at the top of `retry.py` (matching the relative-import depth used by other `backend/app/utils/*` files). +- Replace four f-string `logger.error(...)` arguments only. +- Do not touch the `logger.warning(...)` retry-attempt messages (out of scope per ticket #24). +- Do not change exception handling, control flow, or the public decorator/class signatures. + +**Key Mapping** + +| Line | Existing source | New key | EN translation | ZH translation | +|------|-----------------|---------|----------------|----------------| +| 55 | `f"函数 {func.__name__} 在 {max_retries} 次重试后仍失败: {str(e)}"` | `log.retry.m001` | `Function {func_name} still failing after {max_retries} retries: {e}` | `函数 {func_name} 在 {max_retries} 次重试后仍失败: {e}` | +| 108 | `f"异步函数 {func.__name__} 在 {max_retries} 次重试后仍失败: {str(e)}"` | `log.retry.m002` | `Async function {func_name} still failing after {max_retries} retries: {e}` | `异步函数 {func_name} 在 {max_retries} 次重试后仍失败: {e}` | +| 179 | `f"API调用在 {self.max_retries} 次重试后仍失败: {str(e)}"` | `log.retry.m003` | `API call still failing after {max_retries} retries: {e}` | `API调用在 {max_retries} 次重试后仍失败: {e}` | +| 227 | `f"处理第 {idx + 1} 项失败: {str(e)}"` | `log.retry.m004` | `Failed processing item #{index}: {e}` | `处理第 {index} 项失败: {e}` | + +**Implementation Notes** + +- Use kwargs `func_name=func.__name__`, `max_retries=max_retries` (or `self.max_retries`), `index=idx + 1`, `e=str(e)`. +- Locale resolution at the call site: in Flask request scope → `Accept-Language`; in background tasks → `set_locale` per-thread; in async coroutines → per-thread (asyncio shares the OS thread). Default fallback is `'zh'`. No new wiring needed (Requirement 4). + +### Backend / Data + +#### Locale dictionaries + +| Field | Detail | +|-------|--------| +| Intent | Provide en/zh strings for the eleven new keys with structural parity | +| Requirements | 2.1, 2.2, 2.3, 2.4, 2.5 | + +**Responsibilities & Constraints** + +- Append to existing `log.graph_api` and `log.profile_generator` sub-namespaces. +- Add a new `log.retry` sub-namespace as a sibling of the others. +- No top-level key additions; no modifications to any pre-existing key. +- Maintain UTF-8 encoding and the file's existing 2-space indent style. + +**Implementation Notes** + +- Use `python3 -m json.tool` (or equivalent) to round-trip the JSON files after editing, to ensure formatting consistency. +- Validate parity with a small Python script that recursively compares key paths. + +## System Flows + +(Skipped — no non-trivial flow change. The build / profile / retry call paths execute as before; only the message text source language differs.) + +## Error Handling + +### Error Strategy + +This spec changes only message-string sources. Error-handling semantics in the touched code are preserved: + +- `graph.py:513` continues to set `project.status = ProjectStatus.FAILED` and call `task_manager.update_task(..., status=TaskStatus.FAILED, ...)` after the `build_logger.error(...)` call. +- `retry.py` continues to `raise` the underlying exception after the final `logger.error(...)`. +- The `t()` helper does not raise on missing keys — it returns the key string and emits a deduped warning. This contract is unchanged. + +### Error Categories and Responses + +Out of scope — no new error category is introduced. + +## Testing Strategy + +### Unit / Integration Tests + +The project does not currently maintain a comprehensive backend unit-test suite for these modules. The change is verified mechanically rather than via new pytest tests: + +1. **CJK absence on the touched lines** — `grep -nP "[一-鿿]"` against the nine specific lines must return no matches. +2. **JSON parse + key parity** — a small inline Python check that loads `locales/{en,zh}.json` and asserts every newly-added key path exists in both files. +3. **Placeholder integrity** — for each new key, every `{name}` placeholder in the `zh` value must also appear in the `en` value (and vice versa). +4. **Existing test suite** — `uv run python -m pytest` continues to pass; ticket #6's tests at `backend/scripts/test_profile_format.py` are not affected by this work. + +### Manual Smoke Test + +After implementation: + +- Set `Accept-Language: en` and run an end-to-end graph build via the local Flask app (`npm run dev`); confirm the start / complete / fail log lines render in English. +- Run a profile generation flow and observe the banner prints in English. +- Force a retry exhaustion (e.g., temporarily lower `max_retries=0` and trigger an error) and confirm the `log.retry` message renders in English. + +(Manual smoke is documentation-only; not a blocker for merging.) + +## Optional Sections + +### Security Considerations + +None. No auth, no PII, no external integration changes. The exception text in log messages was already exposed via the previous f-string formatting; routing it through `t()` does not change the surface. + +### Performance & Scalability + +Negligible. `t()` is an in-memory dict lookup with `str.replace` for placeholders; cost is below noise floor for log emission. diff --git a/.kiro/specs/i18n-externalize-remaining-backend-logs/gap-analysis.md b/.kiro/specs/i18n-externalize-remaining-backend-logs/gap-analysis.md new file mode 100644 index 00000000..52bea9d7 --- /dev/null +++ b/.kiro/specs/i18n-externalize-remaining-backend-logs/gap-analysis.md @@ -0,0 +1,124 @@ +# Implementation Gap Analysis + +## 1. Codebase Findings + +### 1.1 Existing infrastructure already covers the i18n mechanics + +- `backend/app/utils/locale.py` already exports `t(key, **kwargs)` with: + - per-thread locale (`set_locale` writes `_thread_local.locale`) + - per-request locale (`get_locale` checks Flask `has_request_context()` then `Accept-Language`) + - `zh` fallback when the active locale is missing a key, then key-string fallback if `zh` is missing too + - dedup'd warning on missing keys (`_warn_missing_key_once`), no exceptions raised +- All wiring required by Requirement 4 is therefore already in place. **No `locale.py` change is needed for ticket #24.** + +### 1.2 The two files we touch already use `t()` + +- `backend/app/api/graph.py:21` — `from ..utils.locale import t` +- `backend/app/services/oasis_profile_generator.py:23` — `from ..utils.locale import get_language_instruction, get_locale, set_locale, t` + +The third file does NOT yet import `t`: +- `backend/app/utils/retry.py` — no `from ..utils.locale import t`. Need to add the import. + +### 1.3 Existing locale namespace shape (from `locales/en.json`) + +- `log.graph_api` — populated `m006`–`m019, m026`. Next free slots that are *contiguous* would be `m027`, `m028`, `m029`. (Could also reuse `m009, m010, m012, m020–m025` since they are absent, but it is safer to append at the tail to avoid colliding with any unmerged work assuming a particular reservation.) +- `log.profile_generator` — populated `m001`–`m023` densely. Next free: `m024`, `m025`. +- `log.retry` — does NOT exist. Will be created with `m001`–`m004`. + +The `log.profile_generator.m017` key already covers a *similar* message ("Starting parallel generation of {total} agent profiles (parallelism: {parallel_count})…"). The `print(...)` at `oasis_profile_generator.py:945` and the `logger.info(t("log.profile_generator.m017", ...))` at line 943 are emitting the same logical event in two channels — log + console banner. The cleanest move is **not** to reuse `m017` (which would lose the banner-style separator/centring) but to introduce dedicated `m024` / `m025` keys for the banner text, so the banner has its own copy decoupled from the log line. + +### 1.4 Translation pattern already established by ticket #6 + +Per the prior spec at `.kiro/specs/i18n-externalize-backend-logs/`, the project's convention is: + +- `t("log..m###", placeholder=value, …)` inside `logger.{info,warning,error,debug,exception}` calls. +- Placeholders use `{name}` syntax (replaced via `str.replace` inside `t()`); positional `{0}`/`{}` are not supported. +- f-string formatting must be removed entirely from the call argument; values are passed as kwargs. +- The Chinese source string is preserved verbatim in `zh.json`, with `f"…{var}…"` rewritten as `"…{var}…"`. + +This work strictly extends the existing pattern. **No new convention is introduced.** + +### 1.5 `build_logger` vs. module logger + +In `graph.py`, the affected calls use a locally-created `build_logger = get_logger('mirofish.build')` inside the `build_task` background function (lines 383). This is a different logger handle, but `t()` is logger-agnostic — it returns a string that any logger can format. No special handling needed. + +### 1.6 `print(...)` calls in `oasis_profile_generator.py` + +The two banner prints (lines 945 and 1001) are deliberate console-output decorations (visible on stdout for the Flask process), separate from the structured log emitted by `logger.info` on lines 943 and earlier. The task is to keep them as `print(...)` but route the message text through `t(...)`: + +```python +print(t("log.profile_generator.m024", total=total, parallel_count=parallel_count)) +``` + +This preserves the user-visible banner cosmetics (`'='*60` separators on lines 944, 946, 1000, 1002) and only changes the text content. + +### 1.7 Locale resolution for `retry.py` + +`retry.py` is invoked from three contexts: + +1. **Flask request handlers (sync)** — `has_request_context()` is true; `get_locale()` reads `Accept-Language`. Works. +2. **Background tasks** — the existing background-task entry points (e.g., `task_manager.run_task`) already call `set_locale(...)` per `i18n-externalize-backend-logs` (verified by reading `oasis_profile_generator.py` which uses the same pattern with `set_locale` imported on line 23). Works. +3. **Async coroutines (`retry_with_backoff_async`)** — `get_locale()` falls back to `_thread_local.locale`. Asyncio runs coroutines on the same thread by default, so the per-thread locale propagates. If the coroutine is dispatched onto a fresh executor thread without `set_locale`, the helper falls back to `zh` (the default) — still a valid string, just defaulting to Chinese. The default-fallback is acceptable here because (a) the helper still returns a non-None string, and (b) the audit only requires the *source code* to be free of Chinese literals, not that every emitted log record be English regardless of caller context. + +**Decision:** No new locale-propagation wiring needed. Document the async fallback in the design and tasks. + +## 2. Out-of-scope items (encountered during research) + +These were observed in the same files but are explicitly **not** part of ticket #24 and will not be addressed: + +- `backend/app/api/graph.py` — Chinese in `task_manager.update_task(..., message="初始化图谱构建服务...")` and similar (#24 lists only the three log calls). +- `backend/app/utils/retry.py` — Chinese in `logger.warning(...)` retry messages (lines 63–66, 115–117, 185–187) and Chinese docstrings (lines 1–3, 25–35, 36–39, 90, 156–166, 200–212). +- `backend/app/services/oasis_profile_generator.py` — Chinese in `progress_callback(... f"已完成 …")` (line 976) and Chinese docstrings/comments throughout. + +These are tracked under sibling tickets (#7 for docstrings/comments; the residual `logger.warning` in `retry.py` is a candidate for a future audit ticket). + +## 3. Implementation Approaches Considered + +### Approach A — Append-at-tail with new `log.retry` namespace (recommended) + +- New keys: `log.graph_api.m027`, `m028`, `m029`; `log.profile_generator.m024`, `m025`; new `log.retry.m001`–`m004`. +- Add `from ..utils.locale import t` to `retry.py`. +- Replace each f-string in the nine call sites with a `t(...)` call. +- Update `locales/en.json` and `locales/zh.json` in lock-step. +- **Pros:** Mirrors the conventions of #6 exactly; no risk of overwriting existing keys; minimal diff. +- **Cons:** Numbering gaps under `log.graph_api` remain (cosmetic). + +### Approach B — Fill numbering gaps in `log.graph_api` + +- Reuse missing slots `m009`, `m010`, `m012`, `m020`–`m025`. +- **Pros:** Tighter numbering. +- **Cons:** Risk of colliding with reserved-but-not-yet-merged keys from another branch; harder to review (mixed insertion sites in JSON). +- **Verdict:** Reject. The cost of conflict review is not worth the cosmetic gain. + +### Approach C — Consolidate the `print(...)` banners into the existing `log.profile_generator.m017` + +- Remove the two `print(...)` calls; rely solely on `logger.info(t(...))`. +- **Pros:** One fewer key to add. +- **Cons:** Deletes user-visible console banner behaviour (a behaviour change), violates Requirement 3.2 ("continue to print exactly two banner messages"), and is out-of-scope per ticket #24 which says "fixed (or explicitly classified as `deliberate`)" — i.e., translate, don't remove. +- **Verdict:** Reject. + +## 4. Recommendation + +Proceed with **Approach A**. + +Implementation will: + +1. Add four entries to `log.retry` (new sub-namespace) — one per `logger.error` line in `retry.py`. +2. Add three entries to `log.graph_api` — one per `build_logger` line in `graph.py`. +3. Add two entries to `log.profile_generator` — one per `print(...)` banner in `oasis_profile_generator.py`. +4. Replace all nine f-strings with `t(...)` calls; pass interpolated values as kwargs. +5. Add `from ..utils.locale import t` to `retry.py`. +6. Mirror every new key in `zh.json` with the verbatim original Chinese text. +7. Run a regex / Python audit to confirm parity and absence of CJK on the touched lines. + +## 5. Risks / open questions + +| Risk | Severity | Mitigation | +|---|---|---| +| `retry.py` async path running on a fresh thread without `set_locale` returns Chinese | Low | Documented; not a blocker for #24 acceptance, which targets *source-code* CJK absence. Any improvement is a separate ticket. | +| Adding `from ..utils.locale import t` introduces a new module import into `retry.py` (low-level utility) | Low | The `locale` module has no transitive imports of `retry.py`, so no circular-import risk. Verified by reading `locale.py`. | +| Existing test that asserts Chinese log text breaks | Low | Searched for `"开始构建图谱"` / `"图谱构建完成"` / `"图谱构建失败"` / `"开始生成Agent人设"` / `"人设生成完成"` / `"重试后仍失败"` / `"处理第"` test fixtures — none found in `backend/`. | + +## 6. Conclusion + +**Ready to proceed to design.** The gap is small: nine string-literal replacements, eleven new locale entries, one new import. The mechanics are identical to the already-merged ticket #6 work. No design uncertainty remains; design phase will simply formalise the key-naming and the per-file edit plan. diff --git a/.kiro/specs/i18n-externalize-remaining-backend-logs/requirements.md b/.kiro/specs/i18n-externalize-remaining-backend-logs/requirements.md new file mode 100644 index 00000000..79571aba --- /dev/null +++ b/.kiro/specs/i18n-externalize-remaining-backend-logs/requirements.md @@ -0,0 +1,91 @@ +# Requirements Document + +## Introduction + +After ticket #6 externalised most backend log/print messages into the project's `t()` localization helper, a small set of call sites in three modules still emit hard-coded Chinese strings. As a result, English operators reading backend logs under the `en` locale see Chinese text leaking from these residual sites. This spec finishes the job for ticket #24 by routing every remaining hard-coded Chinese log/print string in `backend/app/api/graph.py`, `backend/app/services/oasis_profile_generator.py`, and `backend/app/utils/retry.py` through `t("log..", **fmt)` and adding the corresponding entries to `locales/en.json` and `locales/zh.json`. The goal is locale-correct backend logs with zero behavioural drift in HTTP responses, control flow, or interpolated values. + +## Boundary Context + +- **In scope**: + - Replace the Chinese string literals in the nine call sites listed by ticket #24: + - `backend/app/api/graph.py:385` — `build_logger.info(f"[{task_id}] 开始构建图谱...")` + - `backend/app/api/graph.py:494` — `build_logger.info(f"[{task_id}] 图谱构建完成: graph_id={graph_id}, 节点={node_count}, 边={edge_count}")` + - `backend/app/api/graph.py:513` — `build_logger.error(f"[{task_id}] 图谱构建失败: {str(e)}")` + - `backend/app/services/oasis_profile_generator.py:945` — `print(f"开始生成Agent人设 - 共 {total} 个实体,并行数: {parallel_count}")` + - `backend/app/services/oasis_profile_generator.py:1001` — `print(f"人设生成完成!共生成 {len([p for p in profiles if p])} 个Agent")` + - `backend/app/utils/retry.py:55` — `logger.error(f"函数 {func.__name__} 在 {max_retries} 次重试后仍失败: {str(e)}")` + - `backend/app/utils/retry.py:108` — `logger.error(f"异步函数 {func.__name__} 在 {max_retries} 次重试后仍失败: {str(e)}")` + - `backend/app/utils/retry.py:179` — `logger.error(f"API调用在 {self.max_retries} 次重试后仍失败: {str(e)}")` + - `backend/app/utils/retry.py:227` — `logger.error(f"处理第 {idx + 1} 项失败: {str(e)}")` + - Add new locale keys for the externalised strings to both `locales/en.json` (English) and `locales/zh.json` (verbatim original Chinese) under the existing top-level `log.` namespaces (`log.graph_api`, `log.profile_generator`, and a new `log.retry`). + - Pass interpolated values (`task_id`, `graph_id`, `node_count`, `edge_count`, `total`, `parallel_count`, `func_name`, `max_retries`, `idx`, exception text, etc.) through `t()` keyword arguments using the helper's `{name}` placeholder syntax. +- **Out of scope**: + - Other Chinese strings in the same files that are not on the ticket's evidence list (Chinese docstrings, Chinese inline comments, the `task_manager.update_task(... message="...")` Chinese values in `graph.py`, the `logger.warning("…重试…")` calls in `retry.py`, and the in-loop `progress_callback(... f"已完成 …")` and `print(f"-" * 70 …)` decorations in `oasis_profile_generator.py`). Those are tracked elsewhere (#7 for docstrings/comments; #25 for prompt/context labels; future audit may pick up the remaining warning-level retry strings under a separate ticket). + - Any change to log levels, response status codes, control flow, public API surface, or to the `t()` helper itself. + - Adding a new locale or changing the per-thread / per-request locale resolution. + - Frontend `vue-i18n` files; this spec touches only backend usage of `t()` and the shared `locales/{en,zh}.json`. +- **Adjacent expectations**: + - The `t()` helper at `backend/app/utils/locale.py` already covers `set_locale`, `get_locale`, missing-key fallback, and per-thread locale (verified by ticket #6). New code reuses it without modification. + - The two top-level `log` sub-namespaces `log.graph_api` and `log.profile_generator` already exist in `locales/en.json` / `locales/zh.json` with `m###` numeric suffixes; new keys must use the next available `m###` slot in each existing namespace and must not collide with or overwrite any existing key. + - `retry.py` is module-level shared infrastructure used from request handlers, background tasks, and async coroutines — locale resolution must continue to work in each of these contexts without new wiring (Requirement 4 below documents this explicitly so behaviour is mechanically verified). + - Ticket #24's acceptance criterion mentions a verification script under `.kiro/specs/i18n-e2e-english-verification/audit/scripts/run_audit.sh`. That script is not present in the repository at this commit; this spec substitutes a deterministic regex audit (see Requirement 5) that is runnable from the repo root with `grep` + `python` only and that any future `run_audit.sh` can incorporate. + +## Requirements + +### Requirement 1: Externalise Remaining Chinese Log/Print Strings via `t()` + +**Objective:** As a backend operator viewing logs under the `en` locale, I want every Chinese log/print string in the nine listed call sites to be emitted via the existing `t()` helper, so that backend logs no longer leak Chinese text in English deployments. + +#### Acceptance Criteria + +1. The Backend Logging Layer shall replace the f-string argument of each of the three `build_logger.{info,error}` calls in `backend/app/api/graph.py` at lines 385, 494, and 513 with `t("log.graph_api.", task_id=task_id, ...)`, where the key is a new entry under the existing `log.graph_api` namespace. +2. The Backend Logging Layer shall replace the f-string argument of each of the two `print(...)` calls in `backend/app/services/oasis_profile_generator.py` at lines 945 and 1001 with `print(t("log.profile_generator.", ...))`, keeping the `print` call (so console-output behaviour is preserved) but routing the message text through `t()` under the existing `log.profile_generator` namespace. +3. The Backend Logging Layer shall replace the f-string argument of each of the four `logger.error` calls in `backend/app/utils/retry.py` at lines 55, 108, 179, and 227 with `t("log.retry.", **kwargs)`, introducing a new top-level sub-namespace `log.retry` that mirrors the structure of the other `log.` sub-namespaces. +4. The Backend Logging Layer shall preserve every interpolated value (`task_id`, `graph_id`, `node_count`, `edge_count`, `total`, `parallel_count`, `func.__name__`, `max_retries`, `idx`, exception text) by passing them as keyword arguments to `t(...)` and referencing them via `{name}` placeholders inside the locale dictionaries; no `f"..."` formatting, `%`-formatting, or string concatenation shall remain around the call. +5. The Backend Logging Layer shall not contain any Chinese character (Unicode range `U+4E00`–`U+9FFF`) inside the string-literal argument of any `logger.{info,warning,error,debug,exception}`, `build_logger.{info,warning,error,debug,exception}`, or `print(...)` call at the nine listed line locations after the change. + +### Requirement 2: Locale Dictionary Parity for the New Keys + +**Objective:** As a translator or developer adding a new locale, I want every newly externalised key to exist in both `locales/en.json` and `locales/zh.json` with identical nested structure, so that the locale files remain mechanically diffable. + +#### Acceptance Criteria + +1. The Locale Dictionary shall add, in `locales/en.json`, an English translation for every key introduced by Requirement 1, placed under the relevant `log.` sub-namespace (`log.graph_api`, `log.profile_generator`, or the new `log.retry`). +2. The Locale Dictionary shall add, in `locales/zh.json`, the original Chinese text (verbatim, with `{placeholder}` substitutions where the source had `f"…{var}…"`) for every key introduced by Requirement 1, under the same key path used in `en.json`. +3. The Locale Dictionary shall use the next available `m###` numeric suffix per existing sub-namespace (so it does not overwrite or shadow any pre-existing `log.graph_api.m###` or `log.profile_generator.m###` key); the new `log.retry` sub-namespace shall start its keys at `m001`. +4. The Locale Dictionary shall expose a structurally identical key tree across `locales/en.json` and `locales/zh.json` for every newly added key path: a recursive comparison of the two files' key paths (ignoring values) shall produce an empty difference for the keys this spec introduces. +5. The Locale Dictionary shall not introduce a new top-level key (the only addition is the new `log.retry` sub-key under the existing top-level `log` namespace) and shall not modify, remove, or re-order any existing key already present in `locales/{en,zh}.json`. + +### Requirement 3: Behavioural and Functional Equivalence + +**Objective:** As a reviewer, I want to confirm that swapping the message strings does not change runtime behaviour, so that this PR is purely a localisation change. + +#### Acceptance Criteria + +1. The Graph Build Pipeline shall, after the change, continue to: update `project.status` to `GRAPH_BUILDING` then `GRAPH_COMPLETED` (or `FAILED` on error), call `task_manager.update_task(...)` with the same status/progress/result payloads, and emit one log record at each of the three pre-existing log points (start, completion, failure) with identical level (`info`/`info`/`error`) and identical interpolated values; only the human-readable text and its language source shall differ. +2. The Profile Generator shall, after the change, continue to print exactly two banner messages around `concurrent.futures.ThreadPoolExecutor`-driven generation (one before, one after), retain the surrounding `'='*60` separator lines verbatim, and not emit additional log records or alter the order of `logger.info`/`logger.warning` calls. +3. The Retry Utility shall, after the change, continue to: raise the original exception after the final retry, sleep for the same backoff durations, and emit exactly one `logger.error` per call site at the same control-flow position; the helper's signature, decorator behaviour, and async/sync split shall be unchanged. +4. The Backend HTTP Layer shall return the same HTTP status code, response key set, and (for non-translated keys) value structure for `/api/graph/build` and any other endpoint that transitively triggers the touched code paths; no `jsonify(...)` payload shape shall change as a side-effect of this work. + +### Requirement 4: Locale Resolution in Background and Async Contexts + +**Objective:** As a backend service author, I want the new `t()` calls to resolve to the correct locale even when invoked from background threads or async coroutines, so that operators see consistent log language regardless of where the call originates. + +#### Acceptance Criteria + +1. When `t("log.graph_api.", ...)` is called from the `build_task` background thread inside `backend/app/api/graph.py` (started via `task_manager.run_task`), the Locale Helper shall resolve to the locale that was established for that thread (per the existing per-thread / `set_locale` mechanism), not silently fall back to the default `zh`. +2. When `t("log.retry.", ...)` is called from the synchronous `retry_with_backoff` decorator wrapping a Flask request handler, the Locale Helper shall resolve via the active Flask request context (`Accept-Language` header), consistent with how request-scoped `t()` calls behave elsewhere in the codebase. +3. When `t("log.retry.", ...)` is called from the asynchronous `retry_with_backoff_async` decorator under `asyncio`, the Locale Helper shall resolve via whichever locale source is in scope for that coroutine (request context if present; otherwise the per-thread fallback set by the caller), without raising and without requiring any new locale-propagation wiring inside `retry.py`. +4. If a `t()` call introduced by this spec references a key that is missing from both the active locale and the `zh` fallback, the Locale Helper shall continue to behave per the existing contract: emit a single deduped warning naming the key and locale, and return the key string itself (never `None`, never raise). + +### Requirement 5: Verification and Regression Guards + +**Objective:** As a reviewer of this PR, I want repeatable mechanical checks that prove the in-scope files are clean of stray hard-coded Chinese log/print strings on those nine lines, so that the acceptance criteria can be re-validated on every future change. + +#### Acceptance Criteria + +1. The Verification Procedure shall, when run against the repository, report zero matches of any Unicode CJK character (range `U+4E00`–`U+9FFF`) on the nine specific lines covered by Requirement 1 in their post-change form (i.e., `grep -P "[一-鿿]"` against the replaced lines returns no hits). +2. The Verification Procedure shall, when run against `locales/en.json` and `locales/zh.json`, confirm via a Python `json.load` + recursive key walk that every newly introduced key path exists in both files, and exit non-zero if a key path is present in only one of them. +3. The Verification Procedure shall confirm via Python that for each new key in `locales/zh.json` whose source f-string contained an `{var}` placeholder, the same `{var}` placeholder appears in the new English translation in `locales/en.json` (so interpolation is not silently dropped during translation). +4. The Verification Procedure shall require only tools already available in the dev environment (`grep`, `python3`, optional `jq`) — no new runtime or dev dependencies shall be added by this spec. +5. The Backend Test Suite shall continue to pass (`uv run python -m pytest`) after the change, with no new failures introduced; in particular, any pre-existing tests that assert the prior Chinese log/print text shall be updated to assert via the same `t()` lookup or an English translation rather than removed. diff --git a/.kiro/specs/i18n-externalize-remaining-backend-logs/research.md b/.kiro/specs/i18n-externalize-remaining-backend-logs/research.md new file mode 100644 index 00000000..79a0f8d3 --- /dev/null +++ b/.kiro/specs/i18n-externalize-remaining-backend-logs/research.md @@ -0,0 +1,91 @@ +# Research & Design Decisions + +## Summary + +- **Feature**: `i18n-externalize-remaining-backend-logs` +- **Discovery Scope**: Simple Addition (extending an established convention from ticket #6) +- **Key Findings**: + - The `t()` helper, per-thread locale, and missing-key fallback are already in place in `backend/app/utils/locale.py` and require no changes. + - The convention `t("log..m###", **kwargs)` with `{name}` placeholders is already used by all sibling modules; this spec strictly extends it. + - No existing test fixtures reference any of the nine Chinese strings to be replaced. + +## Research Log + +### Existing locale namespace structure +- **Context**: Need to add new keys without colliding with existing entries. +- **Sources Consulted**: `locales/en.json`, `locales/zh.json`, `.kiro/specs/i18n-externalize-backend-logs/requirements.md`. +- **Findings**: + - `log.graph_api` is densely populated `m006`–`m019` plus `m026`. Free contiguous slots starting at the tail: `m027`, `m028`, `m029`. + - `log.profile_generator` is densely populated `m001`–`m023`. Free slots: `m024`, `m025`. + - `log.retry` does not exist; introducing it as a sibling to other `log.` namespaces matches the existing pattern. +- **Implications**: New keys append at the tail per existing namespace; `log.retry` is created fresh starting at `m001`. + +### Locale resolution in async / background contexts +- **Context**: `retry.py` is shared infrastructure invoked from sync request handlers, background tasks, and async coroutines. +- **Sources Consulted**: `backend/app/utils/locale.py`, `backend/app/services/oasis_profile_generator.py` (uses `set_locale`), Flask docs (request-context behaviour). +- **Findings**: + - `get_locale()` returns the request-context `Accept-Language` header when a Flask request is active, the per-thread locale otherwise, and `'zh'` as the default. + - Asyncio coroutines run on the same OS thread by default, so the per-thread locale set by the parent function propagates into `await`-driven calls. + - Missing-key fallback returns the key string and emits a deduped warning — never raises. +- **Implications**: No new locale-propagation wiring needed inside `retry.py`. Adding `from ..utils.locale import t` is sufficient. + +### `print(...)` vs `logger` for the OASIS banners +- **Context**: Two `print(...)` banner statements at `oasis_profile_generator.py:945` and `:1001` decorate stdout. Should we keep them as `print` or fold them into existing `logger.info` calls? +- **Sources Consulted**: `backend/app/services/oasis_profile_generator.py:943` (existing `logger.info(t("log.profile_generator.m017", …))`), ticket #24 acceptance ("each `file:line` is fixed"). +- **Findings**: + - The existing `logger.info` and the `print(...)` are emitting the same logical event in two channels. The banner adds `'='*60` separators on the surrounding lines, which is purely a console-cosmetic; replacing the print with a logger call would lose the visual banner. + - Ticket #24 wants externalisation, not removal. +- **Implications**: Keep both calls. Wrap the `print(f"...")` argument with `t(...)`. Introduce dedicated keys (`m024`, `m025`) so the banner copy is decoupled from the structured log copy at `m017`. + +## Architecture Pattern Evaluation + +| Option | Description | Strengths | Risks / Limitations | Notes | +|--------|-------------|-----------|---------------------|-------| +| Append-at-tail (selected) | Add new `m###` keys at the next contiguous slot per namespace; create `log.retry` fresh | Mirrors #6 convention; minimal diff; no overwrite risk | Numbering gaps under `log.graph_api` remain | Aligns with steering principle of preserving established conventions | +| Fill numbering gaps | Reuse missing slots `m009`, `m010`, etc. | Tighter numbering | Risk of colliding with reserved-but-not-yet-merged keys; mixed insertion sites complicate review | Rejected | +| Consolidate banner prints into logger | Remove the `print(...)` calls; use only `logger.info(t(...))` | One fewer key | Behaviour change (loses console banner); violates Requirement 3.2 | Rejected | + +## Design Decisions + +### Decision: Add a new `log.retry` sub-namespace rather than reusing `log.bootstrap` or `log.graph_api` +- **Context**: `retry.py` is a generic utility used by many callers; it does not belong to a single domain. +- **Alternatives Considered**: + 1. Place keys under `log.bootstrap` — wrong domain (bootstrap is for app startup logs). + 2. Place keys under each caller's namespace — would require dynamic key resolution, adding complexity. + 3. New `log.retry` sub-namespace — clean and self-describing. +- **Selected Approach**: Introduce `log.retry.m001`–`m004` as a peer of `log.graph_api`, `log.profile_generator`, etc. +- **Rationale**: Matches the per-domain naming scheme already in use; locates retry-specific copy in one place. +- **Trade-offs**: Adds one new sub-namespace under `log`, but does not change the top-level key set. +- **Follow-up**: Verify that no other module already defines `log.retry` (verified: it does not exist). + +### Decision: Wrap `print(...)` arguments rather than removing the prints +- **Context**: Ticket #24 mandates externalisation of the listed call sites; behaviour preservation is in scope. +- **Alternatives Considered**: + 1. Keep `print(t("..."))` — preserves console banner, externalises text. + 2. Remove `print(...)`; rely on `logger.info` only — drops banner. +- **Selected Approach**: Option 1. The `'='*60` separator lines stay; only the message text routes through `t(...)`. +- **Rationale**: Minimum change; respects Requirement 3.2. +- **Trade-offs**: None significant. +- **Follow-up**: Confirm during validation that the surrounding separator prints (`print(f"\n{'='*60}")`) are not on the ticket's evidence list (they are not — they contain only ASCII). + +### Decision: Pass exception text as a keyword argument named `e` (not `error`) +- **Context**: Existing `log.profile_generator` keys use `e=str(e)` and `error=...` inconsistently. Need to pick one convention to remain consistent. +- **Alternatives Considered**: + 1. Use `e` — matches `log.profile_generator.m003`, `m005`, `m008`, `m012`. + 2. Use `error` — matches `log.profile_generator.m018`. +- **Selected Approach**: Use `e` for raw exception strings (the more common pattern). Where a separate label is more readable, use a domain-specific name (e.g. `error` is fine when it carries semantic weight). +- **Rationale**: Match the dominant existing convention. +- **Trade-offs**: None. +- **Follow-up**: Use `e` throughout the new keys. + +## Risks & Mitigations + +- **Async retry on a fresh thread without `set_locale`** — Falls back to `'zh'`. Acceptable: ticket #24 acceptance targets *source-code* CJK absence. Documented for future ticket if needed. +- **Circular imports when adding `from ..utils.locale import t` to `retry.py`** — `locale.py` imports only `json`, `logging`, `os`, `threading`, and `flask` (no project modules). No circular risk. +- **Test-suite breakage from changed log text** — No fixtures match the Chinese strings. Verified by grep of `backend/`. Low risk. + +## References + +- Sibling spec: `.kiro/specs/i18n-externalize-backend-logs/requirements.md` — established convention. +- Ticket #6 (closed) and ticket #24 (this work). +- `backend/app/utils/locale.py` — `t()` contract. diff --git a/.kiro/specs/i18n-externalize-remaining-backend-logs/spec.json b/.kiro/specs/i18n-externalize-remaining-backend-logs/spec.json new file mode 100644 index 00000000..af5c73f0 --- /dev/null +++ b/.kiro/specs/i18n-externalize-remaining-backend-logs/spec.json @@ -0,0 +1,24 @@ +{ + "feature_name": "i18n-externalize-remaining-backend-logs", + "created_at": "2026-05-07T22:24:20Z", + "updated_at": "2026-05-07T22:50:00Z", + "language": "en", + "phase": "tasks-generated", + "approvals": { + "requirements": { + "generated": true, + "approved": true + }, + "design": { + "generated": true, + "approved": true + }, + "tasks": { + "generated": true, + "approved": true + } + }, + "ready_for_implementation": true, + "ticket": 24, + "related_tickets": [10, 6] +} diff --git a/.kiro/specs/i18n-externalize-remaining-backend-logs/tasks.md b/.kiro/specs/i18n-externalize-remaining-backend-logs/tasks.md new file mode 100644 index 00000000..fe9ccbfe --- /dev/null +++ b/.kiro/specs/i18n-externalize-remaining-backend-logs/tasks.md @@ -0,0 +1,63 @@ +# Implementation Plan + +- [x] 1. Add three new keys to `log.graph_api` in both locale files + - In `locales/en.json`, append `m027`, `m028`, `m029` under `log.graph_api` with the English translations from the design's key-mapping table + - In `locales/zh.json`, append the same three keys under `log.graph_api` with the verbatim original Chinese text (rewriting `f"...{var}..."` as `"...{var}..."`) + - Confirm via `python3 -m json.tool` that both files round-trip without reformatting other keys + - Observable completion: `python3 -c "import json; en=json.load(open('locales/en.json'))['log']['graph_api']; zh=json.load(open('locales/zh.json'))['log']['graph_api']; assert {'m027','m028','m029'} <= set(en) <= set(zh) | set(en); print('ok')"` exits zero + - _Requirements: 2.1, 2.2, 2.3, 2.5_ + +- [x] 2. Replace the three Chinese f-strings in `backend/app/api/graph.py` with `t()` calls + - Line 385: replace `f"[{task_id}] 开始构建图谱..."` with `t("log.graph_api.m027", task_id=task_id)` + - Line 494: replace the build-completion f-string with `t("log.graph_api.m028", task_id=task_id, graph_id=graph_id, node_count=node_count, edge_count=edge_count)` + - Line 513: replace the build-failure f-string with `t("log.graph_api.m029", task_id=task_id, e=str(e))` + - Do not change log levels, surrounding `task_manager.update_task` calls, or control flow + - Observable completion: `grep -nP "[一-鿿]" backend/app/api/graph.py | grep -E "^(385|494|513):"` returns no matches; `python3 -c "import ast; ast.parse(open('backend/app/api/graph.py').read())"` succeeds + - _Requirements: 1.1, 1.4, 1.5, 3.1, 3.4_ + - _Depends: 1_ + +- [x] 3. Add two new keys to `log.profile_generator` in both locale files + - In `locales/en.json`, append `m024` and `m025` under `log.profile_generator` per the design table + - In `locales/zh.json`, mirror with the verbatim original Chinese banner text (using `{count}` placeholder where the source had `len([p for p in profiles if p])`) + - Observable completion: same key-presence assertion as Task 1 but for `m024`, `m025` + - _Requirements: 2.1, 2.2, 2.3, 2.5_ + +- [x] 4. Replace the two `print(...)` banner strings in `backend/app/services/oasis_profile_generator.py` with `t()` calls + - Line 945: replace `f"开始生成Agent人设 - 共 {total} 个实体,并行数: {parallel_count}"` with `t("log.profile_generator.m024", total=total, parallel_count=parallel_count)` + - Line 1001: replace `f"人设生成完成!共生成 {len([p for p in profiles if p])} 个Agent"` with `t("log.profile_generator.m025", count=len([p for p in profiles if p]))` + - Keep the surrounding `print(f"\n{'='*60}")` separator lines exactly as they are; keep both `print(...)` calls (do not collapse into the existing `logger.info` at line 943) + - Observable completion: `grep -nP "[一-鿿]" backend/app/services/oasis_profile_generator.py | grep -E "^(945|1001):"` returns no matches; the file still parses with `ast.parse` + - _Requirements: 1.2, 1.4, 1.5, 3.2_ + - _Depends: 3_ + +- [x] 5. Add a new `log.retry` sub-namespace with four keys to both locale files + - In `locales/en.json`, add `log.retry` as a peer of the other `log.` sub-namespaces, with keys `m001`–`m004` per the design table + - In `locales/zh.json`, mirror the same `log.retry` sub-namespace with verbatim original Chinese + - Use placeholder names `func_name`, `max_retries`, `index`, `e` consistently across both files (note: the source `idx + 1` is bound to `index=idx + 1` at the call site — placeholder names cannot contain `+`) + - Observable completion: `python3 -c "import json; en=json.load(open('locales/en.json'))['log']['retry']; zh=json.load(open('locales/zh.json'))['log']['retry']; assert set(en)==set(zh)=={'m001','m002','m003','m004'}; print('ok')"` exits zero + - _Requirements: 2.1, 2.2, 2.3, 2.5_ + +- [x] 6. Externalise the four `logger.error` strings in `backend/app/utils/retry.py` + - Add `from .locale import t` at the top of `retry.py` (use the same relative-import depth as `from ..utils.logger import get_logger` already in the file — i.e., `from .locale import t`) + - Line 55: replace `f"函数 {func.__name__} 在 {max_retries} 次重试后仍失败: {str(e)}"` with `t("log.retry.m001", func_name=func.__name__, max_retries=max_retries, e=str(e))` + - Line 108: replace `f"异步函数 {func.__name__} 在 {max_retries} 次重试后仍失败: {str(e)}"` with `t("log.retry.m002", func_name=func.__name__, max_retries=max_retries, e=str(e))` + - Line 179: replace `f"API调用在 {self.max_retries} 次重试后仍失败: {str(e)}"` with `t("log.retry.m003", max_retries=self.max_retries, e=str(e))` + - Line 227: replace `f"处理第 {idx + 1} 项失败: {str(e)}"` with `t("log.retry.m004", index=idx + 1, e=str(e))` + - Do not modify the `logger.warning(...)` retry-attempt messages or the docstrings (out of scope for #24) + - Observable completion: `grep -nP "[一-鿿]" backend/app/utils/retry.py | grep -E "^(55|108|179|227):"` returns no matches; `python3 -c "import ast; ast.parse(open('backend/app/utils/retry.py').read())"` succeeds; `python3 -c "from backend.app.utils import retry; print(retry.t)"` resolves the import + - _Requirements: 1.3, 1.4, 1.5, 3.3, 4.1, 4.2, 4.3, 4.4_ + - _Depends: 5_ + +- [x] 7. Run mechanical verification across the change + - From the repo root, verify zero CJK on the nine affected lines: + ``` + grep -nP "[一-鿿]" backend/app/api/graph.py | grep -E "^(385|494|513):" || echo OK_graph + grep -nP "[一-鿿]" backend/app/services/oasis_profile_generator.py | grep -E "^(945|1001):" || echo OK_profile + grep -nP "[一-鿿]" backend/app/utils/retry.py | grep -E "^(55|108|179|227):" || echo OK_retry + ``` + Each should print `OK_*`. + - Run a Python parity check that asserts every newly-added key path exists in both `locales/en.json` and `locales/zh.json` and that every `{name}` placeholder in the `zh` value also appears in the `en` value (and vice versa). + - Run `cd backend && uv run python -m pytest` and confirm no new failures relative to the pre-change baseline. + - Observable completion: all three grep assertions print `OK_*`; the parity Python check exits zero; the pytest run reports the same pass/fail count as on `main` for these files. + - _Requirements: 1.5, 2.4, 5.1, 5.2, 5.3, 5.4, 5.5_ + - _Depends: 2, 4, 6_ diff --git a/backend/app/api/graph.py b/backend/app/api/graph.py index d4cafa12..669b816e 100644 --- a/backend/app/api/graph.py +++ b/backend/app/api/graph.py @@ -382,7 +382,7 @@ def build_graph(): def build_task(): build_logger = get_logger('mirofish.build') try: - build_logger.info(f"[{task_id}] 开始构建图谱...") + build_logger.info(t("log.graph_api.m027", task_id=task_id)) task_manager.update_task( task_id, status=TaskStatus.PROCESSING, @@ -491,7 +491,13 @@ def build_graph(): node_count = graph_data.get("node_count", 0) edge_count = graph_data.get("edge_count", 0) - build_logger.info(f"[{task_id}] 图谱构建完成: graph_id={graph_id}, 节点={node_count}, 边={edge_count}") + build_logger.info(t( + "log.graph_api.m028", + task_id=task_id, + graph_id=graph_id, + node_count=node_count, + edge_count=edge_count, + )) # 完成 task_manager.update_task( @@ -510,7 +516,7 @@ def build_graph(): except Exception as e: # 更新项目状态为失败 - build_logger.error(f"[{task_id}] 图谱构建失败: {str(e)}") + build_logger.error(t("log.graph_api.m029", task_id=task_id, e=str(e))) build_logger.debug(traceback.format_exc()) project.status = ProjectStatus.FAILED diff --git a/backend/app/services/oasis_profile_generator.py b/backend/app/services/oasis_profile_generator.py index 1cf9158a..d80f8df3 100644 --- a/backend/app/services/oasis_profile_generator.py +++ b/backend/app/services/oasis_profile_generator.py @@ -942,7 +942,7 @@ class OasisProfileGenerator: logger.info(t("log.profile_generator.m017", total=total, parallel_count=parallel_count)) print(f"\n{'='*60}") - print(f"开始生成Agent人设 - 共 {total} 个实体,并行数: {parallel_count}") + print(t("log.profile_generator.m024", total=total, parallel_count=parallel_count)) print(f"{'='*60}\n") # 使用线程池并行执行 @@ -998,7 +998,7 @@ class OasisProfileGenerator: save_profiles_realtime() print(f"\n{'='*60}") - print(f"人设生成完成!共生成 {len([p for p in profiles if p])} 个Agent") + print(t("log.profile_generator.m025", count=len([p for p in profiles if p]))) print(f"{'='*60}\n") return profiles diff --git a/backend/app/utils/retry.py b/backend/app/utils/retry.py index 819b1cfc..23ecd45c 100644 --- a/backend/app/utils/retry.py +++ b/backend/app/utils/retry.py @@ -8,6 +8,7 @@ import random import functools from typing import Callable, Any, Optional, Type, Tuple from ..utils.logger import get_logger +from .locale import t logger = get_logger('mirofish.retry') @@ -52,7 +53,12 @@ def retry_with_backoff( last_exception = e if attempt == max_retries: - logger.error(f"函数 {func.__name__} 在 {max_retries} 次重试后仍失败: {str(e)}") + logger.error(t( + "log.retry.m001", + func_name=func.__name__, + max_retries=max_retries, + e=str(e), + )) raise # 计算延迟 @@ -105,7 +111,12 @@ def retry_with_backoff_async( last_exception = e if attempt == max_retries: - logger.error(f"异步函数 {func.__name__} 在 {max_retries} 次重试后仍失败: {str(e)}") + logger.error(t( + "log.retry.m002", + func_name=func.__name__, + max_retries=max_retries, + e=str(e), + )) raise current_delay = min(delay, max_delay) @@ -176,7 +187,11 @@ class RetryableAPIClient: last_exception = e if attempt == self.max_retries: - logger.error(f"API调用在 {self.max_retries} 次重试后仍失败: {str(e)}") + logger.error(t( + "log.retry.m003", + max_retries=self.max_retries, + e=str(e), + )) raise current_delay = min(delay, self.max_delay) @@ -224,7 +239,7 @@ class RetryableAPIClient: results.append(result) except Exception as e: - logger.error(f"处理第 {idx + 1} 项失败: {str(e)}") + logger.error(t("log.retry.m004", index=idx + 1, e=str(e))) failures.append({ "index": idx, "item": item, diff --git a/locales/en.json b/locales/en.json index 0c924b04..b9f6ab1c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -772,7 +772,9 @@ "m020": "Exception while processing entity {entity}: {str}", "m021": "Saved {len} Twitter profiles to {file_path} (OASIS CSV format)", "m022": "Saved {len} Reddit profiles to {file_path} (JSON format with user_id field)", - "m023": "save_profiles_to_json is deprecated; use save_profiles instead" + "m023": "save_profiles_to_json is deprecated; use save_profiles instead", + "m024": "Starting agent profile generation — {total} entities, parallelism: {parallel_count}", + "m025": "Profile generation complete — generated {count} agents" }, "simulation_config": { "m001": "Smart simulation config generation started: simulation_id={simulation_id}, entities={len}", @@ -920,7 +922,10 @@ "m017": "=== Graph build started ===", "m018": "Configuration error: {errors}", "m019": "Request parameters: project_id={project_id}", - "m026": "Created graph build task: task_id={task_id}, project_id={project_id}" + "m026": "Created graph build task: task_id={task_id}, project_id={project_id}", + "m027": "[{task_id}] Starting graph build...", + "m028": "[{task_id}] Graph build completed: graph_id={graph_id}, nodes={node_count}, edges={edge_count}", + "m029": "[{task_id}] Graph build failed: {e}" }, "bootstrap": { "m001": "MiroFish backend starting...", @@ -929,6 +934,12 @@ "m004": "Request body: {request}", "m005": "Response: {response}", "m006": "MiroFish backend started" + }, + "retry": { + "m001": "Function {func_name} still failing after {max_retries} retries: {e}", + "m002": "Async function {func_name} still failing after {max_retries} retries: {e}", + "m003": "API call still failing after {max_retries} retries: {e}", + "m004": "Failed processing item #{index}: {e}" } }, "report": { diff --git a/locales/zh.json b/locales/zh.json index 961d66ef..99229863 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -772,7 +772,9 @@ "m020": "处理实体 {entity} 时发生异常: {str}", "m021": "已保存 {len} 个Twitter Profile到 {file_path} (OASIS CSV格式)", "m022": "已保存 {len} 个Reddit Profile到 {file_path} (JSON格式,包含user_id字段)", - "m023": "save_profiles_to_json已废弃,请使用save_profiles方法" + "m023": "save_profiles_to_json已废弃,请使用save_profiles方法", + "m024": "开始生成Agent人设 - 共 {total} 个实体,并行数: {parallel_count}", + "m025": "人设生成完成!共生成 {count} 个Agent" }, "simulation_config": { "m001": "开始智能生成模拟配置: simulation_id={simulation_id}, 实体数={len}", @@ -920,7 +922,10 @@ "m017": "=== 开始构建图谱 ===", "m018": "配置错误: {errors}", "m019": "请求参数: project_id={project_id}", - "m026": "创建图谱构建任务: task_id={task_id}, project_id={project_id}" + "m026": "创建图谱构建任务: task_id={task_id}, project_id={project_id}", + "m027": "[{task_id}] 开始构建图谱...", + "m028": "[{task_id}] 图谱构建完成: graph_id={graph_id}, 节点={node_count}, 边={edge_count}", + "m029": "[{task_id}] 图谱构建失败: {e}" }, "bootstrap": { "m001": "MiroFish Backend 启动中...", @@ -929,6 +934,12 @@ "m004": "请求体: {request}", "m005": "响应: {response}", "m006": "MiroFish Backend 启动完成" + }, + "retry": { + "m001": "函数 {func_name} 在 {max_retries} 次重试后仍失败: {e}", + "m002": "异步函数 {func_name} 在 {max_retries} 次重试后仍失败: {e}", + "m003": "API调用在 {max_retries} 次重试后仍失败: {e}", + "m004": "处理第 {index} 项失败: {e}" } }, "report": {