Skip to content

feat(i18n): add hooks/lib/locale.ts, wire praise-vocabulary union into SatisfactionCapture - #1706

Open
takanorinishida wants to merge 1 commit into
danielmiessler:mainfrom
takanorinishida:feature/i18n/locale-core
Open

feat(i18n): add hooks/lib/locale.ts, wire praise-vocabulary union into SatisfactionCapture#1706
takanorinishida wants to merge 1 commit into
danielmiessler:mainfrom
takanorinishida:feature/i18n/locale-core

Conversation

@takanorinishida

Copy link
Copy Markdown

What this does

Generalizes hooks/lib/work-strings.ts's design contract (#1695: key-level fallback, fail-open, paiUserDir()-resolved locale files) into a reusable lib any surface can adopt.

Design

Two lookup shapes, because prose and vocabulary need opposite fallback semantics — this is the actual design contribution, not just a rename of work-strings' machinery:

  • t(lang, key, fallback, vars) — OVERRIDE. A locale bundle replaces the caller's default for a key it translates. No baked-in EN dictionary (unlike work-strings.ts, which legitimately owns one scoped to Work System issue bodies) — the caller supplies its own default, since a generic lib has no single key-space to own.
  • tList(lang, key) — UNION. A locale bundle's array values are ADDED to the caller's own list, never replacing it. A Japanese locale should still recognize "perfect" as praise, not lose English vocabulary by opting in — this is what makes "locale unset → output unchanged" provable by construction rather than by convention: with no bundle, tList() returns [] and the caller's list is untouched.

resolveLang() priority: explicit arg > LIFEOS_CONFIG.toml [principal].language (new optional field) > WORK.ISSUE_LANGUAGE (read independently, not by importing work-config.ts's unexported loadIssueLanguage() — keeps this lib standalone rather than reaching into Work System internals for one value) > "en". Every step is try/catch-guarded and never throws, same fail-open contract work-strings.ts uses for locale bundles.

First consumer

SatisfactionCapture.hook.ts's positive-praise fast-path. POSITIVE_PRAISE_WORDS/POSITIVE_PHRASES stay exactly as they are — EFFECTIVE_PRAISE_WORDS/EFFECTIVE_PRAISE_PHRASES union in tList(lang, "praise.words"/"praise.phrases") only when non-empty, so with no locale configured they're the literal same Set object, not a copy. Also widens the fast-path's punctuation-stripping regex to full-width CJK punctuation (。、!?…「」) alongside the existing ASCII set — English prompts never contain these characters, so this is a no-op for them; it's what lets "完璧!" normalize to "完璧" and match locale vocabulary.

Why this doesn't change English behavior

Every deletion in this diff is either a regex line replaced by a wider version of itself (same normalization purpose) or a Set-reference read that now points at a value which, absent locale configuration, IS the original Set (not a copy) — same "dispatch line branching to the original" pattern I used in #1704/#1705, applied to a value reference instead of a function call. LifeosConfig.ts's changes are purely additive (one optional interface field, one line threading it through validateAndNormalize) — zero deletions.

Verification

Isolated LIFEOS_CONFIG_PATH/LIFEOS_DIR probe harness (same style as #1703):

  • (a) locale unset (no [principal].language, no WORK/config.yaml, no locale file) — perfect/great job/nice/now do X/8 produce byte-identical output to pre-change; Japanese praise ("完璧", "ありがとう") correctly does NOT fire (no vocabulary configured, matches English-only baseline)
  • (b) [principal].language = "ja" + ja.json with praise.words/praise.phrases — English praise still fires (union, not replacement); "完璧!", "ありがとう", "助かった" now fire rating 8; non-praise Japanese ("次はどうする", "これを直して") correctly produces no rating
  • (c) WORK.ISSUE_LANGUAGE: ja (no [principal].language set) — confirms the fallback chain's third tier resolves and activates the same vocabulary independently of the toml field
  • (d) malformed locale JSON — no throw, exit 0, English behavior preserved (bundle load fails closed, tList() returns [])

Also verified on develop after merging alongside #1703 (the ASCII-guard fix, which touches a disjoint region of the same file): both changes coexist correctly — "2はあとで" is now rejected (per #1703) and "ありがとう" now fires as praise (per this PR) in the same merged file, confirmed via the same probe harness.

Related: #1695 (Work System locale mechanism, the design contract this generalizes), #1703 (the other fix touching this file).

🤖 Generated with Claude Code

…o SatisfactionCapture

Generalizes work-strings.ts's design contract (LifeOS#1695: key-level
fallback, fail-open, paiUserDir()-resolved locale files) into a reusable
lib any surface can adopt, per USER/FORK/GOALS.md's stated plan for
i18n goal 2 ("Work System の hooks/lib/work-strings.ts の設計契約...を
汎用 locale lib に一般化する PR を最初に出す").

Two lookup shapes, because prose and vocabulary need opposite fallback
semantics — this is the actual design contribution here, not just a
rename of work-strings' machinery:

- `t(lang, key, fallback, vars)` — OVERRIDE. A locale bundle replaces
  the caller's default for a key it translates. No baked-in EN
  dictionary (unlike work-strings.ts, which legitimately owns one
  scoped to Work System issue bodies) — the caller supplies its own
  default, since a generic lib has no single key-space to own.
- `tList(lang, key)` — UNION. A locale bundle's array values are ADDED
  to the caller's own list, never replacing it. A Japanese locale
  should still recognize "perfect" as praise, not lose English
  vocabulary by opting in — this is what makes "locale unset → output
  unchanged" provable by construction rather than by convention: with
  no bundle, tList() returns [] and the caller's list is untouched.

resolveLang() priority: explicit arg > LIFEOS_CONFIG.toml
[principal].language (new optional field, LifeosConfig.ts) >
WORK.ISSUE_LANGUAGE (read independently, not by importing
work-config.ts's unexported loadIssueLanguage() — this keeps
locale.ts standalone rather than reaching into Work System internals
for one value) > "en". Every step is try/catch-guarded and never
throws, same fail-open contract work-strings.ts uses for locale
bundles.

First consumer: SatisfactionCapture.hook.ts's positive-praise fast-path.
Its POSITIVE_PRAISE_WORDS/POSITIVE_PHRASES stay exactly as they are —
EFFECTIVE_PRAISE_WORDS/EFFECTIVE_PRAISE_PHRASES union in tList(lang,
"praise.words"/"praise.phrases") only when non-empty, so with no locale
configured they're the literal same Set object, not a copy. Also widens
the fast-path's punctuation-stripping regex to full-width CJK
punctuation (。、!?…「」) alongside the existing ASCII set — English
prompts never contain these characters, so this is a no-op for them;
it's what lets "完璧!" normalize to "完璧" and match the locale
vocabulary.

Every deletion in this diff is either a regex line replaced by a wider
version of itself (same normalization purpose) or a Set-reference read
that now points at a value which, absent locale configuration, IS the
original Set (not a copy) — same "dispatch line branching to the
original" pattern used across this fork's other PRs, applied to a value
reference instead of a function call. LifeosConfig.ts's changes are
purely additive (one optional interface field, one line threading it
through validateAndNormalize) — zero deletions.

Verified with an isolated LIFEOS_CONFIG_PATH/LIFEOS_DIR probe harness
(same style as the ASCII-guard fix):

(a) locale unset (no [principal].language, no WORK/config.yaml, no
    locale file) — perfect/great job/nice/now do X/8 produce byte-
    identical output to pre-change; Japanese praise ("完璧", "ありがとう")
    correctly does NOT fire (no vocabulary configured, matches
    English-only baseline)
(b) [principal].language = "ja" + USER/CONFIG/locales/ja.json with
    praise.words/praise.phrases — English praise still fires
    (union, not replacement); "完璧!", "ありがとう", "助かった" now fire
    rating 8; non-praise Japanese ("次はどうする", "これを直して")
    correctly produces no rating
(c) WORK.ISSUE_LANGUAGE: ja (config.yaml) with no [principal].language
    set — confirms the fallback chain's third tier resolves and
    activates the same vocabulary independently of the toml field
(d) malformed locale JSON — no throw, exit 0, English behavior
    preserved (bundle load fails closed, tList() returns [])

Note: "2はあとで" still misreads as rating 2 on this branch in isolation
— that's the bug fix/satisfaction-rating-ascii-guard (danielmiessler#1703) already
closed, on a branch cut from `develop` after that fix landed there; this
branch was cut from `main`, which doesn't have it yet. No conflict
expected on merge — the two changes touch disjoint regions of the file
(explicit-rating guards vs. praise-vocabulary lookup).
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