feat(i18n): add hooks/lib/locale.ts, wire praise-vocabulary union into SatisfactionCapture - #1706
Open
takanorinishida wants to merge 1 commit into
Open
Conversation
…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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 (unlikework-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 importingwork-config.ts's unexportedloadIssueLanguage()— 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 contractwork-strings.tsuses for locale bundles.First consumer
SatisfactionCapture.hook.ts's positive-praise fast-path.POSITIVE_PRAISE_WORDS/POSITIVE_PHRASESstay exactly as they are —EFFECTIVE_PRAISE_WORDS/EFFECTIVE_PRAISE_PHRASESunion intList(lang, "praise.words"/"praise.phrases")only when non-empty, so with no locale configured they're the literal sameSetobject, 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 throughvalidateAndNormalize) — zero deletions.Verification
Isolated
LIFEOS_CONFIG_PATH/LIFEOS_DIRprobe harness (same style as #1703):[principal].language, noWORK/config.yaml, no locale file) —perfect/great job/nice/now do X/8produce byte-identical output to pre-change; Japanese praise ("完璧", "ありがとう") correctly does NOT fire (no vocabulary configured, matches English-only baseline)[principal].language = "ja"+ja.jsonwithpraise.words/praise.phrases— English praise still fires (union, not replacement); "完璧!", "ありがとう", "助かった" now fire rating 8; non-praise Japanese ("次はどうする", "これを直して") correctly produces no ratingWORK.ISSUE_LANGUAGE: ja(no[principal].languageset) — confirms the fallback chain's third tier resolves and activates the same vocabulary independently of the toml fieldtList()returns[])Also verified on
developafter 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