Skip to content

[feat] i18n: no-hardcoded-i18n-string ESLint rule - #4010

Merged
nynexman4464 merged 1 commit into
mainfrom
nynexman4464/feat/i18n-lint-rule
Jul 17, 2026
Merged

nynexman4464 merged 1 commit into
mainfrom
nynexman4464/feat/i18n-lint-rule

Conversation

@nynexman4464

@nynexman4464 nynexman4464 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Stacked on #4011. Adds an ESLint rule that flags hardcoded English string literals on user-facing props so future i18n regressions get caught at author time. All the pre-existing strings this rule flagged are fixed in #4011, so this PR is intentionally small: just the rule + wiring.

The rule — @astryx/no-hardcoded-i18n-string

Flags any string literal on a JSX attribute, object property, or destructure default whose name matches user-facing conventions:

  • Exact matches: label, placeholder, title, tooltip, text, summary, message, description, hint
  • Suffix patterns: *Label, *Text, *Placeholder, *Title, *Message, *Tooltip, *Hint, *Description, *Summary
  • aria-* attributes that per WAI-ARIA take user-visible text: aria-label, aria-description, aria-placeholder, aria-roledescription, aria-valuetext, aria-braillelabel, aria-brailleroledescription, aria-keyshortcuts. Attributes taking IDs, booleans, or enum tokens are intentionally not flagged.

Ignores test files (*.test.*, __tests__/**), Storybook stories, doc files, and non-user-facing string shapes (single lowercase words, SCREAMING_SNAKE constants, URLs, paths, empty strings).

Configuration follows the plugin's existing pattern — error in strict mode (CI / agents), warn in recommended (local dev).

Portable

The rule is filesystem-agnostic — no paths are hardcoded in the rule source. Downstream packages that build on astryx and ship translatable UI can enable it too, scoped with the standard flat-config files / ignores pattern:

{
  files: ["packages/mypackage/src/**/*.{ts,tsx}"],
  plugins: { '@astryx': astryxPlugin },
  rules: {
    '@astryx/no-hardcoded-i18n-string': 'error',
  },
}

In this repo it is registered under the existing astryx config (already scoped to packages/core/src/**) plus an override that turns it off for packages/core/src/i18n/** — the runtime that defines the strings.

Testing

Follow-ups

Refs #3641, builds on #4011

@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview, Comment Jul 17, 2026 9:15pm

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 15, 2026
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-lint-rule branch from fe540af to fe71752 Compare July 15, 2026 16:54
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-lint-rule branch from fe71752 to 5dce897 Compare July 15, 2026 16:59
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-lint-rule branch from 5dce897 to 20bf153 Compare July 15, 2026 17:10
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-lint-rule branch from 20bf153 to 3b2c4c5 Compare July 15, 2026 17:20
@nynexman4464 nynexman4464 changed the title [feat] i18n: no-hardcoded-i18n-string lint rule + 33 caught-violation fixes [feat] i18n: no-hardcoded-i18n-string ESLint rule Jul 15, 2026
@nynexman4464
nynexman4464 changed the base branch from nynexman4464/feat/i18n-rest to nynexman4464/feat/i18n-remaining-fixes July 15, 2026 17:20
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-remaining-fixes branch from 3598cf6 to 936197e Compare July 15, 2026 17:25
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-lint-rule branch from 3b2c4c5 to c1e8af7 Compare July 15, 2026 17:25
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-remaining-fixes branch from 936197e to 9630886 Compare July 15, 2026 18:19
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-lint-rule branch from c1e8af7 to 04a8c1f Compare July 15, 2026 18:19
@nynexman4464
nynexman4464 marked this pull request as ready for review July 15, 2026 18:35
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-remaining-fixes branch from 9630886 to 6f31894 Compare July 15, 2026 18:48
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-lint-rule branch from 04a8c1f to 1d20b5d Compare July 15, 2026 18:48
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-remaining-fixes branch from 6f31894 to e2791bf Compare July 15, 2026 19:43
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-lint-rule branch from 1d20b5d to 77b9865 Compare July 15, 2026 19:43
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-remaining-fixes branch from e2791bf to 74ff041 Compare July 15, 2026 20:12
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-lint-rule branch from 77b9865 to 1503117 Compare July 15, 2026 20:12
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-remaining-fixes branch from 74ff041 to b91aed4 Compare July 15, 2026 22:51
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-lint-rule branch from 1503117 to be1a23b Compare July 15, 2026 22:51
nynexman4464 added a commit that referenced this pull request Jul 15, 2026
Extends the i18n migration series to catch the two patterns that a
plain-literal audit missed:

  aria-label={isOpen ? 'Close X' : 'Open X'}     (ternary in JSX attr)
  aria-label={`Clear ${label}`}                (templated string)

Both slipped past the initial migration because the value on the
attribute is a JSX expression container, not a bare string literal.
Real-world integration testing (French / pseudo locale swap on a
consumer app) caught them.

Migrations by component (27 files):

  DateInput / DateRangeInput / DateTimeInput
    - calendar toggle aria-label (open/close)
    - clear button aria-label (`Clear ${label}`)
  Banner, SideNavItem, ChatComposerDrawer
    - expand/collapse ternaries
  ChatSendButton
    - send/stop ternary
  ChatLayout, ChatLayoutScrollButton, ChatMessage, ChatTriggerMenu
    - templated aria-labels
  Citation, CodeBlock, Lightbox, MobileNav
    - templated / static aria-labels
  MultiSelector, NumberInput, Selector, TextInput, TimeInput, FileInput
    - `Clear ${label}` aria-labels
  SideNavCollapseButton
    - expand/collapse sidebar ternary
  Table plugins:
    - useTableFiltering — 14 `Filter ${header}` sites consolidated
      behind one shared key with ICU {header} variable
    - useTableGroupedRows — expand/collapse group templates
    - useTableRowExpansion — row-toggle and all-rows-toggle ternaries
  Thumbnail, Token
    - remove/open templates

33 new catalog entries in packages/core/locales/en.json, all
@AstryX.<component>.* camelCase. All interpolations expressed as ICU
MessageFormat variables. Zero user-visible English changes —
defaultMessage always matches the pre-migration string exactly.

5 sub-components in useTableFiltering that lacked `useTranslator`
now have it. useTriggerMenu / useTableGroupedRows /
useTableRowExpansion needed `t` added to a few exhaustive-deps
arrays (no-op at runtime since useTranslator returns a stable
reference).

Refs #3641, builds on #4010
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-remaining-fixes branch from b91aed4 to 1f1fa76 Compare July 17, 2026 16:26
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-lint-rule branch from be1a23b to 062f650 Compare July 17, 2026 16:26
nynexman4464 added a commit that referenced this pull request Jul 17, 2026
Extends the i18n migration series to catch the two patterns that a
plain-literal audit missed:

  aria-label={isOpen ? 'Close X' : 'Open X'}     (ternary in JSX attr)
  aria-label={`Clear ${label}`}                (templated string)

Both slipped past the initial migration because the value on the
attribute is a JSX expression container, not a bare string literal.
Real-world integration testing (French / pseudo locale swap on a
consumer app) caught them.

Migrations by component (27 files):

  DateInput / DateRangeInput / DateTimeInput
    - calendar toggle aria-label (open/close)
    - clear button aria-label (`Clear ${label}`)
  Banner, SideNavItem, ChatComposerDrawer
    - expand/collapse ternaries
  ChatSendButton
    - send/stop ternary
  ChatLayout, ChatLayoutScrollButton, ChatMessage, ChatTriggerMenu
    - templated aria-labels
  Citation, CodeBlock, Lightbox, MobileNav
    - templated / static aria-labels
  MultiSelector, NumberInput, Selector, TextInput, TimeInput, FileInput
    - `Clear ${label}` aria-labels
  SideNavCollapseButton
    - expand/collapse sidebar ternary
  Table plugins:
    - useTableFiltering — 14 `Filter ${header}` sites consolidated
      behind one shared key with ICU {header} variable
    - useTableGroupedRows — expand/collapse group templates
    - useTableRowExpansion — row-toggle and all-rows-toggle ternaries
  Thumbnail, Token
    - remove/open templates

33 new catalog entries in packages/core/locales/en.json, all
@AstryX.<component>.* camelCase. All interpolations expressed as ICU
MessageFormat variables. Zero user-visible English changes —
defaultMessage always matches the pre-migration string exactly.

5 sub-components in useTableFiltering that lacked `useTranslator`
now have it. useTriggerMenu / useTableGroupedRows /
useTableRowExpansion needed `t` added to a few exhaustive-deps
arrays (no-op at runtime since useTranslator returns a stable
reference).

Refs #3641, builds on #4010
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-remaining-fixes branch from 1f1fa76 to bd1f0b2 Compare July 17, 2026 17:54
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-lint-rule branch from 062f650 to 726cf27 Compare July 17, 2026 17:54
nynexman4464 added a commit that referenced this pull request Jul 17, 2026
Extends the i18n migration series to catch the two patterns that a
plain-literal audit missed:

  aria-label={isOpen ? 'Close X' : 'Open X'}     (ternary in JSX attr)
  aria-label={`Clear ${label}`}                (templated string)

Both slipped past the initial migration because the value on the
attribute is a JSX expression container, not a bare string literal.
Real-world integration testing (French / pseudo locale swap on a
consumer app) caught them.

Migrations by component (27 files):

  DateInput / DateRangeInput / DateTimeInput
    - calendar toggle aria-label (open/close)
    - clear button aria-label (`Clear ${label}`)
  Banner, SideNavItem, ChatComposerDrawer
    - expand/collapse ternaries
  ChatSendButton
    - send/stop ternary
  ChatLayout, ChatLayoutScrollButton, ChatMessage, ChatTriggerMenu
    - templated aria-labels
  Citation, CodeBlock, Lightbox, MobileNav
    - templated / static aria-labels
  MultiSelector, NumberInput, Selector, TextInput, TimeInput, FileInput
    - `Clear ${label}` aria-labels
  SideNavCollapseButton
    - expand/collapse sidebar ternary
  Table plugins:
    - useTableFiltering — 14 `Filter ${header}` sites consolidated
      behind one shared key with ICU {header} variable
    - useTableGroupedRows — expand/collapse group templates
    - useTableRowExpansion — row-toggle and all-rows-toggle ternaries
  Thumbnail, Token
    - remove/open templates

33 new catalog entries in packages/core/locales/en.json, all
@AstryX.<component>.* camelCase. All interpolations expressed as ICU
MessageFormat variables. Zero user-visible English changes —
defaultMessage always matches the pre-migration string exactly.

5 sub-components in useTableFiltering that lacked `useTranslator`
now have it. useTriggerMenu / useTableGroupedRows /
useTableRowExpansion needed `t` added to a few exhaustive-deps
arrays (no-op at runtime since useTranslator returns a stable
reference).

Refs #3641, builds on #4010
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-remaining-fixes branch from bd1f0b2 to 3396467 Compare July 17, 2026 20:24
Adds an ESLint rule that flags hardcoded user-facing string literals on
JSX attributes, object properties, and destructure defaults when the
prop name matches user-facing conventions:

  Exact:    label, placeholder, title, tooltip, text, summary, message,
            description, hint
  Suffix:   *Label, *Text, *Placeholder, *Title, *Message, *Tooltip,
            *Hint, *Description, *Summary
  aria-*:   Only WAI-ARIA text-valued attributes — aria-label,
            aria-description, aria-placeholder, aria-roledescription,
            aria-valuetext, aria-braillelabel,
            aria-brailleroledescription, aria-keyshortcuts. Attributes
            taking IDs / booleans / enums are intentionally not
            flagged.

Ignores test files, stories, doc files, and identifier-shaped values
(single lowercase words, SCREAMING_SNAKE constants, URLs, paths).

The rule is filesystem-agnostic — it hardcodes no paths. Consumers
scope it with the standard flat-config 'files' / 'ignores' pattern,
which lets downstream packages that build on astryx and ship
translatable UI enable it too. In this repo it is registered under the
existing astryx config (already scoped to packages/core/src/**) with
an additional override that turns it off for packages/core/src/i18n/**
(the runtime that defines the strings).

Configuration follows the plugin convention: 'error' in strict,
'warn' in recommended.

The pre-existing hardcoded strings this rule flagged are fixed in a
sibling PR so this PR contains only the rule + wiring and can be
reviewed in isolation.

Refs #3641
@nynexman4464
nynexman4464 force-pushed the nynexman4464/feat/i18n-lint-rule branch from 726cf27 to 3e97482 Compare July 17, 2026 21:12
@nynexman4464
nynexman4464 changed the base branch from nynexman4464/feat/i18n-remaining-fixes to main July 17, 2026 21:12
github-actions Bot added a commit that referenced this pull request Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

No new or modified components detected.

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.7KB 0B

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@nynexman4464
nynexman4464 merged commit dd1d655 into main Jul 17, 2026
19 checks passed
nynexman4464 added a commit that referenced this pull request Jul 17, 2026
Extends the i18n migration series to catch the two patterns that a
plain-literal audit missed:

  aria-label={isOpen ? 'Close X' : 'Open X'}     (ternary in JSX attr)
  aria-label={`Clear ${label}`}                (templated string)

Both slipped past the initial migration because the value on the
attribute is a JSX expression container, not a bare string literal.
Real-world integration testing (French / pseudo locale swap on a
consumer app) caught them.

Migrations by component (27 files):

  DateInput / DateRangeInput / DateTimeInput
    - calendar toggle aria-label (open/close)
    - clear button aria-label (`Clear ${label}`)
  Banner, SideNavItem, ChatComposerDrawer
    - expand/collapse ternaries
  ChatSendButton
    - send/stop ternary
  ChatLayout, ChatLayoutScrollButton, ChatMessage, ChatTriggerMenu
    - templated aria-labels
  Citation, CodeBlock, Lightbox, MobileNav
    - templated / static aria-labels
  MultiSelector, NumberInput, Selector, TextInput, TimeInput, FileInput
    - `Clear ${label}` aria-labels
  SideNavCollapseButton
    - expand/collapse sidebar ternary
  Table plugins:
    - useTableFiltering — 14 `Filter ${header}` sites consolidated
      behind one shared key with ICU {header} variable
    - useTableGroupedRows — expand/collapse group templates
    - useTableRowExpansion — row-toggle and all-rows-toggle ternaries
  Thumbnail, Token
    - remove/open templates

33 new catalog entries in packages/core/locales/en.json, all
@AstryX.<component>.* camelCase. All interpolations expressed as ICU
MessageFormat variables. Zero user-visible English changes —
defaultMessage always matches the pre-migration string exactly.

5 sub-components in useTableFiltering that lacked `useTranslator`
now have it. useTriggerMenu / useTableGroupedRows /
useTableRowExpansion needed `t` added to a few exhaustive-deps
arrays (no-op at runtime since useTranslator returns a stable
reference).

Refs #3641, builds on #4010
nynexman4464 added a commit that referenced this pull request Jul 17, 2026
…#4013)

Extends the i18n migration series to catch the two patterns that a
plain-literal audit missed:

  aria-label={isOpen ? 'Close X' : 'Open X'}     (ternary in JSX attr)
  aria-label={`Clear ${label}`}                (templated string)

Both slipped past the initial migration because the value on the
attribute is a JSX expression container, not a bare string literal.
Real-world integration testing (French / pseudo locale swap on a
consumer app) caught them.

Migrations by component (27 files):

  DateInput / DateRangeInput / DateTimeInput
    - calendar toggle aria-label (open/close)
    - clear button aria-label (`Clear ${label}`)
  Banner, SideNavItem, ChatComposerDrawer
    - expand/collapse ternaries
  ChatSendButton
    - send/stop ternary
  ChatLayout, ChatLayoutScrollButton, ChatMessage, ChatTriggerMenu
    - templated aria-labels
  Citation, CodeBlock, Lightbox, MobileNav
    - templated / static aria-labels
  MultiSelector, NumberInput, Selector, TextInput, TimeInput, FileInput
    - `Clear ${label}` aria-labels
  SideNavCollapseButton
    - expand/collapse sidebar ternary
  Table plugins:
    - useTableFiltering — 14 `Filter ${header}` sites consolidated
      behind one shared key with ICU {header} variable
    - useTableGroupedRows — expand/collapse group templates
    - useTableRowExpansion — row-toggle and all-rows-toggle ternaries
  Thumbnail, Token
    - remove/open templates

33 new catalog entries in packages/core/locales/en.json, all
@AstryX.<component>.* camelCase. All interpolations expressed as ICU
MessageFormat variables. Zero user-visible English changes —
defaultMessage always matches the pre-migration string exactly.

5 sub-components in useTableFiltering that lacked `useTranslator`
now have it. useTriggerMenu / useTableGroupedRows /
useTableRowExpansion needed `t` added to a few exhaustive-deps
arrays (no-op at runtime since useTranslator returns a stable
reference).

Refs #3641, builds on #4010
@github-actions
github-actions Bot deleted the nynexman4464/feat/i18n-lint-rule branch July 18, 2026 06:42
josephfarina pushed a commit that referenced this pull request Jul 22, 2026
…#4149)

The tree data plugin (#3789) shipped hardcoded "Expand row" / "Collapse
row" aria-labels on the expander button. The @astryx/no-hardcoded-i18n-string
rule (added in #4010) flags these as errors, so `pnpm lint` fails
repo-wide — main is red and every open PR inherits the failure.

Route both strings through useTranslator() with new catalog keys
@astryx.tableTree.expandRow / .collapseRow (mirroring the sibling
selection and rowExpansion plugins) and add matching entries to
packages/core/locales/en.json.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant