diff --git a/AGENTS.md b/AGENTS.md index 8807889..feb5ebf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -276,10 +276,12 @@ Same test before adding a row to a table or the file map — a clause, not a par A pure view-model builder takes an optional `t` defaulting to its own English, so a unit test reads in words; handlers pass `ctx.t`. - **One verb per action in the English UI: sign in, sign out, create account.** Not "log in", - "log out" or "sign up" — a second spelling for one button reads as a second thing. Guarded by - `src/i18n/load.test.ts` (a test file cannot live in `locales/`, which loads every `.ts` in it as a - catalog); the noun ("a sign-in error") is unaffected. A plugin's catalog - and every other locale follow the same one-verb-per-action rule in their own language. + "log out" or "sign up", inflections included — a second spelling for one button reads as a second + thing; the noun ("a sign-in error", "the sign-in identifier") is unaffected. Guarded over the + shipped core catalog by `src/i18n/load.test.ts` (a test file cannot live in `locales/`, which loads + every `.ts` in it as a catalog); a plugin's catalog and every other locale follow the same rule in + their own language, unguarded. An unmapped Kratos id still renders Kratos' own wording — map it to + hold the line. - Use well formed, standard compliant, rich URIs. Prefer state in the URL over POST:ing in for for example list pages with filters and pagination. Do: "ids=x&ids=y" and not "ids[]=x&ids[]=y" and not "ids=x,y". diff --git a/examples/plugins/admin/i18n/en-US.ts b/examples/plugins/admin/i18n/en-US.ts index ab65993..85c0894 100644 --- a/examples/plugins/admin/i18n/en-US.ts +++ b/examples/plugins/admin/i18n/en-US.ts @@ -129,7 +129,7 @@ const messages = { "admin.users.error.selfDeactivate": "You can't deactivate your own account.", "admin.users.error.selfDelete": "You can't delete your own account.", "admin.users.field.email": "Email", - "admin.users.field.emailHint": "The login identifier — can't be changed here.", + "admin.users.field.emailHint": "The sign-in identifier — can't be changed here.", "admin.users.field.first": "First name", "admin.users.field.last": "Last name", "admin.users.field.password": "Password", diff --git a/src/i18n/load.test.ts b/src/i18n/load.test.ts index fe4ed33..8209ed9 100644 --- a/src/i18n/load.test.ts +++ b/src/i18n/load.test.ts @@ -18,17 +18,16 @@ async function fixture(files: Record): Promise<{ localesDir: str return { localesDir: join(root, "locales"), pluginsDir: join(root, "plugins") }; } -test("the shipped core catalogs load and agree key for key", async () => { +test("the shipped core catalogs load, agree key for key, and use one verb per action", async () => { const loaded = await loadI18n(); // no args ⇒ the real src/i18n/locales + plugins/ assert.ok(loaded.available.includes("en-US")); assert.ok(loaded.available.includes("sv-SE")); assert.deepEqual([...loaded.available].sort(), loaded.available); // sorted, so "sv" resolves deterministically assert.ok(Object.keys(loaded.core.get("en-US") ?? {}).length > 20); - // One verb per action: sign in / sign out / create account. Two spellings for one button ("Log in" - // on the landing, "Sign in" in the sidebar) read as two different things. Nouns ("a sign-in error") - // are fine — only the competing verbs are out. AGENTS.md → Rules. - const competing = /\b(log[\s-]?in|log[\s-]?out|sign[\s-]?up)\b/i; + // One verb per action; inflected too, and the noun ("a sign-in error") is fine. AGENTS.md → Rules. + // The lookbehind spares a path (/login) and a word ending in one (blog in…). + const competing = /(? [key, typeof message === "string" ? message : Object.values(message).join(" ")] as const) .filter(([, text]) => competing.test(text))