Skip to content

Add locale mechanism for Work System auto-filed issue text - #1695

Open
takanorinishida wants to merge 3 commits into
danielmiessler:mainfrom
takanorinishida:work-issue-i18n
Open

Add locale mechanism for Work System auto-filed issue text#1695
takanorinishida wants to merge 3 commits into
danielmiessler:mainfrom
takanorinishida:work-issue-i18n

Conversation

@takanorinishida

Copy link
Copy Markdown

Summary

WorkSweep.ts and ReminderRouter.hook.ts write every GitHub issue body they auto-file with hardcoded English strings. There was no way to get issue text in another language without hand-patching SYSTEM code.

This PR adds a locale mechanism, all opt-in and English-unchanged by default:

  • hooks/lib/work-strings.ts (new) — a t(lang, key, vars) helper with a built-in EN bundle (a byte-for-byte port of the strings that used to be inline) and key-level fallback: a locale only needs to cover the keys it has a translation for; anything missing — or the whole locale file missing/malformed — falls back to EN. Never throws.
  • hooks/lib/work-config.ts — adds WORK.ISSUE_LANGUAGE (default "en") to the existing config loader, following the same extractScalar/fail-open pattern already used for CAPTURE_NATIVE etc.
  • WorkSweep.ts / ReminderRouter.hook.ts — the four sub-sweep bodies and the reminder/research/queue body now route through t(). Control flow, dedup logic, and label construction are untouched.
  • LIFEOS/TOOLS/InitWorkLocale.ts (new) — a deterministic scaffold/audit CLI (no LLM calls, matching WorkSweep.ts's own gh + fs-only style): InitWorkLocale.ts <lang> writes a full locale file (every key present, English placeholders — valid and equivalent to en output the moment it's created), --check reports missing/extra keys against the current EN bundle, --list shows installed locales + translation coverage. The <lang> argument is validated against a BCP-47-ish regex before it ever becomes part of a filesystem path (it's used directly as a filename), plus a resolved-path containment check as a second layer.
  • Docs: a new "Localization" section in WorkSystem.md, and a new INTERFACE row in SystemUserBoundary.md for USER/CONFIG/locales/<lang>.json (schema ships in SYSTEM via the EN bundle, values are USER data — same shape as the existing USER/CONFIG/memory-review.json precedent). Locale files resolve via LifeosConfig.ts's paiUserDir(), never a literal LIFEOS/USER/... path.
  • A separate small commit fixes a pre-existing bug found while touching this code: sweepGoals() and sweepBpeCadence() hardcoded "Agent:kai" instead of getDAName() like every other label site in the file, so any principal whose labels.yml doesn't have an Agent:kai entry (i.e. anyone who isn't named "kai") silently loses the Agent label via filterLabels()'s filter-to-existing behavior.

What stays English regardless of locale, by design: labels (Type:*, Status:*, Priority:*, Property:*, Agent:*) and the bracketed title prefixes ([Goal], [Sweep], [Project-Check], [BPE], [Native], [Reminder], [Research], [Queue]) — these are classification tokens that dedup logic and the Pulse kanban badge depend on, not prose.

Known limitations (documented in WorkSystem.md, apply to every non-English locale alike): ReminderRouter.hook.ts's trigger phrases (remind me to …) and relative-date parsing (tomorrow, next friday) are intentionally left English-only in this PR — translating those would mean feeding locale-supplied regexes into a hook that runs on every prompt, which felt like a separate, more debatable change from "translate the output text." A non-English request still works and still gets the Due: date if it uses an English trigger verb, e.g. remind me to <anything, any language> tomorrow. classifyType()'s English keyword classification and the lack of ICU-style plural rules (interpolation is {name} literal substitution only) are the other two known gaps, called out in the docs with a suggested workaround (phrase around the count).

Testing evidence

Fidelity check (en output unchanged): wrote a standalone script that reconstructs the original hardcoded body/title-building logic side by side with the new t()-based version at lang="en", fed both identical synthetic inputs across all 4 sub-sweeps (native/algorithm session capture, with/without a stated goal, project-check, goal, BPE never-run/overdue) and both ReminderRouter cases (with/without a due date), and diffed. All 11 cases matched byte-for-byte.

Real end-to-end on a private Work System repo (repo name redacted; same setup WorkSweep.ts/ReminderRouter.hook.ts already target):

$ bun ~/.claude/LIFEOS/TOOLS/InitWorkLocale.ts ja
InitWorkLocale: wrote .../USER/CONFIG/locales/ja.json (23 keys, all English placeholders)

# translated all 23 keys, then:
$ bun ~/.claude/LIFEOS/TOOLS/InitWorkLocale.ts ja --check
  keys: 23 in file, 23 in code
  translated: 23/23 (differ from English default)
  missing: none
  extra: none
  in sync with hooks/lib/work-strings.ts EN bundle

# WORK.ISSUE_LANGUAGE: ja in config.yaml, then a real BPE sweep run:
$ bun ~/.claude/LIFEOS/TOOLS/WorkSweep.ts
+ created #NN [BPE] Subtraction pass due — <ja overdue string> [bpe-audit-due]

gh issue view on the created issue confirmed: fully Japanese body, English labels including the corrected Agent:<DA name> (confirming the bugfix), and the English [BPE]/[bpe-audit-due] title tokens intact. Same pattern verified for ReminderRouter.hook.ts with remind me to <Japanese text> tomorrow — Japanese body, correct Due: date, English labels. All test issues closed after inspection.

Also verified:

  • Two consecutive WorkSweep.ts --dry-run runs produce 0 new issues (tag-based dedup for goals/BPE/sessions is untouched and still works with locale-translated bodies)
  • Precision-negative prompts (both Japanese and English non-matching text) still don't fire ReminderRouter.hook.ts
  • A real run through the production systemd --user timer entrypoint (com.lifeos.worksweep.service) exits status=0/SUCCESS with errors:[]
  • A throwaway third locale (de, one key translated) proved key-level fallback isn't Japanese-specific — the untranslated key fell back to en, and the presence of de.json didn't affect en/ja output
  • bun build --target=bun passes clean on all four changed/new TypeScript files
  • Path-traversal args (../../evil, ja/../x) to InitWorkLocale.ts are rejected before any file write

🤖 Generated with Claude Code

takanorinishida and others added 3 commits July 29, 2026 15:02
WorkSweep.ts and ReminderRouter.hook.ts wrote every issue body string
in English, hardcoded inline. Non-English LifeOS users had no way to
get issue text in their own language without hand-patching SYSTEM code.

Adds hooks/lib/work-strings.ts: a key-value locale bundle with a
built-in EN default (a byte-for-byte port of the strings that used to
be inline) and key-level fallback to a user-supplied
USER/CONFIG/locales/<lang>.json. Never throws — a missing, malformed,
or partial locale file degrades gracefully to English for whatever
keys it doesn't cover.

WORK.ISSUE_LANGUAGE in config.yaml selects the locale (default "en",
unchanged output). Labels (Type:*, Status:*, Priority:*, Property:*,
Agent:*) and the bracketed title prefixes ([Goal], [Sweep],
[Project-Check], [BPE], [Reminder], [Research], [Queue], [Native]) are
classification tokens that dedup logic and the Pulse kanban badge
depend on — they stay English regardless of locale.

USER/CONFIG/locales/<lang>.json is documented as a new INTERFACE path
in SystemUserBoundary.md (schema ships in SYSTEM via the EN bundle,
values are USER data), matching the existing USER/CONFIG/*.json
precedent (memory-review.json). Resolved via LifeosConfig.ts's
paiUserDir(), never a literal LIFEOS/USER/ path.

Reminder trigger phrases and relative-date parsing in
ReminderRouter.hook.ts are untouched — still English-only by design.
A non-English request routes fine if it uses an English trigger verb
("remind me to <anything> tomorrow"); only the resulting issue body
language and free-text subject change. Documented as a known
limitation, not a partial implementation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hand-writing a USER/CONFIG/locales/<lang>.json against the key set in
hooks/lib/work-strings.ts's EN bundle means keeping two things in sync
by memory. This CLI removes that: it generates a full scaffold (every
key present, English placeholders) so a locale file is always valid
JSON with complete coverage from the moment it's created, and a
--check mode reports missing/extra keys against the current EN bundle
so drift (e.g. after a LifeOS update adds a key) is visible instead of
silent.

  InitWorkLocale.ts <lang>            scaffold
  InitWorkLocale.ts <lang> --force    overwrite
  InitWorkLocale.ts <lang> --check    report drift, don't write
  InitWorkLocale.ts --list            enumerate installed locales + coverage %

<lang> is validated against a BCP-47-ish regex before it ever becomes
part of a filesystem path (it's used directly as a filename), with a
resolved-path containment check as a second layer. Zero LLM calls —
deterministic file generation only, matching the existing WorkSweep.ts
/ BootstrapLabels.ts pattern (gh + fs, nothing else). Doesn't write
WORK.ISSUE_LANGUAGE itself; that's left for the principal to set after
reviewing the translation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sweepGoals() and sweepBpeCadence() hardcoded "Agent:kai" instead of
using getDAName() like every other label-building site in this file
(sweepSessions, sweepProjectChecks). Any repo whose labels.yml doesn't
have an Agent:kai entry — i.e. every principal who isn't named "kai" —
silently drops the Agent label via filterLabels()'s
filter-to-existing-labels behavior, with no error surfaced anywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant