@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.
Files changed (51) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +78 -0
  3. package/dist/configs/recommended-strict.d.ts +36 -0
  4. package/dist/configs/recommended-strict.js +20 -0
  5. package/dist/configs/recommended-strict.js.map +7 -0
  6. package/dist/configs/recommended.d.ts +40 -0
  7. package/dist/configs/recommended.js +19 -0
  8. package/dist/configs/recommended.js.map +7 -0
  9. package/dist/eslint.d.ts +8 -0
  10. package/dist/eslint.js +10 -0
  11. package/dist/eslint.js.map +7 -0
  12. package/dist/index.d.ts +26 -0
  13. package/dist/index.js +30 -0
  14. package/dist/index.js.map +7 -0
  15. package/dist/oxlint.d.ts +16 -0
  16. package/dist/oxlint.js +6 -0
  17. package/dist/oxlint.js.map +7 -0
  18. package/dist/rules/no-concat-in-translatable-attr.d.ts +8 -0
  19. package/dist/rules/no-concat-in-translatable-attr.js +62 -0
  20. package/dist/rules/no-concat-in-translatable-attr.js.map +7 -0
  21. package/dist/rules/no-string-literal-jsx-expr.d.ts +11 -0
  22. package/dist/rules/no-string-literal-jsx-expr.js +42 -0
  23. package/dist/rules/no-string-literal-jsx-expr.js.map +7 -0
  24. package/dist/rules/no-ternary-in-translatable-attr.d.ts +16 -0
  25. package/dist/rules/no-ternary-in-translatable-attr.js +51 -0
  26. package/dist/rules/no-ternary-in-translatable-attr.js.map +7 -0
  27. package/dist/rules/no-ternary-literals-in-jsx-child.d.ts +18 -0
  28. package/dist/rules/no-ternary-literals-in-jsx-child.js +60 -0
  29. package/dist/rules/no-ternary-literals-in-jsx-child.js.map +7 -0
  30. package/dist/rules/prefer-t-for-label-expr.d.ts +19 -0
  31. package/dist/rules/prefer-t-for-label-expr.js +72 -0
  32. package/dist/rules/prefer-t-for-label-expr.js.map +7 -0
  33. package/dist/rules/prefer-t-for-label-props.d.ts +25 -0
  34. package/dist/rules/prefer-t-for-label-props.js +50 -0
  35. package/dist/rules/prefer-t-for-label-props.js.map +7 -0
  36. package/dist/rules/t-literal-first-arg.d.ts +10 -0
  37. package/dist/rules/t-literal-first-arg.js +46 -0
  38. package/dist/rules/t-literal-first-arg.js.map +7 -0
  39. package/dist/rules/t-no-concat.d.ts +10 -0
  40. package/dist/rules/t-no-concat.js +46 -0
  41. package/dist/rules/t-no-concat.js.map +7 -0
  42. package/dist/shared/t-import.d.ts +13 -0
  43. package/dist/shared/t-import.js +22 -0
  44. package/dist/shared/t-import.js.map +7 -0
  45. package/dist/shared/translatable-attrs.d.ts +26 -0
  46. package/dist/shared/translatable-attrs.js +41 -0
  47. package/dist/shared/translatable-attrs.js.map +7 -0
  48. package/dist/shared/translate-no.d.ts +12 -0
  49. package/dist/shared/translate-no.js +27 -0
  50. package/dist/shared/translate-no.js.map +7 -0
  51. 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
+ }