Skip to content

fix(i18n-keys): resolve i18next count and context suffixes - #10

Merged
agjs merged 2 commits into
mainfrom
fix/i18n-keys-plural-context
Sep 14, 2026
Merged

agjs merged 2 commits into
mainfrom
fix/i18n-keys-plural-context

Conversation

@agjs

@agjs agjs commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Summary

static-translation-key-exists accepted only the literal key, so every plural dictionary (files_one / files_other) forced hand-rolled filesOne / filesOther keys or a lint disable. This surfaced twice while building a product on BoringStack (feedback entries 70 and 73), and boringstack-xyz/boringstack#455 currently works around it with a bun patch of the published dist. This release replaces that patch.

The rule now mirrors i18next's key resolution:

  • a call with a count option accepts <key>_other (or <key>_ordinal_other with ordinal: true), the fallback form every language must define;
  • a string-literal context option accepts <key>_<context>;
  • the two combine as <key>_<context>_other, falling back the way i18next does;
  • a counted call with no _other fallback reports the new missingPluralKey message naming the expected key;
  • an uncounted call to a plural-only key (t("files")) still fails, because i18next would render the raw key;
  • a variable context cannot be resolved statically and falls back to the base key.

README documents the accepted forms. Changeset: patch release 0.1.3.

Test plan

  • pnpm typecheck, pnpm test: 17 rule-tester cases (9 valid, 8 invalid) covering count, ordinal, context, combined, nested keys, plain-key fallback, and the error paths.
  • pnpm build, then ran the built dist/index.js through BoringStack's real ESLint 10 entry point against the fixture from apps/ui/tests/lint-meta/i18n-plugin.test.ts in #455: 3 errors on lines 4, 5 and 6, matching the test's expectation.

static-translation-key-exists accepted only the literal key, so every
plural dictionary (files_one/files_other) forced hand-rolled keys or a
lint disable. The rule now mirrors i18next's resolution: a call with a
count option accepts <key>_other (or <key>_ordinal_other with
ordinal: true), a string-literal context accepts <key>_<context>, and
the two combine. Counted calls without an _other fallback report the new
missingPluralKey message naming the expected key; an uncounted call to a
plural-only key still fails because i18next would render the raw key.
@agjs
agjs merged commit c9dfd15 into main Sep 14, 2026
1 check passed
@agjs
agjs deleted the fix/i18n-keys-plural-context branch September 14, 2026 16:54
@agjs agjs mentioned this pull request Sep 14, 2026
@github-actions github-actions Bot mentioned this pull request Sep 14, 2026
agjs added a commit to boringstack-xyz/boringstack that referenced this pull request Sep 14, 2026
@boring-stack-pkg/eslint-plugin-i18n-keys 0.1.3 resolves i18next count,
ordinal and context suffixes upstream (boringstack-xyz/eslint-plugins#10),
so the Bun patch of the published dist, its patchedDependencies entry and
the COPY patches lines in the three Dockerfiles go away. The installed
plugin regression in tests/lint-meta keeps guarding the behaviour.
agjs added a commit to boringstack-xyz/boringstack that referenced this pull request Sep 14, 2026
* fix: address product setup and agent validation feedback

* fix(agent): copy UI dependency patches into the isolated candidate image

The isolated evaluation image installs apps/ui from package.json and
bun.lock alone. With patchedDependencies present, a frozen install
aborts on the missing patch file, which failed the agent verification
contract job. Copy the patches directory before installing and assert
in a unit test that every patched dependency is copied ahead of the
install step.

* fix(ui): keep the ESLint cache and invalidate it on dictionary changes

Removing --cache made every lint run cold. ESLint keys its result cache
on a hash of each file's resolved config, so the config now carries a
sha256 digest of every English dictionary in settings; editing a
dictionary changes the hash and re-lints every file. A regression runs
ESLint twice with --cache against a shrinking dictionary and shows the
deleted key is reported with the digest and hidden without it.

* fix(ui): drop an unused import and format the ESLint config

The cache regression built its digest inside a generated config string,
so the createHash import in the test itself was unused; the config edit
also missed a Prettier pass.

* fix(ui): take the plural-aware i18n lint rule from the released plugin

@boring-stack-pkg/eslint-plugin-i18n-keys 0.1.3 resolves i18next count,
ordinal and context suffixes upstream (boringstack-xyz/eslint-plugins#10),
so the Bun patch of the published dist, its patchedDependencies entry and
the COPY patches lines in the three Dockerfiles go away. The installed
plugin regression in tests/lint-meta keeps guarding the behaviour.

* chore(lint): take three rule fixes from the released plugins

react-component-architecture 0.3.1 exempts .tsx files that export
nothing from component-folder-structure; audit-log 0.2.0 checks a
service's public surface only; drizzle-conventions 0.2.0 accepts column
arithmetic such as the atomic increment. Guides describe the accepted
shapes and the feedback ledger closes rows 40, 65 and 82 as implemented
upstream (boringstack-xyz/eslint-plugins#15).

* feat: close the remaining product-feedback rows the template can fix itself

- Session in lib: useMe, IMe, isAuthenticatedMe and SESSION_QUERY_KEYS
  move to src/lib/session so every feature reads the current user without
  a cross-feature exception; auth consumes them too, the allow-list keeps
  only accounts -> auth for the MFA settings section. Reference UI
  templates follow.
- Cache generations: bumpGeneration / readGeneration / generationScopedKey
  give seeds, publishes and migrations a way to drop a namespace at once;
  regression proves old keys become unreachable.
- Guides: relational row typing and numeric/bigint strings (drizzle),
  portable API schemas (typescript, api-client), cache namespaces, grouped
  store selectors, session module (state-management, routing).
- Dead-key lint message says which template literals it can follow.
- Ledger rows 23, 24, 26, 36, 37, 38, 51, 63, 78, 80 record the fix or
  the upstream rule (boringstack-xyz/eslint-plugins#17).

* fix(agent): reference UI needs no auth allow-list entry; Docker errors carry a redacted stderr tail

The reference UI generator patched the cross-feature allow-list at the
["dashboard", "auth"] anchor to add projects -> auth; with useMe in
@/lib/session the entry is unnecessary and the anchor is gone, which
failed the isolated positive control. The isolated Docker helper now
appends the last 600 characters of stderr with every argument value
redacted, so the next failure of this kind is readable in CI.

* chore(lint): enable portable schema types and private declarations from the released plugins

elysia 0.2.0 adds portable-schema-types, on for every *.schemas.ts;
module-boundaries 0.2.0 adds ignorePrivateDeclarations, on for the UI so
private helpers and constants stay next to the hook or component that
reads them. Ledger rows 21, 35 and 51 close as implemented upstream.
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