Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# 2026-10-01 β€” feat-i18n-api-messages-auth (implementer note)

- Worker: implementer (main session)
- Node: `feat-i18n-api-messages-auth`
- GitHub issue: #159 β€” "i18n for API messages β€” auth flow (phase 1/2)"
- Branch: `159-i18n-for-api` (from `staging`)

## Finding: already fully implemented, same backfill pattern as #158/#161

Read the full acceptance criteria and every file the scope names before
writing anything:

- `src/utils/i18n.ts` β€” `t(key, lang)` (dot-path lookup, falls back to
`DEFAULT_LANG` then to the raw key) and `tErrorType(type, lang)` (flat
one-level lookup for Joi/Mongoose error-type codes) both already exist.
- `src/locales/{vi,en}.ts` β€” both locale tables exist (TS modules, not
`.json`, but functionally the "hand-rolled t(key,lang)" the issue
describes) with a full `auth.*` namespace (19 keys) covering every
register/login/logout/refresh/forgot-password/reset-password/
verify-email message in both languages.
- `src/middlewares/language.middleware.ts` β€” resolves `Accept-Language`
(first weighted tag, primary subtag, case-insensitive, defaults to
`vi` for missing/unsupported), attaches `req.lang` + `req.t(key)`.
- `src/auth/auth.controller.ts` / `src/auth/auth.service.ts` β€” read in
full. Every one of the 21 `message:` sites in both files resolves
through `t(key, lang)`; every handler (`handlerRegister`,
`handlerLogin`, `handlerForgotPassword`, `handlerResetPassword`,
`handlerVerifyEmail`) takes `lang: string = DEFAULT_LANG`; every
controller action reads `(req as any).lang` and threads it through to
the service call, the literal `t()` calls, and `handleError(err, next,
lang)`. No hardcoded string found anywhere in either file β€” confirmed
by grepping every `message:` line in both files.

Real gap: **zero test coverage** existed for any of this β€” no
`i18n.test.ts`, no `language.middleware` test, and the existing
`auth.controller.test.ts`/`auth.service.test.ts` only ever exercised the
default `vi` path (mock requests never set `req.lang`, so `(req as
any).lang` was `undefined` and `t()` fell through to `DEFAULT_LANG`).
Nothing proved the `en` path actually worked end-to-end.

## What changed (tests only, 0 production code)

1. **`src/__tests__/utils/i18n.test.ts`** (new) β€” unit tests for `t()`
(resolves per requested lang, defaults to vi, falls back to
`DEFAULT_LANG` for an unsupported lang code, falls back to the raw key
when missing everywhere) and `tErrorType()` (flat lookup vs `t()`'s
dot-path walk, default-lang fallback, `undefined` for an unknown
type). Satisfies acceptance criterion 1 ("`t(key,lang)` utility exists
and is unit-tested").

2. **`src/__tests__/middlewares/language.test.ts`** (new) β€” unit tests
for `languageMiddleware`: defaults to `vi` with no header, plain tag,
regional tag (`en-US` β†’ `en`), multi-tag weighted header (first tag
wins), case-insensitivity, unsupported-language fallback, and that
`req.t(key)` genuinely delegates to the real `t()` table (asserts the
real English/Vietnamese strings, not a mock).

3. **`src/__tests__/auth/auth.controller.test.ts`** (extended in place,
+117 lines, existing tests untouched) β€” added one `en`-language test
per controller action group, real (unmocked) `t()`, asserting the
actual English string comes back when `req.lang = 'en'`:
- `authRegister` β€” fallback success message (`message ||
t('auth.registerSuccess', lang)`)
- `authLogin` β€” fallback failure message (`t('auth.loginFailed')`)
- `authRefreshToken` β€” no-refresh-token + revoked-token messages
(both direct `t()` calls, not fallbacks)
- `authLogout` β€” no-token + success messages
- `authLogoutAll` β€” success message

Combined with the pre-existing tests (which all run with `req.lang`
unset β†’ `vi`), this now proves both languages for every branch
touched, satisfying acceptance criterion 2 ("verified in both vi and
en via Accept-Language") at the controller boundary β€” the layer that
actually reads `Accept-Language` via `req.lang`.

`handlerLogin`/`handlerRegister`'s own service-level messages were
already implicitly exercised in both directions by `auth.service.test.ts`
(default `vi` calls) β€” no service-level `en` test was added since the
service functions take a raw `lang` string param (already proven
language-agnostic by the `t()` unit tests) rather than reading
`Accept-Language` themselves; the controller is the integration point
worth testing end-to-end.

## Explicitly not touched

- Joi validation messages (`utils/valid.ts`) and Mongoose `required`
messages (`utils/helper.ts`'s `handleError`) β€” out of scope per the
issue body, tracked as phase 2 (#160 / node
`feat-i18n-full-coverage`).
- Candidate/CV section messages β€” same, phase 2.
- `api/v2/auth` (register/login WIP mirror) β€” issue #159 scopes only the
v1 auth flow; v2 has its own separate node/issue history
(`consolidate-v1-v2-auth`).

## Verification run

```
npm test
```
```
Test Suites: 31 passed, 31 total
Tests: 181 passed, 181 total
Snapshots: 0 total
Time: 7.474 s, estimated 8 s
```
(baseline before this change: 29 suites / 160 tests β€” +2 suites / +21
tests: 8 in `i18n.test.ts`, 8 in `language.test.ts`, 5 new `en`-language
cases in `auth.controller.test.ts`)

```
npm run build
```
```
tsc && npm run copy
```
Clean, no typecheck errors.

## Hub bytes before
(measured by verifier, same 5-category `/hub-tokens` formula)

## Status
`sealed_pending_verifier`
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# 2026-10-01 β€” feat-i18n-api-messages-auth (verifier verdict)

- Worker: verifier (subagent, dispatched via Agent tool)
- Node: `feat-i18n-api-messages-auth`
- New PM status: SEALED

## Isolation proof
Dispatched fresh via the Agent tool with the task string: "You are being
spawned as an independent verifier subagent... Run `verify_seal` for
evidence note: `agent-hub/evidence/implementer/2026-10-01/feat-i18n-api-messages-auth-plan.md`.
Node: `feat-i18n-api-messages-auth`..." β€” no memory of any implementation
session. Everything below was re-derived independently: reading real
files (`i18n.ts`, `language.middleware.ts`, `auth.controller.ts`,
`auth.service.ts`, `locales/en.ts`, both new test files, the test diff),
running `git status`/`git diff staging` myself, re-running `npm test`/
`npm run build` myself, and fetching GitHub issue #159 directly via `gh
issue view` rather than trusting the note's paraphrase.

## Reasoning

1. **`t(key,lang)`/`tErrorType` read in full** (`src/utils/i18n.ts`) β€”
matches the note's description exactly: dot-path `getNested` walker,
falls back to `DEFAULT_LANG` then the raw key; `tErrorType` does a flat
one-level `joiErrors[type]` lookup specifically because a Joi type
string like `any.required` already contains a dot.

2. **`language.middleware.ts` read in full** β€” `resolveLang` takes the
first comma-separated tag, strips `-XX` regional suffix, lowercases,
falls back to `vi` if unsupported; attaches `req.lang` + `req.t`.
Matches the note.

3. **Zero hardcoded strings confirmed by my own grep**, not the note's
claim: `grep -n "message:" src/auth/auth.controller.ts
src/auth/auth.service.ts` β†’ exactly 21 lines, every single one calls
`t('auth.xxx', lang)` or `t('auth.xxx', (req as any).lang)`. Cross-
checked against `src/locales/en.ts:6-25` β€” 19 `auth.*` keys present,
covering every key referenced by the 21 call sites.

4. **Diff matches the note's claim, genuinely minimal.** `git status
--short` on `159-i18n-for-api`: `M
src/__tests__/auth/auth.controller.test.ts` plus 2 new untracked test
files (`i18n.test.ts`, `language.test.ts`) and the evidence-note
directory β€” no production file touched. `git diff --stat staging --
src/`: 1 file, `+117/-0` on `auth.controller.test.ts` β€” confirmed via
full diff read that every hunk is a pure addition (new `it(...)`
blocks appended after existing tests), the pre-existing tests are
byte-for-byte untouched.

5. **New test files read in full.** `i18n.test.ts` β€” 8 real assertions:
per-lang resolution (`loginSuccess` en/vi), default-vi-with-no-lang,
fallback-to-default-lang for an unsupported code, fallback-to-raw-key
when missing everywhere, `tErrorType`'s flat lookup (`any.required` en/
vi), `tErrorType` default-lang fallback, `tErrorType` returns
`undefined` for an unknown type. `language.test.ts` β€” 8 real
assertions matching the note's description (default/plain/regional/
multi-tag-weighted/case-insensitive/unsupported-fallback, plus `req.t`
proven to delegate to the real table for both en and vi, not a mock).
`auth.controller.test.ts`'s 5 new tests each set `req.lang = 'en'` and
assert the literal English string from `locales/en.ts` comes back
through `formatReturn` β€” genuinely exercises the previously-untested
`en` branch, not a re-assertion of the `vi` default.

6. **GitHub issue #159 fetched directly** (`gh issue view 159`), not
trusted from the note's paraphrase. Its 3 acceptance criteria:
- "`t(key, lang)` utility exists and is unit-tested" β€” met, `i18n.test.ts`.
- "Every auth-flow response/error message resolves through `t()`,
verified in both `vi` and `en` via `Accept-Language`" β€” met: the
21-site grep (step 3) proves the "resolves through `t()`" half;
the 5 new controller `en` tests + pre-existing `vi`-default tests
prove the "verified in both languages" half at the layer that
actually reads `Accept-Language`.
- "No regression in existing auth tests" β€” met: `git diff` on
`auth.controller.test.ts` shows pure additions; `auth.service.
test.ts` untouched; full suite green (see below).
Issue explicitly scopes out Joi validation messages and candidate/CV
messages as a separate phase β€” matches the note's "explicitly not
touched" section and the node's own PENDING description before this
SEAL.

7. **`npm test` independently re-run.** My own verbatim tail:
```
Test Suites: 31 passed, 31 total
Tests: 181 passed, 181 total
Snapshots: 0 total
Time: 7.946 s
Ran all test suites.
```
Matches the note's claimed `31 passed, 31 total` / `181 passed, 181
total` exactly (baseline 29/160 + 2 new suites + 21 new tests, also
checks out arithmetically: 8 + 8 + 5 = 21).

8. **`npm run build` independently re-run.** `tsc && npm run copy` β†’
clean, no typecheck errors, `cp -R ./src/views ./src/public ./dist/`
ran without complaint.

**Proportion**: 3 test files, 0 production code β€” appropriately minimal
given the feature was already fully live; does not exceed the node's
scope (SmallestDiff) and does not fall short of any of #159's 3
acceptance criteria.

**Forbidden states scan**: `ADHOC_WORK` β€” no, real diagram node existed
(PENDING) and this ran through the verify_seal recipe. `NO_EVIDENCE` β€”
no, implementer note + this verdict note both exist. `EDIT_UNVERIFIED` β€”
no, `npm test`/`npm run build` independently re-run and read back
verbatim above. `CODE_IN_HAVEN` β€” no `.ts`/`.js` files added to `haven/`.
`DIAGRAM_DRIFT` β€” corrected by this SEAL (PM status now matches the
confirmed-live code + newly-added test coverage).

## Re-run
`full` β€” re-ran `npm test` (31/31 suites, 181/181 tests, matched exactly)
and `npm run build` (clean) myself from the repo root; independently read
every file the note cited (`i18n.ts`, `language.middleware.ts`,
`auth.controller.ts`, `auth.service.ts`, `locales/en.ts`, both new test
files, the full `auth.controller.test.ts` diff) rather than auditing the
note's prose alone; ran `git status`/`git diff staging --stat` myself to
confirm the minimal-diff claim; fetched GitHub issue #159 directly via
`gh issue view 159 --json body` rather than trusting the note's
paraphrase of acceptance criteria. Justification: same class as
`add-candidate-self-delete`/`fix-idor-broken-access-control` β€” an
unusual "0 production diff, already fully live" claim, and the
dispatching task explicitly asked me to judge whether that warrants a
re-run under the recipe's criteria; it does.

## Hub bytes before
n/a β€” not measured (not trivial to isolate cheaply from this dispatch context).

## Hub bytes after
n/a β€” same reason; the diagram-row edit is the only hub-byte-affecting
write made this pass.
Loading
Loading