feat(i18n): add eloqnt lint, and fix what it found - #9106
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
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. Thank you! |
There was a problem hiding this comment.
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
| "guides": "الأدلة", | ||
| "learn": "تعلّم", | ||
| "security": "الأمان", |
There was a problem hiding this comment.
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
| "overview": "نظرة عامة", | ||
| "minorVersions": "الإصدارات الفرعية", | ||
| "releaseAnnouncement": "إعلان الإصدار", | ||
| "unsupportedVersionWarning": "هذا الإصدار خارج مرحلة الصيانة. يرجى استخدام إصدار مدعوم. <link>تعرّف على دعم EOL.</link>", |
There was a problem hiding this comment.
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
| "addedIn": "أضيف في", | ||
| "author": "المؤلف", | ||
| "authors": "المؤلفون", | ||
| "contribute": "المساهمة", | ||
| "contributeText": "عدّل هذه الصفحة", | ||
| "viewAs": "عرض كـ", |
There was a problem hiding this comment.
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
| "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": "إغلاق الدردشة", |
There was a problem hiding this comment.
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": "المجلس الاستشاري", |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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>" |
There was a problem hiding this comment.
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.
Generated by Claude Code
| "noScriptDetected": "此網頁需要使用 JavaScript。您可以直接造訪 <link>發行網頁</link> 以下載 Node.js,無需 JavaScript。", | ||
| "platformInfo": { | ||
| "default": "套件管理器與其安裝指令稿並非由 Node.js 專案維護。", | ||
| "default": "{platform}與其安裝指令稿並非由 Node.js 專案維護。", |
There was a problem hiding this comment.
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
| .map(locale => locale.code), | ||
| sourceLocale: 'en', | ||
| format: 'json', | ||
| }, |
There was a problem hiding this comment.
Since this consumes files both from ./apps/site and ./packages/i18n, I've added this as a monorepo dependency.
| ], | ||
| rules: { 'orphan-message': 'off' }, | ||
| }, | ||
| ], |
There was a problem hiding this comment.
A few exceptions to avoid refactors to code (see also: orphan-messages false positives)
eloqnt lint, and fix what it found
858738f to
cfa92d5
Compare
cfa92d5 to
db0b4db
Compare
|
|
||
| minimumReleaseAgeExclude: | ||
| - '@eloqnt/cli@0.6.9' | ||
| - '@eloqnt/sdk@0.6.8' |
Description
Not sure if you've seen it, but I've worked on
eloqnt/cli, a linter for i18n issues that is specifically made fornext-intlusers.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-intldrops an unused tag or value rather than throwing:<link>tag, so the sentence ends on a colon and the link never renders.{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 innavigation.json.140 keys in translation files with no counterpart in
en.json— 57 of them thecomponents.navigation.learn.*subtree, left behind when the learn section moved to an external link.Result
eloqnt lintreports 0 errors. The remaining 562 warnings are untranslated strings, which are pre-existing and unrelated to this PR.Validation
pnpm run lint:i18ngoes from 920 errors / 719 warnings to 0 errors / 562 warnings; every remaining warning is amissing-translation, all pre-existing (the drop from 719 came from removing translations of the dead keys).withMetaBar.tsx,navigation.json, blog category front matter) were checked by hand.git diffonpackages/i18n/src/localestouches every locale file and only locale files; the superfluous-key removal did not changeen.json.pnpm testandpnpm buildpass;tsc --noEmitran via the pre-commit hook on every commit.Related Issues
Check List
pnpm formatto ensure the code follows the style guide.pnpm testto check if all tests are passing.pnpm buildto check if the website builds without errors.