Skip to content

feat(i18n): add eloqnt lint, and fix what it found - #9106

Open
amannn wants to merge 9 commits into
nodejs:mainfrom
amannn:claude/eloquent-lint-setup-erc7tb
Open

feat(i18n): add eloqnt lint, and fix what it found#9106
amannn wants to merge 9 commits into
nodejs:mainfrom
amannn:claude/eloquent-lint-setup-erc7tb

Conversation

@amannn

@amannn amannn commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Description

Not sure if you've seen it, but I've worked on eloqnt/cli, a linter for i18n issues that is specifically made for next-intl users.

Since nodejs.org is open source, I gave it a go on your messages. It found 920 errors that were fixable.

Claude Code added some inline comments for explanation.

Up to you if you're interested in adopting eloqnt/cli, but I think the errors are worth fixing in any case!

What it found

Two user-visible translation bugs, both silent at runtime because use-intl drops an unused tag or value rather than throwing:

  • The Spanish EOL warning on the download page lost its <link> tag, so the sentence ends on a colon and the link never renders.
  • The Traditional Chinese package-manager notice lost {platform}, so it names no package manager.

36 dead keys, including 18 left over from a removed search-chat feature, and two navigation labels (navBar.links.guides, .security) that no longer appear in navigation.json.

140 keys in translation files with no counterpart in en.json — 57 of them the components.navigation.learn.* subtree, left behind when the learn section moved to an external link.

Result

eloqnt lint reports 0 errors. The remaining 562 warnings are untranslated strings, which are pre-existing and unrelated to this PR.

Validation

  • pnpm run lint:i18n goes from 920 errors / 719 warnings to 0 errors / 562 warnings; every remaining warning is a missing-translation, all pre-existing (the drop from 719 came from removing translations of the dead keys).
  • Every deleted key was verified unreferenced with a repository-wide literal search excluding the locale catalogs; the dynamic call sites (withMetaBar.tsx, navigation.json, blog category front matter) were checked by hand.
  • git diff on packages/i18n/src/locales touches every locale file and only locale files; the superfluous-key removal did not change en.json.
  • pnpm test and pnpm build pass; tsc --noEmit ran via the pre-commit hook on every commit.

Related Issues

Check List

  • I have read the Contributing Guidelines and made commit messages that follow the guideline.
  • I have run pnpm format to ensure the code follows the style guide.
  • I have run pnpm test to check if all tests are passing.
  • I have run pnpm build to check if the website builds without errors.
  • I've covered new added functionality with unit tests if necessary.

amannn added 9 commits August 14, 2026 09:49
Each key was verified unreferenced with a repository-wide literal search
(excluding the locale catalogs). Eighteen are leftovers from the removed
search-chat feature, and navBar.links.guides/.security no longer have
entries in navigation.json. components.metabar.addedIn/.viewAs occur only
as Storybook fixture data, and no blog post uses the advisory-board
category. Keys are removed from ar.json as well, although that locale is
disabled, so it does not drift further.
Both dropped an argument the English source defines and rendered
silently, because use-intl drops an unused tag or value rather than
throwing:

- es layouts.download.codeBox.unsupportedVersionWarning lost its <link>
  tag, so the sentence ended on a colon and the EOL-support link never
  rendered.
- zh-tw layouts.download.codeBox.platformInfo.default lost {platform},
  so the sentence named no package manager.

Note: these two strings are machine-suggested and should be replaced
via 'eloqnt translate' or reviewed by a native speaker before release.
Removes 812 stale entries across ten translation files, covering keys
that were dropped from the source catalog but survived in translations.
Most of it is one fact: the components.navigation.learn.* subtree
(57 keys) was removed from en.json when the learn section moved to an
external link, and lived on in ten locales.

The list is derived from 'eloqnt lint --json' superfluous-key findings,
not transcribed by hand. en.json itself is untouched.
Adds @eloqnt/cli and a config covering the 14 enabled locales from
packages/i18n/src/config.json (ar is disabled and deliberately absent).

The orphan-message rule resolves a key only when the call site names it
as a literal string. This codebase builds some keys at runtime — labels
come from navigation.json and constants.json, and several components
interpolate a variable into the key — so the rule is turned off for
exactly those keys via lint.overrides. Namespaces that also hold
statically referenced keys list the dynamic ones individually instead
of using a wildcard, so the rule keeps covering the rest.

Result: eloqnt lint reports 0 errors and 562 pre-existing
missing-translation warnings.
…nents from srcPath

The locale list now comes from packages/i18n/src/config.json (enabled
locales only) instead of being duplicated. 'infer' is not used because it
would pick up the disabled ar.json from disk.

packages/ui-components does not use next-intl — components receive
translated strings as props — and removing it from srcPath leaves the
lint result unchanged (verified: 0 errors, 562 warnings).
The i18n message cleanup does not need a release; the empty changeset
keeps the Changesets CI check satisfied without bumping the package.
Adds a root lint:i18n script following the same pattern as the root
prettier task: the tool spans apps/site and packages/i18n, so it is
installed at the workspace root and wired into the Quality checks job
via a turbo root task. Also shortens the config comment per review.
@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
nodejs-org Ready Ready Preview Aug 14, 2026 1:57pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

Note

Your Pull Request seems to be updating Translations of the Node.js Website.

Whilst we appreciate your intent; Any Translation update should be done through our Crowdin Project.
We recommend giving a read on our Translation Guidelines.

Thank you!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule: orphan-message

Dead-key removal (this file and every other locale file). 36 keys are deleted because they are referenced nowhere in the repository — verified with a repo-wide literal search excluding the locale catalogs, plus a hand-check of the dynamic call sites that could theoretically reach them. The same 36 keys are removed from en.json and all locale files, including this disabled ar.json so it doesn't drift further; the per-locale hunks are this one change mirrored, not separate decisions. The non-obvious cases are called out on their lines below.


Generated by Claude Code

Comment on lines -39 to -41
"guides": "الأدلة",
"learn": "تعلّم",
"security": "الأمان",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule: orphan-message

guides and security look load-bearing but aren't: navbar labels are consumed via t(link.text) with keys stored in apps/site/navigation.json, and that file has no entry pointing at either — every other navBar.links.* key has one.


Generated by Claude Code

Comment on lines -89 to -92
"overview": "نظرة عامة",
"minorVersions": "الإصدارات الفرعية",
"releaseAnnouncement": "إعلان الإصدار",
"unsupportedVersionWarning": "هذا الإصدار خارج مرحلة الصيانة. يرجى استخدام إصدار مدعوم. <link>تعرّف على دعم EOL.</link>",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule: orphan-message

The release modal only renders title, titleWithoutCodename, minorVersions and ltsVersionFeaturesNotice; overview, releaseAnnouncement and unsupportedVersionWarning have no call site.

Deleting unsupportedVersionWarning here also resolves an inconsistent-args error on this key in zh-tw (that translation had dropped the <link> tag) without spending a translation on a dead key — the live EOL warning is the separate layouts.download.codeBox.unsupportedVersionWarning.


Generated by Claude Code

Comment on lines -182 to -187
"addedIn": "أضيف في",
"author": "المؤلف",
"authors": "المؤلفون",
"contribute": "المساهمة",
"contributeText": "عدّل هذه الصفحة",
"viewAs": "عرض كـ",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule: orphan-message

addedIn and viewAs sit under a namespace a dynamic call site reaches, so they deserve a closer look: the template in withMetaBar.tsx is t(`components.metabar.${usernames.length > 1 ? 'authors' : 'author'}`) and can only ever produce .author or .authors. The only occurrences of these two keys are mock props in MetaBar/index.stories.tsx — Storybook fixture data, not a render path.


Generated by Claude Code

Comment on lines -199 to -217
"chatPlaceholder": "اسألني عن أي شيء",
"noResultsFoundFor": "لم يتم العثور على نتائج لـ",
"suggestions": "اقتراحات",
"seeAll": "عرض الكل",
"addMore": "إضافة المزيد",
"clearChat": "مسح المحادثة",
"errorMessage": "حدث خطأ أثناء محاولة البحث. يرجى المحاولة مرة أخرى.",
"disclaimer": "قد تخطئ ملخصات AI. يرجى التحقق من المعلومات.",
"startYourSearch": "ابدأ البحث",
"initErrorSearch": "تعذّر تهيئة خدمة البحث",
"initErrorChat": "تعذّر تهيئة خدمة الدردشة",
"chatButtonLabel": "الحصول على ملخص AI",
"searchButtonLabel": "بحث",
"poweredBy": "مدعوم من",
"suggestionOne": "كيف أثبّت Node.js؟",
"suggestionTwo": "كيف أنشئ خادم HTTP؟",
"suggestionThree": "ترقية إصدار Node.js",
"scrollToBottom": "التمرير إلى الأسفل",
"closeChat": "إغلاق الدردشة",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule: orphan-message

These 18 components.search.* keys are leftovers from the removed search-chat feature. withSearch.tsx uses only the search keys that remain (searchPlaceholder, noResultsFoundFor, keyboardShortcuts.*). In es and tr, seeAll and poweredBy had additionally drifted into objects with a .text child, which is why their deletion also clears four superfluous-key errors.


Generated by Claude Code

"announcements": "الإعلانات",
"release": "الإصدارات",
"vulnerability": "الثغرات",
"advisory-board": "المجلس الاستشاري",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule: orphan-message

layouts.blog.categories.advisory-board: the BlogCategory type derives from this namespace, so every key here is a valid category by construction — but reachability is decided by content. No blog post in apps/site/pages/*/blog carries category: advisory-board in its front matter; the 13 categories in use plus the all tab are exactly the keys that remain.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule: superfluous-key

Superfluous-key removal (first shown here; also applies to id, ko, pt, pt-br, ro, ta, tr, zh-cn, zh-tw). Beyond the shared 36 dead keys, everything deleted in these ten files is a translation whose key no longer exists in en.json — the source string was removed at some point but the translations survived. The list was derived mechanically from eloqnt lint --json superfluous-key findings, and en.json itself is untouched by this part. The bulk is one fact: the components.navigation.learn.* subtree (57 keys) was removed from the source catalog when the learn section moved to an external link, and lived on in all ten files. These are real translations being deleted — git history is the remaining record.


Generated by Claude Code

"chocolateyIsNotNodejsPackageManager": "Chocolatey no es un administrador de paquetes de Node.js.",
"chocolateyInstructions": "Siga las instrucciones oficiales en https://chocolatey.org/",
"chocolateyNotMaintanedByNodejs": "Chocolatey no es mantenido oficialmente por el proyecto Node.js y puede que no sea compatible con la versión {version} de Node.js"
"unsupportedVersionWarning": "Esta versión está fuera de mantenimiento. Utilice una versión con soporte vigente. <link>Conozca el soporte EOL.</link>"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule: inconsistent-args

layouts.download.codeBox.unsupportedVersionWarning: this translation had dropped the <link> tag the English source defines — the sentence ended on a colon ("Encuentre más información aquí:") and the EOL-support link never rendered.

Image

Generated by Claude Code

"noScriptDetected": "此網頁需要使用 JavaScript。您可以直接造訪 <link>發行網頁</link> 以下載 Node.js,無需 JavaScript。",
"platformInfo": {
"default": "套件管理器與其安裝指令稿並非由 Node.js 專案維護。",
"default": "{platform}與其安裝指令稿並非由 Node.js 專案維護。",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule: inconsistent-args

layouts.download.codeBox.platformInfo.default: this translation had dropped {platform}, so the rendered sentence named no package manager ("套件管理器與其安裝指令稿…" — "package managers and their install scripts…" generically). The fix restores the argument, matching the English source.


Generated by Claude Code

Comment thread .eloqnt/config.ts
.map(locale => locale.code),
sourceLocale: 'en',
format: 'json',
},

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this consumes files both from ./apps/site and ./packages/i18n, I've added this as a monorepo dependency.

Comment thread .eloqnt/config.ts
],
rules: { 'orphan-message': 'off' },
},
],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few exceptions to avoid refactors to code (see also: orphan-messages false positives)

@amannn amannn changed the title feat(i18n): add eloqnt lint, and fix what it found feat(i18n): add eloqnt lint, and fix what it found Aug 14, 2026
@amannn
amannn marked this pull request as ready for review August 14, 2026 12:52
@amannn
amannn requested review from a team as code owners August 14, 2026 12:52
@amannn
amannn force-pushed the claude/eloquent-lint-setup-erc7tb branch from 858738f to cfa92d5 Compare August 14, 2026 13:55
Comment thread pnpm-workspace.yaml
Comment on lines +24 to +27

minimumReleaseAgeExclude:
- '@eloqnt/cli@0.6.9'
- '@eloqnt/sdk@0.6.8'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you revert this?

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.

2 participants