fix(i18n): externalize remaining chinese backend log strings
Replace the last hard-coded Chinese log/print strings in the Flask graph API, OASIS profile generator, and retry utility with calls to the existing t() helper, completing the backend i18n coverage started by ticket #6 so EN-locale operators see English logs end to end. Adds nine entries to locales/{en,zh}.json: log.graph_api.m027-m029, log.profile_generator.m024-m025, and a new log.retry.m001-m004 sub-namespace for the retry utility. Closes #24
This commit is contained in:
parent
063b7fb17d
commit
e60a5a93d3
|
|
@ -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.<domain>.<key>", **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.<domain>.<key>", **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.<domain>.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.<domain>.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<br/>build_logger.{info,error}] -->|t("log.graph_api.mNNN", ...)| L[t() helper<br/>backend/app/utils/locale.py]
|
||||
B[oasis_profile_generator.py:945/1001<br/>print(...)] -->|t("log.profile_generator.mNNN", ...)| L
|
||||
C[retry.py:55/108/179/227<br/>logger.error] -->|t("log.retry.mNNN", ...)| L
|
||||
L --> EN[locales/en.json<br/>log.graph_api.m027-m029<br/>log.profile_generator.m024-m025<br/>log.retry.m001-m004]
|
||||
L --> ZH[locales/zh.json<br/>same key paths<br/>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.<key>", **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.<key>", **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.<key>", **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.<key>", ...)` | Build pipeline log emission |
|
||||
| 1.2 | Replace `oasis_profile_generator.py` banner prints via `t()` | `OasisProfileGenerator.generate_profiles_parallel` | `t("log.profile_generator.<key>", ...)` | 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.<key>", ...)` | 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.
|
||||
|
|
@ -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.<domain>.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.
|
||||
|
|
@ -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.<domain>.<key>", **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.<domain>` 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.<key>", 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.<key>", ...))`, 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.<key>", **kwargs)`, introducing a new top-level sub-namespace `log.retry` that mirrors the structure of the other `log.<domain>` 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.<domain>` 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.<key>", ...)` 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.<key>", ...)` 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.<key>", ...)` 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.
|
||||
|
|
@ -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.<domain>.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.<domain>` 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.
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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.<domain>` 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_
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue