@neokapi/i18n-react-lint 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +78 -0
- package/dist/configs/recommended-strict.d.ts +36 -0
- package/dist/configs/recommended-strict.js +20 -0
- package/dist/configs/recommended-strict.js.map +7 -0
- package/dist/configs/recommended.d.ts +40 -0
- package/dist/configs/recommended.js +19 -0
- package/dist/configs/recommended.js.map +7 -0
- package/dist/eslint.d.ts +8 -0
- package/dist/eslint.js +10 -0
- package/dist/eslint.js.map +7 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +7 -0
- package/dist/oxlint.d.ts +16 -0
- package/dist/oxlint.js +6 -0
- package/dist/oxlint.js.map +7 -0
- package/dist/rules/no-concat-in-translatable-attr.d.ts +8 -0
- package/dist/rules/no-concat-in-translatable-attr.js +62 -0
- package/dist/rules/no-concat-in-translatable-attr.js.map +7 -0
- package/dist/rules/no-string-literal-jsx-expr.d.ts +11 -0
- package/dist/rules/no-string-literal-jsx-expr.js +42 -0
- package/dist/rules/no-string-literal-jsx-expr.js.map +7 -0
- package/dist/rules/no-ternary-in-translatable-attr.d.ts +16 -0
- package/dist/rules/no-ternary-in-translatable-attr.js +51 -0
- package/dist/rules/no-ternary-in-translatable-attr.js.map +7 -0
- package/dist/rules/no-ternary-literals-in-jsx-child.d.ts +18 -0
- package/dist/rules/no-ternary-literals-in-jsx-child.js +60 -0
- package/dist/rules/no-ternary-literals-in-jsx-child.js.map +7 -0
- package/dist/rules/prefer-t-for-label-expr.d.ts +19 -0
- package/dist/rules/prefer-t-for-label-expr.js +72 -0
- package/dist/rules/prefer-t-for-label-expr.js.map +7 -0
- package/dist/rules/prefer-t-for-label-props.d.ts +25 -0
- package/dist/rules/prefer-t-for-label-props.js +50 -0
- package/dist/rules/prefer-t-for-label-props.js.map +7 -0
- package/dist/rules/t-literal-first-arg.d.ts +10 -0
- package/dist/rules/t-literal-first-arg.js +46 -0
- package/dist/rules/t-literal-first-arg.js.map +7 -0
- package/dist/rules/t-no-concat.d.ts +10 -0
- package/dist/rules/t-no-concat.js +46 -0
- package/dist/rules/t-no-concat.js.map +7 -0
- package/dist/shared/t-import.d.ts +13 -0
- package/dist/shared/t-import.js +22 -0
- package/dist/shared/t-import.js.map +7 -0
- package/dist/shared/translatable-attrs.d.ts +26 -0
- package/dist/shared/translatable-attrs.js +41 -0
- package/dist/shared/translatable-attrs.js.map +7 -0
- package/dist/shared/translate-no.d.ts +12 -0
- package/dist/shared/translate-no.js +27 -0
- package/dist/shared/translate-no.js.map +7 -0
- package/package.json +76 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function collectTLocalNames(context) {
|
|
2
|
+
const names = /* @__PURE__ */ new Set();
|
|
3
|
+
const program = context.sourceCode.ast;
|
|
4
|
+
for (const stmt of program.body) {
|
|
5
|
+
if (!isRuntimeImport(stmt)) continue;
|
|
6
|
+
for (const spec of stmt.specifiers) {
|
|
7
|
+
if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier" && spec.imported.name === "t") {
|
|
8
|
+
names.add(spec.local.name);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return names;
|
|
13
|
+
}
|
|
14
|
+
function isRuntimeImport(node) {
|
|
15
|
+
if (node.type !== "ImportDeclaration") return false;
|
|
16
|
+
const src = node.source.value;
|
|
17
|
+
return typeof src === "string" && (src === "@neokapi/i18n-react/runtime" || src === "@neokapi/i18n-react");
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
collectTLocalNames
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=t-import.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/shared/t-import.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Context } from \"@oxlint/plugins\";\nimport type { ImportDeclaration, Node, Program } from \"estree\";\n\n/**\n * Walks an ESTree program and returns the set of local identifier\n * names bound to the `t` function from `@neokapi/i18n-react/runtime`.\n *\n * Handles:\n * import { t } from '@neokapi/i18n-react/runtime'\n * import { t as translate } from '@neokapi/i18n-react/runtime'\n *\n * Returns a Set because a file could in principle import `t` twice\n * under different aliases, and we want to catch all of them.\n */\nexport function collectTLocalNames(context: Context): Set<string> {\n const names = new Set<string>();\n // Both ESLint and oxlint hand JS rules an ESTree-shaped AST at runtime.\n // The oxlint types model the oxc AST, so bridge to ESTree node types here.\n const program = context.sourceCode.ast as unknown as Program;\n for (const stmt of program.body) {\n if (!isRuntimeImport(stmt)) continue;\n for (const spec of stmt.specifiers) {\n if (\n spec.type === \"ImportSpecifier\" &&\n spec.imported.type === \"Identifier\" &&\n spec.imported.name === \"t\"\n ) {\n names.add(spec.local.name);\n }\n }\n }\n return names;\n}\n\nfunction isRuntimeImport(node: Node): node is ImportDeclaration {\n if (node.type !== \"ImportDeclaration\") return false;\n const src = node.source.value;\n return (\n typeof src === \"string\" &&\n (src === \"@neokapi/i18n-react/runtime\" || src === \"@neokapi/i18n-react\")\n );\n}\n"],
|
|
5
|
+
"mappings": "AAcO,SAAS,mBAAmB,SAA+B;AAChE,QAAM,QAAQ,oBAAI,IAAY;AAG9B,QAAM,UAAU,QAAQ,WAAW;AACnC,aAAW,QAAQ,QAAQ,MAAM;AAC/B,QAAI,CAAC,gBAAgB,IAAI,EAAG;AAC5B,eAAW,QAAQ,KAAK,YAAY;AAClC,UACE,KAAK,SAAS,qBACd,KAAK,SAAS,SAAS,gBACvB,KAAK,SAAS,SAAS,KACvB;AACA,cAAM,IAAI,KAAK,MAAM,IAAI;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,gBAAgB,MAAuC;AAC9D,MAAI,KAAK,SAAS,oBAAqB,QAAO;AAC9C,QAAM,MAAM,KAAK,OAAO;AACxB,SACE,OAAO,QAAQ,aACd,QAAQ,iCAAiC,QAAQ;AAEtD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attribute names that neokapi-i18n extracts. Keep in sync with
|
|
3
|
+
* packages/i18n-react's defaults (`translatableAttributes`) —
|
|
4
|
+
* duplicated rather than imported so this package stays usable
|
|
5
|
+
* without installing the full neokapi-i18n transform, and guarded by
|
|
6
|
+
* tests/attr-sync.test.ts which compares against the canonical set.
|
|
7
|
+
*
|
|
8
|
+
* Note the extractor scopes the React-convention bucket to PascalCase
|
|
9
|
+
* components; the lint vocabulary stays the union (a flagged concat
|
|
10
|
+
* in `label=` on an html element is still worth a look).
|
|
11
|
+
*/
|
|
12
|
+
export declare const TRANSLATABLE_ATTRS: ReadonlySet<string>;
|
|
13
|
+
/**
|
|
14
|
+
* Object-literal keys we treat as "likely user-facing strings" when
|
|
15
|
+
* checking data-array patterns like `const ITEMS = [{ label: 'Foo' }]`.
|
|
16
|
+
* Intentionally narrower than TRANSLATABLE_ATTRS — must be a strong
|
|
17
|
+
* signal to keep false positives low.
|
|
18
|
+
*
|
|
19
|
+
* Explicitly excluded: `name`, `description`, `text`, `message`. Those
|
|
20
|
+
* overwhelmingly name backend / runtime data in real React apps
|
|
21
|
+
* (plugin.name, error.message, schema.description, file.name, …) that
|
|
22
|
+
* isn't authoring-time translatable, so flagging them fires mostly
|
|
23
|
+
* false positives. The remaining set skews strongly toward hardcoded
|
|
24
|
+
* UI copy.
|
|
25
|
+
*/
|
|
26
|
+
export declare const LIKELY_LABEL_KEYS: ReadonlySet<string>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const TRANSLATABLE_ATTRS = /* @__PURE__ */ new Set([
|
|
2
|
+
// HTML
|
|
3
|
+
"alt",
|
|
4
|
+
"title",
|
|
5
|
+
"placeholder",
|
|
6
|
+
// ARIA
|
|
7
|
+
"aria-label",
|
|
8
|
+
"aria-description",
|
|
9
|
+
"aria-placeholder",
|
|
10
|
+
"aria-roledescription",
|
|
11
|
+
"aria-valuetext",
|
|
12
|
+
// React conventions
|
|
13
|
+
"subtitle",
|
|
14
|
+
"description",
|
|
15
|
+
"label",
|
|
16
|
+
"heading",
|
|
17
|
+
"caption",
|
|
18
|
+
"helpText",
|
|
19
|
+
"helperText",
|
|
20
|
+
"errorMessage",
|
|
21
|
+
"hint",
|
|
22
|
+
"tooltip",
|
|
23
|
+
"emptyMessage",
|
|
24
|
+
"emptyStateText",
|
|
25
|
+
"filterPlaceholder"
|
|
26
|
+
]);
|
|
27
|
+
const LIKELY_LABEL_KEYS = /* @__PURE__ */ new Set([
|
|
28
|
+
"label",
|
|
29
|
+
"title",
|
|
30
|
+
"heading",
|
|
31
|
+
"caption",
|
|
32
|
+
"subtitle",
|
|
33
|
+
"tooltip",
|
|
34
|
+
"placeholder",
|
|
35
|
+
"summary"
|
|
36
|
+
]);
|
|
37
|
+
export {
|
|
38
|
+
LIKELY_LABEL_KEYS,
|
|
39
|
+
TRANSLATABLE_ATTRS
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=translatable-attrs.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/shared/translatable-attrs.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Attribute names that neokapi-i18n extracts. Keep in sync with\n * packages/i18n-react's defaults (`translatableAttributes`) \u2014\n * duplicated rather than imported so this package stays usable\n * without installing the full neokapi-i18n transform, and guarded by\n * tests/attr-sync.test.ts which compares against the canonical set.\n *\n * Note the extractor scopes the React-convention bucket to PascalCase\n * components; the lint vocabulary stays the union (a flagged concat\n * in `label=` on an html element is still worth a look).\n */\nexport const TRANSLATABLE_ATTRS: ReadonlySet<string> = new Set([\n // HTML\n \"alt\",\n \"title\",\n \"placeholder\",\n // ARIA\n \"aria-label\",\n \"aria-description\",\n \"aria-placeholder\",\n \"aria-roledescription\",\n \"aria-valuetext\",\n // React conventions\n \"subtitle\",\n \"description\",\n \"label\",\n \"heading\",\n \"caption\",\n \"helpText\",\n \"helperText\",\n \"errorMessage\",\n \"hint\",\n \"tooltip\",\n \"emptyMessage\",\n \"emptyStateText\",\n \"filterPlaceholder\",\n]);\n\n/**\n * Object-literal keys we treat as \"likely user-facing strings\" when\n * checking data-array patterns like `const ITEMS = [{ label: 'Foo' }]`.\n * Intentionally narrower than TRANSLATABLE_ATTRS \u2014 must be a strong\n * signal to keep false positives low.\n *\n * Explicitly excluded: `name`, `description`, `text`, `message`. Those\n * overwhelmingly name backend / runtime data in real React apps\n * (plugin.name, error.message, schema.description, file.name, \u2026) that\n * isn't authoring-time translatable, so flagging them fires mostly\n * false positives. The remaining set skews strongly toward hardcoded\n * UI copy.\n */\nexport const LIKELY_LABEL_KEYS: ReadonlySet<string> = new Set([\n \"label\",\n \"title\",\n \"heading\",\n \"caption\",\n \"subtitle\",\n \"tooltip\",\n \"placeholder\",\n \"summary\",\n]);\n"],
|
|
5
|
+
"mappings": "AAWO,MAAM,qBAA0C,oBAAI,IAAI;AAAA;AAAA,EAE7D;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAeM,MAAM,oBAAyC,oBAAI,IAAI;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W3C `translate="no"` is the standard hint for "don't try to
|
|
3
|
+
* translate this subtree". neokapi-i18n's extractor already respects
|
|
4
|
+
* it (walker.ts early-returns from the element), and the lint rules
|
|
5
|
+
* should too — otherwise a developer using the HTML-standard escape
|
|
6
|
+
* hatch still gets noisy warnings for dynamic / code-like content.
|
|
7
|
+
*
|
|
8
|
+
* The helper walks up through the parent chain from an arbitrary
|
|
9
|
+
* starting node (attribute, expression container, whatever) and
|
|
10
|
+
* returns true as soon as it finds a JSX ancestor with the attribute.
|
|
11
|
+
*/
|
|
12
|
+
export declare function hasTranslateNoAncestor(start: unknown): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function hasTranslateNoAncestor(start) {
|
|
2
|
+
let cursor = start;
|
|
3
|
+
while (cursor) {
|
|
4
|
+
if (cursor.type === "JSXElement") {
|
|
5
|
+
const open = cursor.openingElement ?? cursor.opening;
|
|
6
|
+
if (hasTranslateNoAttr(open)) return true;
|
|
7
|
+
}
|
|
8
|
+
cursor = cursor.parent;
|
|
9
|
+
}
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
function hasTranslateNoAttr(opening) {
|
|
13
|
+
const attrs = opening?.attributes;
|
|
14
|
+
if (!Array.isArray(attrs)) return false;
|
|
15
|
+
for (const a of attrs) {
|
|
16
|
+
const attr = a;
|
|
17
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
18
|
+
if (attr.name?.type !== "JSXIdentifier" || attr.name.name !== "translate") continue;
|
|
19
|
+
const v = attr.value;
|
|
20
|
+
if (v?.type === "Literal" && v.value === "no") return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
hasTranslateNoAncestor
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=translate-no.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/shared/translate-no.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * W3C `translate=\"no\"` is the standard hint for \"don't try to\n * translate this subtree\". neokapi-i18n's extractor already respects\n * it (walker.ts early-returns from the element), and the lint rules\n * should too \u2014 otherwise a developer using the HTML-standard escape\n * hatch still gets noisy warnings for dynamic / code-like content.\n *\n * The helper walks up through the parent chain from an arbitrary\n * starting node (attribute, expression container, whatever) and\n * returns true as soon as it finds a JSX ancestor with the attribute.\n */\nexport function hasTranslateNoAncestor(start: unknown): boolean {\n let cursor = start as\n | {\n type?: string;\n parent?: unknown;\n // ESLint (acorn-jsx) calls it `openingElement`; SWC calls it\n // `opening`. Accept both so the helper works under either\n // AST provider.\n openingElement?: unknown;\n opening?: unknown;\n }\n | undefined;\n while (cursor) {\n if (cursor.type === \"JSXElement\") {\n const open = cursor.openingElement ?? cursor.opening;\n if (hasTranslateNoAttr(open)) return true;\n }\n cursor = cursor.parent as typeof cursor;\n }\n return false;\n}\n\nfunction hasTranslateNoAttr(opening: unknown): boolean {\n const attrs = (opening as { attributes?: unknown[] } | undefined)?.attributes;\n if (!Array.isArray(attrs)) return false;\n for (const a of attrs) {\n const attr = a as {\n type?: string;\n name?: { type?: string; name?: string };\n value?: { type?: string; value?: unknown };\n };\n if (attr.type !== \"JSXAttribute\") continue;\n if (attr.name?.type !== \"JSXIdentifier\" || attr.name.name !== \"translate\") continue;\n const v = attr.value;\n if (v?.type === \"Literal\" && v.value === \"no\") return true;\n }\n return false;\n}\n"],
|
|
5
|
+
"mappings": "AAWO,SAAS,uBAAuB,OAAyB;AAC9D,MAAI,SAAS;AAWb,SAAO,QAAQ;AACb,QAAI,OAAO,SAAS,cAAc;AAChC,YAAM,OAAO,OAAO,kBAAkB,OAAO;AAC7C,UAAI,mBAAmB,IAAI,EAAG,QAAO;AAAA,IACvC;AACA,aAAS,OAAO;AAAA,EAClB;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,SAA2B;AACrD,QAAM,QAAS,SAAoD;AACnE,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO;AAClC,aAAW,KAAK,OAAO;AACrB,UAAM,OAAO;AAKb,QAAI,KAAK,SAAS,eAAgB;AAClC,QAAI,KAAK,MAAM,SAAS,mBAAmB,KAAK,KAAK,SAAS,YAAa;AAC3E,UAAM,IAAI,KAAK;AACf,QAAI,GAAG,SAAS,aAAa,EAAE,UAAU,KAAM,QAAO;AAAA,EACxD;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neokapi/i18n-react-lint",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Authoring-time lint rules for neokapi-i18n. Same rule objects run under ESLint and oxlint (via jsPlugins). Catches unextractable patterns (t(variable), string concat in translatable attrs, label strings hidden in data arrays) that the build-time extractor can't see.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"eslint",
|
|
8
|
+
"i18n",
|
|
9
|
+
"neokapi-i18n",
|
|
10
|
+
"lint",
|
|
11
|
+
"oxlint",
|
|
12
|
+
"react"
|
|
13
|
+
],
|
|
14
|
+
"license": "Apache-2.0",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./eslint": {
|
|
30
|
+
"types": "./dist/eslint.d.ts",
|
|
31
|
+
"default": "./dist/eslint.js"
|
|
32
|
+
},
|
|
33
|
+
"./oxlint": {
|
|
34
|
+
"types": "./dist/oxlint.d.ts",
|
|
35
|
+
"default": "./dist/oxlint.js"
|
|
36
|
+
},
|
|
37
|
+
"./recommended": {
|
|
38
|
+
"types": "./dist/configs/recommended.d.ts",
|
|
39
|
+
"default": "./dist/configs/recommended.js"
|
|
40
|
+
},
|
|
41
|
+
"./recommended-strict": {
|
|
42
|
+
"types": "./dist/configs/recommended-strict.d.ts",
|
|
43
|
+
"default": "./dist/configs/recommended-strict.js"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@oxlint/plugins": "^1.73.0",
|
|
48
|
+
"@types/estree": "^1.0.0",
|
|
49
|
+
"@types/node": "^25.9.2",
|
|
50
|
+
"esbuild": "^0.28.1",
|
|
51
|
+
"eslint": "^10.7.0",
|
|
52
|
+
"typescript": "^7.0.2",
|
|
53
|
+
"vitest": "^4.1.10"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"eslint": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=22"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "node build.mjs",
|
|
68
|
+
"test": "vp test",
|
|
69
|
+
"test:watch": "vp test watch",
|
|
70
|
+
"typecheck": "vp check --no-fmt --no-lint",
|
|
71
|
+
"lint": "vp lint",
|
|
72
|
+
"fmt": "vp fmt --write src/ tests/",
|
|
73
|
+
"fmt:check": "vp fmt --check src/ tests/",
|
|
74
|
+
"check": "vp run typecheck && vp run lint && vp run fmt:check && vp run test"
|
|
75
|
+
}
|
|
76
|
+
}
|