83c3c1ccf1
Co-authored-by: Kandev Agent <agent@kandev.dev>
184 lines
8.1 KiB
JavaScript
184 lines
8.1 KiB
JavaScript
/**
|
|
* Options for `i18next/no-literal-string`, the guard against hardcoded
|
|
* user-facing strings. Kept in its own module because the list is long enough to
|
|
* bury the rest of eslint.config.mjs, and because it needs the explanatory
|
|
* comments below to stay maintainable.
|
|
*/
|
|
export const noLiteralStringOptions = {
|
|
// `jsx-only` (not `jsx-text-only`) so the guard also sees copy that
|
|
// never appears as a JSX text node: ternary button labels
|
|
// (`{saving ? "Saving..." : "Save"}`) and display props on internal
|
|
// components (`label=`, `description=`, `tooltip=`). Those are the
|
|
// majority of user-facing strings in this codebase, and the narrower
|
|
// mode reported them as clean. The cost is that every attribute is
|
|
// now checked, so the `words.exclude` list below has to carry the
|
|
// weight of separating copy from prop enum values.
|
|
mode: "jsx-only",
|
|
// Template literals in JSX ARE copy: `Select task ${title}`, `${label} tasks,
|
|
// over WIP limit`. Leaving them unchecked was the largest hole in the guard, and
|
|
// turning it on measured cheap — +2 to +5 findings per un-migrated directory,
|
|
// every one of them a real string, and zero new findings across the allowlist
|
|
// (className templates are already covered by the Tailwind patterns below).
|
|
"should-validate-template": true,
|
|
// Brand/proper nouns and symbol-only strings are not translatable copy.
|
|
//
|
|
// NOTE: the plugin wraps each pattern as `^<pattern>$`
|
|
// (helper/generateFullMatchRegExp), so every entry must match the WHOLE
|
|
// literal. A prefix-only pattern like "^https?://" silently never
|
|
// matches — add an explicit `.*` instead.
|
|
words: {
|
|
exclude: [
|
|
"^\\s*$",
|
|
"^[^A-Za-z]*$",
|
|
"^(Kandev|GitHub|GitLab|Jira|Linear|Slack|Sentry|Azure DevOps|Docker|Codex|OpenCode|Claude|Copilot|Amp|Sprites\\.dev)$",
|
|
"^(ACP|MCP|SSH|URL|ID|PR|CI|AI|API|JSON|YAML|LSP|TLS|SQL|JQL)$",
|
|
// Units, version prefixes, and keyboard glyphs — not translatable
|
|
// copy; these show up as fragments beside an interpolated value.
|
|
"^(v|ms\\)?|s|m|h|d|K|B|KB|MB|GB|TB|esc)$",
|
|
"^\\+[A-Z]\\)?$",
|
|
"^[·+\\-|/(),.:\\s]+$",
|
|
// All-caps acronym badges (ENTRY, KAN, MTD, WIQL, JQL) label a
|
|
// field or entity; they are identifiers, not prose.
|
|
"^[A-Z][A-Z0-9_]+$",
|
|
// Multi-word ALL-CAPS tokens are type-to-confirm phrases ("DELETE ALL NOW").
|
|
// The user must type them verbatim and they are compared with `===`, so
|
|
// translating one makes the dialog impossible to satisfy — see
|
|
// docs/i18n.md ("Do not translate").
|
|
"[A-Z][A-Z0-9_]*( [A-Z][A-Z0-9_]*)+",
|
|
// Terminal control glyphs (^C, ^D) and repeat counts (3x) are
|
|
// symbols, and "id · vN.N" is a version line, not prose.
|
|
"^\\^[A-Z]$",
|
|
"^·?\\s*v?$",
|
|
// Keyboard key names label a physical key, so they are not translatable
|
|
// copy — the spec lists keyboard glyphs as out of scope. They appear as
|
|
// <Kbd> children and on keybar config.
|
|
"(Esc|Escape|Tab|Home|End|PgUp|PgDn|Enter|Return|Space|Backspace|Delete|Del|Ins|Shift|Ctrl|Alt|Cmd|Meta|F\\d{1,2})",
|
|
// URLs, home-relative paths, dotted placeholder tokens, and a
|
|
// single letter (an avatar initial) are values, not prose.
|
|
"(https?|file|ssh|git)://.*",
|
|
"^~?/[\\w./~-]*$",
|
|
"^[a-z][a-z0-9]*(\\.[a-z0-9<>]+)+$",
|
|
"^[A-Za-z]$",
|
|
// Tailwind class lists with variants/important modifiers.
|
|
".*[!\\[].*",
|
|
// Example values shown in placeholders: emails, CSS functions,
|
|
// inline JSON, and ALLCAPS filename stand-ins.
|
|
"[\\w.+-]+@[\\w-]+\\.[\\w.-]+",
|
|
".*(calc|env|url|var)\\(.*",
|
|
"\\{.*\\}",
|
|
"[A-Z][A-Z0-9_]*\\.[a-z]{2,4}",
|
|
// Static chunks of an interpolated DOM id. With
|
|
// `should-validate-template` on, the plugin checks each literal chunk of a
|
|
// template separately, so `startup-page-${v}-label` arrives as
|
|
// "startup-page-" and "-label" — kebab/snake fragments with a dangling
|
|
// separator, which the token patterns above do not cover. Copy chunks
|
|
// ("Select task ", " tasks, over WIP limit") carry a capital, a space or
|
|
// punctuation and so still get flagged.
|
|
"[a-z0-9]+(?:[-_][a-z0-9]*)*|[-_][a-z0-9]+(?:[-_][a-z0-9]*)*",
|
|
// Single lowercase/camel/kebab tokens are prop enum values,
|
|
// classnames, and identifiers (variant="ghost", side="top",
|
|
// value="work-items") — never display copy, which is capitalized
|
|
// or multi-word.
|
|
"^[a-z][a-zA-Z0-9]*$",
|
|
"^[a-z0-9]+([-_][a-z0-9]+)+$",
|
|
// CSS lengths, colors, Tailwind class lists, link rel/target
|
|
// keywords, route paths, and `__sentinel__` option values.
|
|
"^\\d+(\\.\\d+)?(px|rem|em|%|vh|vw|ch|fr|s|ms|d|h|m|w|y)$",
|
|
"^#[0-9a-fA-F]{3,8}$",
|
|
"^_(blank|self|parent|top)$",
|
|
"^(noopener|noreferrer)( (noopener|noreferrer))*$",
|
|
"^__[a-z_]+__$",
|
|
"^/[\\w/\\-\\[\\]:.]*(\\?[\\w=&%.\\-]*)?$",
|
|
"(?:-?[a-z0-9]+(?:[:/-][a-z0-9.]+)*\\s+)*-?[a-z0-9]+(?:[:/-][a-z0-9.]+)*",
|
|
],
|
|
},
|
|
"jsx-attributes": {
|
|
// Attributes that carry display copy and must be translated.
|
|
include: ["placeholder", "aria-label", "aria-description", "title", "alt"],
|
|
exclude: [
|
|
".*[Cc]lassName$",
|
|
"class",
|
|
"id",
|
|
"key",
|
|
"type",
|
|
"name",
|
|
"role",
|
|
"href",
|
|
"src",
|
|
"to",
|
|
"htmlFor",
|
|
"data-.*",
|
|
// Identifiers and prefixes the caller composes into ids/testids.
|
|
"id",
|
|
"k",
|
|
// Option/badge values are data the app compares and submits.
|
|
"value",
|
|
"cmd",
|
|
".*[Ii]dPrefix$",
|
|
".*[Ii]dSuffix$",
|
|
".*SaveId$",
|
|
"aria-labelledby",
|
|
"aria-controls",
|
|
"aria-describedby",
|
|
],
|
|
},
|
|
callees: {
|
|
// String args to these are identifiers/classnames, not copy.
|
|
exclude: [
|
|
"cn",
|
|
"clsx",
|
|
// `skipAll("User skipped")` records a reason sent to the server
|
|
// alongside the skip; it is stored data, not rendered copy.
|
|
".*\\.skipAll",
|
|
"cva",
|
|
"tv",
|
|
"t",
|
|
"i18n(ext)?.*",
|
|
"require",
|
|
"console\\.\\w+",
|
|
".*\\.(getAttribute|setAttribute|matches|closest|querySelector)",
|
|
],
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Allowlist of paths the `i18next/no-literal-string` ERROR applies to.
|
|
*
|
|
* Deliberately NOT `components/**` + `app/**`. A repo-wide error means every PR
|
|
* that lands a hardcoded string anywhere — including PRs that have nothing to do
|
|
* with i18n — breaks. The first attempt at this migration shipped the global
|
|
* form and spent two days in conflict-resolution rounds because `main` kept
|
|
* moving underneath it.
|
|
*
|
|
* Instead the guard is opt-in per path, so it can only ever tighten:
|
|
*
|
|
* - Migrating a page or directory? Externalize its strings, then append its
|
|
* path here in the SAME PR. That path is now permanently protected.
|
|
* - Everything not listed is unaffected, so unrelated PRs never see this rule
|
|
* and there is no treadmill.
|
|
*
|
|
* Entries may be single files or directory globs — use a file glob while a
|
|
* directory is partially migrated, and collapse to `dir/**` once it is done.
|
|
* The list only shrinks by mistake; adding to it is the whole point.
|
|
*
|
|
* A clean lint is NOT proof a path is fully migrated — the rule only sees plain
|
|
* literals in JSX. Template literals, `confirm()`/`alert()` arguments, and copy
|
|
* returned from plain `.ts` helpers are invisible to it. The pseudo-locale is
|
|
* the completeness check. See docs/i18n.md.
|
|
*/
|
|
export const i18nGuardFiles = [
|
|
// The i18n runtime itself.
|
|
"lib/i18n/**/*.{ts,tsx}",
|
|
// Settings → General → Appearance, migrated end-to-end as the worked example:
|
|
// the page, the two sections it owns, and the settings chrome around them.
|
|
"app/settings/general/appearance/**/*.{ts,tsx}",
|
|
"components/app-sidebar/sections/settings/general-group.tsx",
|
|
"components/settings/general-settings.tsx",
|
|
"components/settings/general-nav.ts",
|
|
"components/settings/language-settings.tsx",
|
|
"components/settings/settings-floating-save.tsx",
|
|
"components/settings/settings-layout-client.tsx",
|
|
"components/settings/startup-page-settings-card.tsx",
|
|
"components/settings/system-metrics-settings-card.tsx",
|
|
];
|