@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,11 @@
|
|
|
1
|
+
import type { Rule } from "@oxlint/plugins";
|
|
2
|
+
/**
|
|
3
|
+
* Flags `<p>{'Hello'}</p>` — a bare string literal inside a JSX
|
|
4
|
+
* expression container. This looks extractable but isn't: the
|
|
5
|
+
* neokapi-i18n transform walks JSXText nodes, not expression containers
|
|
6
|
+
* with string literals inside them.
|
|
7
|
+
*
|
|
8
|
+
* A developer who writes this usually meant `<p>Hello</p>` (trivially
|
|
9
|
+
* extractable) or meant to interpolate with `t()` but forgot.
|
|
10
|
+
*/
|
|
11
|
+
export declare const rule: Rule;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const rule = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "problem",
|
|
4
|
+
docs: {
|
|
5
|
+
description: "flag string literals wrapped in JSX expression containers \u2014 not extractable, probably unintended",
|
|
6
|
+
recommended: true
|
|
7
|
+
},
|
|
8
|
+
schema: [],
|
|
9
|
+
fixable: "code",
|
|
10
|
+
messages: {
|
|
11
|
+
bareLiteral: "String literal inside a JSX expression container is not extractable. Write it as JSX text (`<p>Hello</p>`) or wrap with `t()` if it must be an expression."
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
create(context) {
|
|
15
|
+
return {
|
|
16
|
+
JSXExpressionContainer(node) {
|
|
17
|
+
const container = node;
|
|
18
|
+
if (container.parent?.type !== "JSXElement" && container.parent?.type !== "JSXFragment") {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const expr = container.expression;
|
|
22
|
+
if (!expr) return;
|
|
23
|
+
if (expr.type !== "Literal") return;
|
|
24
|
+
if (typeof expr.value !== "string") return;
|
|
25
|
+
if (expr.value.trim() === "") return;
|
|
26
|
+
context.report({
|
|
27
|
+
node,
|
|
28
|
+
messageId: "bareLiteral",
|
|
29
|
+
fix(fixer) {
|
|
30
|
+
if (typeof expr.value !== "string") return null;
|
|
31
|
+
if (/[\r\n{}]/.test(expr.value)) return null;
|
|
32
|
+
return fixer.replaceText(node, expr.value);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
export {
|
|
40
|
+
rule
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=no-string-literal-jsx-expr.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/rules/no-string-literal-jsx-expr.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Rule, Node } from \"@oxlint/plugins\";\n\n/**\n * Flags `<p>{'Hello'}</p>` \u2014 a bare string literal inside a JSX\n * expression container. This looks extractable but isn't: the\n * neokapi-i18n transform walks JSXText nodes, not expression containers\n * with string literals inside them.\n *\n * A developer who writes this usually meant `<p>Hello</p>` (trivially\n * extractable) or meant to interpolate with `t()` but forgot.\n */\nexport const rule: Rule = {\n meta: {\n type: \"problem\",\n docs: {\n description:\n \"flag string literals wrapped in JSX expression containers \u2014 not extractable, probably unintended\",\n recommended: true,\n },\n schema: [],\n fixable: \"code\",\n messages: {\n bareLiteral:\n \"String literal inside a JSX expression container is not extractable. Write it as JSX text (`<p>Hello</p>`) or wrap with `t()` if it must be an expression.\",\n },\n },\n create(context) {\n return {\n JSXExpressionContainer(node: Node) {\n const container = node as unknown as {\n parent: { type: string };\n expression: { type: string; value?: unknown; raw?: string };\n };\n // Only flag inside JSX *children*, not JSX attributes.\n if (container.parent?.type !== \"JSXElement\" && container.parent?.type !== \"JSXFragment\") {\n return;\n }\n const expr = container.expression;\n if (!expr) return;\n if (expr.type !== \"Literal\") return;\n if (typeof expr.value !== \"string\") return;\n if (expr.value.trim() === \"\") return;\n context.report({\n node: node as unknown as Node,\n messageId: \"bareLiteral\",\n fix(fixer) {\n // Strip the quotes, keep the text. JSX text is literal\n // so this is a safe transform as long as it has no\n // newlines or braces (rare in this pattern).\n if (typeof expr.value !== \"string\") return null;\n if (/[\\r\\n{}]/.test(expr.value)) return null;\n return fixer.replaceText(node, expr.value);\n },\n });\n },\n };\n },\n};\n"],
|
|
5
|
+
"mappings": "AAWO,MAAM,OAAa;AAAA,EACxB,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,QAAQ,CAAC;AAAA,IACT,SAAS;AAAA,IACT,UAAU;AAAA,MACR,aACE;AAAA,IACJ;AAAA,EACF;AAAA,EACA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,uBAAuB,MAAY;AACjC,cAAM,YAAY;AAKlB,YAAI,UAAU,QAAQ,SAAS,gBAAgB,UAAU,QAAQ,SAAS,eAAe;AACvF;AAAA,QACF;AACA,cAAM,OAAO,UAAU;AACvB,YAAI,CAAC,KAAM;AACX,YAAI,KAAK,SAAS,UAAW;AAC7B,YAAI,OAAO,KAAK,UAAU,SAAU;AACpC,YAAI,KAAK,MAAM,KAAK,MAAM,GAAI;AAC9B,gBAAQ,OAAO;AAAA,UACb;AAAA,UACA,WAAW;AAAA,UACX,IAAI,OAAO;AAIT,gBAAI,OAAO,KAAK,UAAU,SAAU,QAAO;AAC3C,gBAAI,WAAW,KAAK,KAAK,KAAK,EAAG,QAAO;AACxC,mBAAO,MAAM,YAAY,MAAM,KAAK,KAAK;AAAA,UAC3C;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Rule } from "@oxlint/plugins";
|
|
2
|
+
/**
|
|
3
|
+
* Sibling of `no-concat-in-translatable-attr`. Flags a translatable
|
|
4
|
+
* attribute whose value is a conditional expression where at least
|
|
5
|
+
* one branch is NOT a plain string literal — e.g.
|
|
6
|
+
*
|
|
7
|
+
* <Input placeholder={disabled ? getLabel() : "Type…"} />
|
|
8
|
+
*
|
|
9
|
+
* The neokapi-i18n extractor handles the all-string case
|
|
10
|
+
* (`cond ? "A" : "B"`) by emitting one Block per branch; any other
|
|
11
|
+
* branch shape is un-extractable and silently bypasses translation.
|
|
12
|
+
*
|
|
13
|
+
* The guidance is to lift the strings to be the direct branch values
|
|
14
|
+
* (so the extractor can see them) or use `t()` explicitly.
|
|
15
|
+
*/
|
|
16
|
+
export declare const rule: Rule;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { TRANSLATABLE_ATTRS } from "../shared/translatable-attrs.js";
|
|
2
|
+
import { hasTranslateNoAncestor } from "../shared/translate-no.js";
|
|
3
|
+
const rule = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "problem",
|
|
6
|
+
docs: {
|
|
7
|
+
description: "reject translatable-attribute conditionals whose branches aren't both string literals \u2014 the extractor can't see them",
|
|
8
|
+
recommended: true
|
|
9
|
+
},
|
|
10
|
+
schema: [],
|
|
11
|
+
messages: {
|
|
12
|
+
mixed: 'Attribute `{{attr}}` uses a conditional whose branches aren\'t both plain string literals \u2014 can\'t be extracted. Lift strings to the branches (`cond ? "A" : "B"`) or wrap with `t("key")` explicitly.'
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
JSXAttribute(node) {
|
|
18
|
+
const attr = node;
|
|
19
|
+
const attrName = readAttrName(attr.name);
|
|
20
|
+
if (!attrName || !TRANSLATABLE_ATTRS.has(attrName)) return;
|
|
21
|
+
const value = attr.value;
|
|
22
|
+
if (!value || value.type !== "JSXExpressionContainer") return;
|
|
23
|
+
const expr = value.expression;
|
|
24
|
+
if (!expr || expr.type !== "ConditionalExpression") return;
|
|
25
|
+
const cBranch = isStringLiteral(expr.consequent);
|
|
26
|
+
const aBranch = isStringLiteral(expr.alternate);
|
|
27
|
+
if (cBranch && aBranch) return;
|
|
28
|
+
if (!cBranch && !aBranch) return;
|
|
29
|
+
if (hasTranslateNoAncestor(attr.parent)) return;
|
|
30
|
+
context.report({
|
|
31
|
+
node,
|
|
32
|
+
messageId: "mixed",
|
|
33
|
+
data: { attr: attrName }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function readAttrName(name) {
|
|
40
|
+
if (name.type === "JSXIdentifier") return name.name ?? null;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function isStringLiteral(node) {
|
|
44
|
+
if (!node || typeof node !== "object") return false;
|
|
45
|
+
const n = node;
|
|
46
|
+
return n.type === "Literal" && typeof n.value === "string";
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
rule
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=no-ternary-in-translatable-attr.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/rules/no-ternary-in-translatable-attr.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Rule, Node } from \"@oxlint/plugins\";\nimport { TRANSLATABLE_ATTRS } from \"../shared/translatable-attrs.js\";\nimport { hasTranslateNoAncestor } from \"../shared/translate-no.js\";\n\n/**\n * Sibling of `no-concat-in-translatable-attr`. Flags a translatable\n * attribute whose value is a conditional expression where at least\n * one branch is NOT a plain string literal \u2014 e.g.\n *\n * <Input placeholder={disabled ? getLabel() : \"Type\u2026\"} />\n *\n * The neokapi-i18n extractor handles the all-string case\n * (`cond ? \"A\" : \"B\"`) by emitting one Block per branch; any other\n * branch shape is un-extractable and silently bypasses translation.\n *\n * The guidance is to lift the strings to be the direct branch values\n * (so the extractor can see them) or use `t()` explicitly.\n */\nexport const rule: Rule = {\n meta: {\n type: \"problem\",\n docs: {\n description:\n \"reject translatable-attribute conditionals whose branches aren't both string literals \u2014 the extractor can't see them\",\n recommended: true,\n },\n schema: [],\n messages: {\n mixed:\n 'Attribute `{{attr}}` uses a conditional whose branches aren\\'t both plain string literals \u2014 can\\'t be extracted. Lift strings to the branches (`cond ? \"A\" : \"B\"`) or wrap with `t(\"key\")` explicitly.',\n },\n },\n create(context) {\n return {\n JSXAttribute(node: Node) {\n const attr = node as unknown as {\n name: { type: string; name?: string };\n value: unknown;\n parent?: unknown;\n };\n const attrName = readAttrName(attr.name);\n if (!attrName || !TRANSLATABLE_ATTRS.has(attrName)) return;\n const value = attr.value as\n | {\n type: string;\n expression?: { type: string; consequent?: unknown; alternate?: unknown };\n }\n | null\n | undefined;\n if (!value || value.type !== \"JSXExpressionContainer\") return;\n const expr = value.expression;\n if (!expr || expr.type !== \"ConditionalExpression\") return;\n\n const cBranch = isStringLiteral(expr.consequent);\n const aBranch = isStringLiteral(expr.alternate);\n // All-literal case is handled by the extractor \u2014 no warning.\n if (cBranch && aBranch) return;\n // All-non-literal case is something else entirely (computed\n // values, `t()` calls, etc.) \u2014 also skip; likely intentional.\n if (!cBranch && !aBranch) return;\n\n // Respect W3C `translate=\"no\"` on the element itself or any\n // JSX ancestor \u2014 same semantics as the extractor skip rule.\n if (hasTranslateNoAncestor(attr.parent)) return;\n\n context.report({\n node: node as unknown as Node,\n messageId: \"mixed\",\n data: { attr: attrName },\n });\n },\n };\n },\n};\n\nfunction readAttrName(name: { type: string; name?: string }): string | null {\n if (name.type === \"JSXIdentifier\") return name.name ?? null;\n return null;\n}\n\nfunction isStringLiteral(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n const n = node as { type?: string; value?: unknown };\n return n.type === \"Literal\" && typeof n.value === \"string\";\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAgBhC,MAAM,OAAa;AAAA,EACxB,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,QAAQ,CAAC;AAAA,IACT,UAAU;AAAA,MACR,OACE;AAAA,IACJ;AAAA,EACF;AAAA,EACA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,aAAa,MAAY;AACvB,cAAM,OAAO;AAKb,cAAM,WAAW,aAAa,KAAK,IAAI;AACvC,YAAI,CAAC,YAAY,CAAC,mBAAmB,IAAI,QAAQ,EAAG;AACpD,cAAM,QAAQ,KAAK;AAOnB,YAAI,CAAC,SAAS,MAAM,SAAS,yBAA0B;AACvD,cAAM,OAAO,MAAM;AACnB,YAAI,CAAC,QAAQ,KAAK,SAAS,wBAAyB;AAEpD,cAAM,UAAU,gBAAgB,KAAK,UAAU;AAC/C,cAAM,UAAU,gBAAgB,KAAK,SAAS;AAE9C,YAAI,WAAW,QAAS;AAGxB,YAAI,CAAC,WAAW,CAAC,QAAS;AAI1B,YAAI,uBAAuB,KAAK,MAAM,EAAG;AAEzC,gBAAQ,OAAO;AAAA,UACb;AAAA,UACA,WAAW;AAAA,UACX,MAAM,EAAE,MAAM,SAAS;AAAA,QACzB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,aAAa,MAAsD;AAC1E,MAAI,KAAK,SAAS,gBAAiB,QAAO,KAAK,QAAQ;AACvD,SAAO;AACT;AAEA,SAAS,gBAAgB,MAAwB;AAC/C,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,QAAM,IAAI;AACV,SAAO,EAAE,SAAS,aAAa,OAAO,EAAE,UAAU;AACpD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Rule } from "@oxlint/plugins";
|
|
2
|
+
/**
|
|
3
|
+
* Flags `<X>{cond ? "A" : "B"}</X>` (and template-literal variants)
|
|
4
|
+
* where at least one branch is a plain string literal. neokapi-i18n's
|
|
5
|
+
* extractor treats the entire conditional as an opaque `jsx:var`
|
|
6
|
+
* placeholder — neither branch's string is visible to extraction,
|
|
7
|
+
* so both silently bypass translation.
|
|
8
|
+
*
|
|
9
|
+
* Fix: wrap each literal branch with `t()`. The t-call walker
|
|
10
|
+
* extracts `t("A")` / `t("B")` separately and both become
|
|
11
|
+
* translatable.
|
|
12
|
+
*
|
|
13
|
+
* Ignores:
|
|
14
|
+
* - Ternaries whose branches are neither strings nor templates
|
|
15
|
+
* (computed values, `t()` calls, React elements).
|
|
16
|
+
* - Elements with `translate="no"` on any ancestor.
|
|
17
|
+
*/
|
|
18
|
+
export declare const rule: Rule;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { hasTranslateNoAncestor } from "../shared/translate-no.js";
|
|
2
|
+
const rule = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "problem",
|
|
5
|
+
docs: {
|
|
6
|
+
description: "flag ternary with string-literal branches in JSX children \u2014 extractor treats them as an opaque placeholder; wrap each branch with t()",
|
|
7
|
+
recommended: true
|
|
8
|
+
},
|
|
9
|
+
schema: [],
|
|
10
|
+
messages: {
|
|
11
|
+
literalBranch: 'Ternary branches `{{text}}` render as JSX text \u2014 the extractor treats the whole conditional as an opaque placeholder, so the string never gets translated. Wrap with t() (e.g. `cond ? t("A") : t("B")`).'
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
create(context) {
|
|
15
|
+
return {
|
|
16
|
+
JSXExpressionContainer(node) {
|
|
17
|
+
const container = node;
|
|
18
|
+
const parent = container.parent;
|
|
19
|
+
if (parent?.type !== "JSXElement" && parent?.type !== "JSXFragment") return;
|
|
20
|
+
const expr = container.expression;
|
|
21
|
+
if (!expr || expr.type !== "ConditionalExpression") return;
|
|
22
|
+
const cKind = stringyKind(expr.consequent);
|
|
23
|
+
const aKind = stringyKind(expr.alternate);
|
|
24
|
+
if (!cKind && !aKind) return;
|
|
25
|
+
if (hasTranslateNoAncestor(container.parent)) return;
|
|
26
|
+
const shown = cKind && aKind ? `${summary(expr.consequent)} / ${summary(expr.alternate)}` : cKind ? summary(expr.consequent) : summary(expr.alternate);
|
|
27
|
+
context.report({
|
|
28
|
+
node,
|
|
29
|
+
messageId: "literalBranch",
|
|
30
|
+
data: { text: shown }
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function stringyKind(node) {
|
|
37
|
+
if (!node || typeof node !== "object") return null;
|
|
38
|
+
const n = node;
|
|
39
|
+
if (n.type === "Literal" && typeof n.value === "string") return "literal";
|
|
40
|
+
if (n.type === "TemplateLiteral") {
|
|
41
|
+
const quasis = n.quasis ?? [];
|
|
42
|
+
const hasWord = quasis.some((q) => /[A-Za-z]{2,}/.test(q.value?.cooked ?? q.value?.raw ?? ""));
|
|
43
|
+
return hasWord ? "template" : null;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function summary(node) {
|
|
48
|
+
if (!node || typeof node !== "object") return "\u2026";
|
|
49
|
+
const n = node;
|
|
50
|
+
if (n.type === "Literal" && typeof n.value === "string") {
|
|
51
|
+
const s = n.value;
|
|
52
|
+
return s.length > 24 ? `"${s.slice(0, 24)}\u2026"` : `"${s}"`;
|
|
53
|
+
}
|
|
54
|
+
if (n.type === "TemplateLiteral") return "`\u2026`";
|
|
55
|
+
return "\u2026";
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
rule
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=no-ternary-literals-in-jsx-child.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/rules/no-ternary-literals-in-jsx-child.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Rule, Node } from \"@oxlint/plugins\";\nimport { hasTranslateNoAncestor } from \"../shared/translate-no.js\";\n\n/**\n * Flags `<X>{cond ? \"A\" : \"B\"}</X>` (and template-literal variants)\n * where at least one branch is a plain string literal. neokapi-i18n's\n * extractor treats the entire conditional as an opaque `jsx:var`\n * placeholder \u2014 neither branch's string is visible to extraction,\n * so both silently bypass translation.\n *\n * Fix: wrap each literal branch with `t()`. The t-call walker\n * extracts `t(\"A\")` / `t(\"B\")` separately and both become\n * translatable.\n *\n * Ignores:\n * - Ternaries whose branches are neither strings nor templates\n * (computed values, `t()` calls, React elements).\n * - Elements with `translate=\"no\"` on any ancestor.\n */\nexport const rule: Rule = {\n meta: {\n type: \"problem\",\n docs: {\n description:\n \"flag ternary with string-literal branches in JSX children \u2014 extractor treats them as an opaque placeholder; wrap each branch with t()\",\n recommended: true,\n },\n schema: [],\n messages: {\n literalBranch:\n 'Ternary branches `{{text}}` render as JSX text \u2014 the extractor treats the whole conditional as an opaque placeholder, so the string never gets translated. Wrap with t() (e.g. `cond ? t(\"A\") : t(\"B\")`).',\n },\n },\n create(context) {\n return {\n JSXExpressionContainer(node: Node) {\n const container = node as unknown as {\n parent: unknown;\n expression: { type: string; consequent?: unknown; alternate?: unknown };\n };\n // Only JSX children, not attribute values \u2014 attributes have\n // their own rule (`no-ternary-in-translatable-attr`).\n const parent = container.parent as { type?: string } | undefined;\n if (parent?.type !== \"JSXElement\" && parent?.type !== \"JSXFragment\") return;\n\n const expr = container.expression;\n if (!expr || expr.type !== \"ConditionalExpression\") return;\n\n const cKind = stringyKind(expr.consequent);\n const aKind = stringyKind(expr.alternate);\n // Warn only if at least one branch is a literal string or\n // template literal. All-non-literal (t() calls, elements,\n // computed values) is assumed intentional.\n if (!cKind && !aKind) return;\n\n if (hasTranslateNoAncestor(container.parent)) return;\n\n const shown =\n cKind && aKind\n ? `${summary(expr.consequent)} / ${summary(expr.alternate)}`\n : cKind\n ? summary(expr.consequent)\n : summary(expr.alternate);\n\n context.report({\n node: node as unknown as Node,\n messageId: \"literalBranch\",\n data: { text: shown },\n });\n },\n };\n },\n};\n\nfunction stringyKind(node: unknown): \"literal\" | \"template\" | null {\n if (!node || typeof node !== \"object\") return null;\n const n = node as {\n type?: string;\n value?: unknown;\n quasis?: { value?: { raw?: string; cooked?: string } }[];\n };\n if (n.type === \"Literal\" && typeof n.value === \"string\") return \"literal\";\n if (n.type === \"TemplateLiteral\") {\n // Only flag when the template has translatable-looking text \u2014\n // at least one quasi with alphabetic characters. Pure formatting\n // like `${pct}%` or `v${version}` is code-level, not UI copy,\n // and shouldn't be flagged.\n const quasis = n.quasis ?? [];\n const hasWord = quasis.some((q) => /[A-Za-z]{2,}/.test(q.value?.cooked ?? q.value?.raw ?? \"\"));\n return hasWord ? \"template\" : null;\n }\n return null;\n}\n\nfunction summary(node: unknown): string {\n if (!node || typeof node !== \"object\") return \"\u2026\";\n const n = node as { type?: string; value?: unknown; quasis?: { value?: { raw?: string } }[] };\n if (n.type === \"Literal\" && typeof n.value === \"string\") {\n const s = n.value as string;\n return s.length > 24 ? `\"${s.slice(0, 24)}\u2026\"` : `\"${s}\"`;\n }\n if (n.type === \"TemplateLiteral\") return \"`\u2026`\";\n return \"\u2026\";\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,8BAA8B;AAkBhC,MAAM,OAAa;AAAA,EACxB,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,QAAQ,CAAC;AAAA,IACT,UAAU;AAAA,MACR,eACE;AAAA,IACJ;AAAA,EACF;AAAA,EACA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,uBAAuB,MAAY;AACjC,cAAM,YAAY;AAMlB,cAAM,SAAS,UAAU;AACzB,YAAI,QAAQ,SAAS,gBAAgB,QAAQ,SAAS,cAAe;AAErE,cAAM,OAAO,UAAU;AACvB,YAAI,CAAC,QAAQ,KAAK,SAAS,wBAAyB;AAEpD,cAAM,QAAQ,YAAY,KAAK,UAAU;AACzC,cAAM,QAAQ,YAAY,KAAK,SAAS;AAIxC,YAAI,CAAC,SAAS,CAAC,MAAO;AAEtB,YAAI,uBAAuB,UAAU,MAAM,EAAG;AAE9C,cAAM,QACJ,SAAS,QACL,GAAG,QAAQ,KAAK,UAAU,CAAC,MAAM,QAAQ,KAAK,SAAS,CAAC,KACxD,QACE,QAAQ,KAAK,UAAU,IACvB,QAAQ,KAAK,SAAS;AAE9B,gBAAQ,OAAO;AAAA,UACb;AAAA,UACA,WAAW;AAAA,UACX,MAAM,EAAE,MAAM,MAAM;AAAA,QACtB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,YAAY,MAA8C;AACjE,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,QAAM,IAAI;AAKV,MAAI,EAAE,SAAS,aAAa,OAAO,EAAE,UAAU,SAAU,QAAO;AAChE,MAAI,EAAE,SAAS,mBAAmB;AAKhC,UAAM,SAAS,EAAE,UAAU,CAAC;AAC5B,UAAM,UAAU,OAAO,KAAK,CAAC,MAAM,eAAe,KAAK,EAAE,OAAO,UAAU,EAAE,OAAO,OAAO,EAAE,CAAC;AAC7F,WAAO,UAAU,aAAa;AAAA,EAChC;AACA,SAAO;AACT;AAEA,SAAS,QAAQ,MAAuB;AACtC,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,QAAM,IAAI;AACV,MAAI,EAAE,SAAS,aAAa,OAAO,EAAE,UAAU,UAAU;AACvD,UAAM,IAAI,EAAE;AACZ,WAAO,EAAE,SAAS,KAAK,IAAI,EAAE,MAAM,GAAG,EAAE,CAAC,YAAO,IAAI,CAAC;AAAA,EACvD;AACA,MAAI,EAAE,SAAS,kBAAmB,QAAO;AACzC,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Rule } from "@oxlint/plugins";
|
|
2
|
+
/**
|
|
3
|
+
* Complement of `prefer-t-for-label-props`: that rule flags the
|
|
4
|
+
* *declaration* side (`const X = [{ label: 'Foo' }]`); this one flags
|
|
5
|
+
* the *render* side (`<div>{meta.label}</div>`). Either pattern alone
|
|
6
|
+
* silently bypasses translation — the extractor doesn't walk data
|
|
7
|
+
* arrays, and it treats a JSX expression child as an opaque
|
|
8
|
+
* placeholder.
|
|
9
|
+
*
|
|
10
|
+
* Flags: a JSX expression container whose expression is a member
|
|
11
|
+
* access with a label-like property (`label`, `title`, `description`,
|
|
12
|
+
* …). Covers single-dot (`meta.label`) and two-deep
|
|
13
|
+
* (`items[i].title`). Members whose property name isn't in
|
|
14
|
+
* `LIKELY_LABEL_KEYS` are ignored — keeping FP low.
|
|
15
|
+
*
|
|
16
|
+
* Fix guidance in the message: wrap the source string with `t()` in
|
|
17
|
+
* the data array, or use an explicit `t(key)` lookup here.
|
|
18
|
+
*/
|
|
19
|
+
export declare const rule: Rule;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { LIKELY_LABEL_KEYS } from "../shared/translatable-attrs.js";
|
|
2
|
+
import { hasTranslateNoAncestor } from "../shared/translate-no.js";
|
|
3
|
+
const rule = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "problem",
|
|
6
|
+
docs: {
|
|
7
|
+
description: "flag `{obj.label}` / `{obj.title}` rendered as JSX text \u2014 the dereferenced property is almost certainly user-visible copy that silently bypasses translation",
|
|
8
|
+
recommended: true
|
|
9
|
+
},
|
|
10
|
+
schema: [
|
|
11
|
+
{
|
|
12
|
+
type: "object",
|
|
13
|
+
additionalProperties: false,
|
|
14
|
+
properties: {
|
|
15
|
+
keys: { type: "array", items: { type: "string" } }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
messages: {
|
|
20
|
+
dynLabel: '`{{expr}}` is rendered as JSX text \u2014 the `.{{key}}` property name suggests a user-visible string that won\'t be translated. Wrap the source data with `t()` or use an explicit `t("key")` here.'
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
const opts = context.options[0] ?? {};
|
|
25
|
+
const keys = opts.keys ? new Set(opts.keys) : LIKELY_LABEL_KEYS;
|
|
26
|
+
return {
|
|
27
|
+
JSXExpressionContainer(node) {
|
|
28
|
+
const container = node;
|
|
29
|
+
const parent = container.parent;
|
|
30
|
+
if (parent?.type !== "JSXElement" && parent?.type !== "JSXFragment") return;
|
|
31
|
+
const expr = container.expression;
|
|
32
|
+
if (!expr) return;
|
|
33
|
+
const match = labelLikeMember(expr, keys);
|
|
34
|
+
if (!match) return;
|
|
35
|
+
if (hasTranslateNoAncestor(container.parent)) return;
|
|
36
|
+
context.report({
|
|
37
|
+
node,
|
|
38
|
+
messageId: "dynLabel",
|
|
39
|
+
data: { expr: match.display, key: match.key }
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function labelLikeMember(expr, keys) {
|
|
46
|
+
if (!expr || typeof expr !== "object") return null;
|
|
47
|
+
const node = expr;
|
|
48
|
+
if (node.type !== "MemberExpression") return null;
|
|
49
|
+
if (node.computed) return null;
|
|
50
|
+
const propName = readPropName(node.property);
|
|
51
|
+
if (!propName || !keys.has(propName)) return null;
|
|
52
|
+
return { key: propName, display: renderAccess(node) };
|
|
53
|
+
}
|
|
54
|
+
function readPropName(prop) {
|
|
55
|
+
if (!prop) return null;
|
|
56
|
+
if (prop.type === "Identifier") return prop.name ?? null;
|
|
57
|
+
if (prop.type === "Literal" && typeof prop.value === "string") return prop.value;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
function renderAccess(node) {
|
|
61
|
+
const propName = node.property?.name ?? "?";
|
|
62
|
+
const obj = node.object;
|
|
63
|
+
if (obj?.type === "Identifier" && obj.name) return `${obj.name}.${propName}`;
|
|
64
|
+
if (obj?.type === "MemberExpression" && obj.property?.name) {
|
|
65
|
+
return `${obj.property.name}.${propName}`;
|
|
66
|
+
}
|
|
67
|
+
return propName;
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
rule
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=prefer-t-for-label-expr.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/rules/prefer-t-for-label-expr.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Rule, Node } from \"@oxlint/plugins\";\nimport { LIKELY_LABEL_KEYS } from \"../shared/translatable-attrs.js\";\nimport { hasTranslateNoAncestor } from \"../shared/translate-no.js\";\n\n/**\n * Complement of `prefer-t-for-label-props`: that rule flags the\n * *declaration* side (`const X = [{ label: 'Foo' }]`); this one flags\n * the *render* side (`<div>{meta.label}</div>`). Either pattern alone\n * silently bypasses translation \u2014 the extractor doesn't walk data\n * arrays, and it treats a JSX expression child as an opaque\n * placeholder.\n *\n * Flags: a JSX expression container whose expression is a member\n * access with a label-like property (`label`, `title`, `description`,\n * \u2026). Covers single-dot (`meta.label`) and two-deep\n * (`items[i].title`). Members whose property name isn't in\n * `LIKELY_LABEL_KEYS` are ignored \u2014 keeping FP low.\n *\n * Fix guidance in the message: wrap the source string with `t()` in\n * the data array, or use an explicit `t(key)` lookup here.\n */\nexport const rule: Rule = {\n meta: {\n type: \"problem\",\n docs: {\n description:\n \"flag `{obj.label}` / `{obj.title}` rendered as JSX text \u2014 the dereferenced property is almost certainly user-visible copy that silently bypasses translation\",\n recommended: true,\n },\n schema: [\n {\n type: \"object\",\n additionalProperties: false,\n properties: {\n keys: { type: \"array\", items: { type: \"string\" } },\n },\n },\n ],\n messages: {\n dynLabel:\n '`{{expr}}` is rendered as JSX text \u2014 the `.{{key}}` property name suggests a user-visible string that won\\'t be translated. Wrap the source data with `t()` or use an explicit `t(\"key\")` here.',\n },\n },\n create(context) {\n const opts = (context.options[0] ?? {}) as { keys?: string[] };\n const keys = opts.keys ? new Set(opts.keys) : LIKELY_LABEL_KEYS;\n return {\n JSXExpressionContainer(node: Node) {\n const container = node as unknown as {\n parent: unknown;\n expression: { type: string };\n };\n // Only JSX children, not attribute values \u2014 attributes have\n // their own extraction path and separate lint rules.\n const parent = container.parent as { type?: string } | undefined;\n if (parent?.type !== \"JSXElement\" && parent?.type !== \"JSXFragment\") return;\n\n const expr = container.expression;\n if (!expr) return;\n\n const match = labelLikeMember(expr, keys);\n if (!match) return;\n\n // Respect the W3C `translate=\"no\"` hint on any JSX ancestor \u2014\n // developers use it to mark dynamic / code-like / non-copy\n // content; surfacing a \"needs translation\" warning on it is\n // noise.\n if (hasTranslateNoAncestor(container.parent)) return;\n\n context.report({\n node: node as unknown as Node,\n messageId: \"dynLabel\",\n data: { expr: match.display, key: match.key },\n });\n },\n };\n },\n};\n\ninterface LabelMatch {\n key: string;\n display: string;\n}\n\nfunction labelLikeMember(expr: unknown, keys: ReadonlySet<string>): LabelMatch | null {\n if (!expr || typeof expr !== \"object\") return null;\n const node = expr as {\n type?: string;\n property?: { type?: string; name?: string; value?: unknown };\n object?: { type?: string; name?: string; property?: { type?: string; name?: string } };\n computed?: boolean;\n };\n if (node.type !== \"MemberExpression\") return null;\n if (node.computed) return null;\n const propName = readPropName(node.property);\n if (!propName || !keys.has(propName)) return null;\n return { key: propName, display: renderAccess(node) };\n}\n\nfunction readPropName(prop?: { type?: string; name?: string; value?: unknown }): string | null {\n if (!prop) return null;\n if (prop.type === \"Identifier\") return prop.name ?? null;\n if (prop.type === \"Literal\" && typeof prop.value === \"string\") return prop.value;\n return null;\n}\n\nfunction renderAccess(node: {\n property?: { name?: string };\n object?: { type?: string; name?: string; property?: { name?: string } };\n}): string {\n const propName = node.property?.name ?? \"?\";\n const obj = node.object;\n if (obj?.type === \"Identifier\" && obj.name) return `${obj.name}.${propName}`;\n if (obj?.type === \"MemberExpression\" && obj.property?.name) {\n return `${obj.property.name}.${propName}`;\n }\n return propName;\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,yBAAyB;AAClC,SAAS,8BAA8B;AAmBhC,MAAM,OAAa;AAAA,EACxB,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,QAAQ;AAAA,MACN;AAAA,QACE,MAAM;AAAA,QACN,sBAAsB;AAAA,QACtB,YAAY;AAAA,UACV,MAAM,EAAE,MAAM,SAAS,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,QACnD;AAAA,MACF;AAAA,IACF;AAAA,IACA,UAAU;AAAA,MACR,UACE;AAAA,IACJ;AAAA,EACF;AAAA,EACA,OAAO,SAAS;AACd,UAAM,OAAQ,QAAQ,QAAQ,CAAC,KAAK,CAAC;AACrC,UAAM,OAAO,KAAK,OAAO,IAAI,IAAI,KAAK,IAAI,IAAI;AAC9C,WAAO;AAAA,MACL,uBAAuB,MAAY;AACjC,cAAM,YAAY;AAMlB,cAAM,SAAS,UAAU;AACzB,YAAI,QAAQ,SAAS,gBAAgB,QAAQ,SAAS,cAAe;AAErE,cAAM,OAAO,UAAU;AACvB,YAAI,CAAC,KAAM;AAEX,cAAM,QAAQ,gBAAgB,MAAM,IAAI;AACxC,YAAI,CAAC,MAAO;AAMZ,YAAI,uBAAuB,UAAU,MAAM,EAAG;AAE9C,gBAAQ,OAAO;AAAA,UACb;AAAA,UACA,WAAW;AAAA,UACX,MAAM,EAAE,MAAM,MAAM,SAAS,KAAK,MAAM,IAAI;AAAA,QAC9C,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAOA,SAAS,gBAAgB,MAAe,MAA8C;AACpF,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,QAAM,OAAO;AAMb,MAAI,KAAK,SAAS,mBAAoB,QAAO;AAC7C,MAAI,KAAK,SAAU,QAAO;AAC1B,QAAM,WAAW,aAAa,KAAK,QAAQ;AAC3C,MAAI,CAAC,YAAY,CAAC,KAAK,IAAI,QAAQ,EAAG,QAAO;AAC7C,SAAO,EAAE,KAAK,UAAU,SAAS,aAAa,IAAI,EAAE;AACtD;AAEA,SAAS,aAAa,MAAyE;AAC7F,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,SAAS,aAAc,QAAO,KAAK,QAAQ;AACpD,MAAI,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU,SAAU,QAAO,KAAK;AAC3E,SAAO;AACT;AAEA,SAAS,aAAa,MAGX;AACT,QAAM,WAAW,KAAK,UAAU,QAAQ;AACxC,QAAM,MAAM,KAAK;AACjB,MAAI,KAAK,SAAS,gBAAgB,IAAI,KAAM,QAAO,GAAG,IAAI,IAAI,IAAI,QAAQ;AAC1E,MAAI,KAAK,SAAS,sBAAsB,IAAI,UAAU,MAAM;AAC1D,WAAO,GAAG,IAAI,SAAS,IAAI,IAAI,QAAQ;AAAA,EACzC;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Rule } from "@oxlint/plugins";
|
|
2
|
+
/**
|
|
3
|
+
* Off by default — highest false-positive risk in the set, so gated
|
|
4
|
+
* behind the `recommended-strict` preset.
|
|
5
|
+
*
|
|
6
|
+
* Flags the common pattern where a developer stores a user-facing
|
|
7
|
+
* string in an object literal that gets fed to JSX as an expression:
|
|
8
|
+
*
|
|
9
|
+
* const THEMES = [
|
|
10
|
+
* { value: 'system', label: 'System' }, // ← flagged
|
|
11
|
+
* { value: 'light', label: 'Light' },
|
|
12
|
+
* ];
|
|
13
|
+
* return THEMES.map(({ value, label }) => <button>{label}</button>);
|
|
14
|
+
*
|
|
15
|
+
* `<button>{label}</button>` is an expression child and therefore not
|
|
16
|
+
* extractable, and the string literal `'System'` lives in a data array
|
|
17
|
+
* the extractor doesn't walk. Wrapping with `t('System')` makes the
|
|
18
|
+
* literal visible to extraction.
|
|
19
|
+
*
|
|
20
|
+
* To reduce FPs we only flag string literals whose property name is
|
|
21
|
+
* one of the canonical UI-label keys (`label`, `title`, `description`,
|
|
22
|
+
* etc.). A developer using those key names is almost always talking
|
|
23
|
+
* about user-facing copy.
|
|
24
|
+
*/
|
|
25
|
+
export declare const rule: Rule;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { LIKELY_LABEL_KEYS } from "../shared/translatable-attrs.js";
|
|
2
|
+
const rule = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "suggestion",
|
|
5
|
+
docs: {
|
|
6
|
+
description: "suggest wrapping user-facing strings in data arrays with `t()` so the extractor can see them",
|
|
7
|
+
recommended: false
|
|
8
|
+
},
|
|
9
|
+
schema: [
|
|
10
|
+
{
|
|
11
|
+
type: "object",
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
properties: {
|
|
14
|
+
keys: { type: "array", items: { type: "string" } }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
messages: {
|
|
19
|
+
useT: "String literal stored in `{{key}}` is not visible to the extractor when rendered as a JSX expression. Wrap with `t(...)` or store the translated value elsewhere."
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
const opts = context.options[0] ?? {};
|
|
24
|
+
const keys = opts.keys ? new Set(opts.keys) : LIKELY_LABEL_KEYS;
|
|
25
|
+
return {
|
|
26
|
+
Property(node) {
|
|
27
|
+
const prop = node;
|
|
28
|
+
if (prop.computed || prop.shorthand) return;
|
|
29
|
+
const name = readKey(prop.key);
|
|
30
|
+
if (!name || !keys.has(name)) return;
|
|
31
|
+
if (prop.value.type !== "Literal" || typeof prop.value.value !== "string") return;
|
|
32
|
+
if (prop.value.value.trim() === "") return;
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
messageId: "useT",
|
|
36
|
+
data: { key: name }
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
function readKey(key) {
|
|
43
|
+
if (key.type === "Identifier") return key.name ?? null;
|
|
44
|
+
if (key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
rule
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=prefer-t-for-label-props.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/rules/prefer-t-for-label-props.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Rule, Node } from \"@oxlint/plugins\";\nimport { LIKELY_LABEL_KEYS } from \"../shared/translatable-attrs.js\";\n\n/**\n * Off by default \u2014 highest false-positive risk in the set, so gated\n * behind the `recommended-strict` preset.\n *\n * Flags the common pattern where a developer stores a user-facing\n * string in an object literal that gets fed to JSX as an expression:\n *\n * const THEMES = [\n * { value: 'system', label: 'System' }, // \u2190 flagged\n * { value: 'light', label: 'Light' },\n * ];\n * return THEMES.map(({ value, label }) => <button>{label}</button>);\n *\n * `<button>{label}</button>` is an expression child and therefore not\n * extractable, and the string literal `'System'` lives in a data array\n * the extractor doesn't walk. Wrapping with `t('System')` makes the\n * literal visible to extraction.\n *\n * To reduce FPs we only flag string literals whose property name is\n * one of the canonical UI-label keys (`label`, `title`, `description`,\n * etc.). A developer using those key names is almost always talking\n * about user-facing copy.\n */\nexport const rule: Rule = {\n meta: {\n type: \"suggestion\",\n docs: {\n description:\n \"suggest wrapping user-facing strings in data arrays with `t()` so the extractor can see them\",\n recommended: false,\n },\n schema: [\n {\n type: \"object\",\n additionalProperties: false,\n properties: {\n keys: { type: \"array\", items: { type: \"string\" } },\n },\n },\n ],\n messages: {\n useT: \"String literal stored in `{{key}}` is not visible to the extractor when rendered as a JSX expression. Wrap with `t(...)` or store the translated value elsewhere.\",\n },\n },\n create(context) {\n const opts = (context.options[0] ?? {}) as { keys?: string[] };\n const keys = opts.keys ? new Set(opts.keys) : LIKELY_LABEL_KEYS;\n return {\n Property(node: Node) {\n const prop = node as unknown as {\n key: { type: string; name?: string; value?: unknown };\n value: { type: string; value?: unknown };\n shorthand?: boolean;\n computed?: boolean;\n };\n if (prop.computed || prop.shorthand) return;\n const name = readKey(prop.key);\n if (!name || !keys.has(name)) return;\n if (prop.value.type !== \"Literal\" || typeof prop.value.value !== \"string\") return;\n if ((prop.value.value as string).trim() === \"\") return;\n context.report({\n node: node as unknown as Node,\n messageId: \"useT\",\n data: { key: name },\n });\n },\n };\n },\n};\n\nfunction readKey(key: { type: string; name?: string; value?: unknown }): string | null {\n if (key.type === \"Identifier\") return key.name ?? null;\n if (key.type === \"Literal\" && typeof key.value === \"string\") return key.value;\n return null;\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,yBAAyB;AAyB3B,MAAM,OAAa;AAAA,EACxB,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,QAAQ;AAAA,MACN;AAAA,QACE,MAAM;AAAA,QACN,sBAAsB;AAAA,QACtB,YAAY;AAAA,UACV,MAAM,EAAE,MAAM,SAAS,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,QACnD;AAAA,MACF;AAAA,IACF;AAAA,IACA,UAAU;AAAA,MACR,MAAM;AAAA,IACR;AAAA,EACF;AAAA,EACA,OAAO,SAAS;AACd,UAAM,OAAQ,QAAQ,QAAQ,CAAC,KAAK,CAAC;AACrC,UAAM,OAAO,KAAK,OAAO,IAAI,IAAI,KAAK,IAAI,IAAI;AAC9C,WAAO;AAAA,MACL,SAAS,MAAY;AACnB,cAAM,OAAO;AAMb,YAAI,KAAK,YAAY,KAAK,UAAW;AACrC,cAAM,OAAO,QAAQ,KAAK,GAAG;AAC7B,YAAI,CAAC,QAAQ,CAAC,KAAK,IAAI,IAAI,EAAG;AAC9B,YAAI,KAAK,MAAM,SAAS,aAAa,OAAO,KAAK,MAAM,UAAU,SAAU;AAC3E,YAAK,KAAK,MAAM,MAAiB,KAAK,MAAM,GAAI;AAChD,gBAAQ,OAAO;AAAA,UACb;AAAA,UACA,WAAW;AAAA,UACX,MAAM,EAAE,KAAK,KAAK;AAAA,QACpB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,QAAQ,KAAsE;AACrF,MAAI,IAAI,SAAS,aAAc,QAAO,IAAI,QAAQ;AAClD,MAAI,IAAI,SAAS,aAAa,OAAO,IAAI,UAAU,SAAU,QAAO,IAAI;AACxE,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Rule } from "@oxlint/plugins";
|
|
2
|
+
/**
|
|
3
|
+
* Flags `t(variable)` / `t(someExpr)` — the first argument to `t()`
|
|
4
|
+
* must be a string literal so the extractor can see it at build time.
|
|
5
|
+
*
|
|
6
|
+
* Allows template literals **only** if they have no expressions
|
|
7
|
+
* (`t(\`Hello world\`)` is fine; `t(\`Hello ${name}\`)` is not — that
|
|
8
|
+
* case is caught by `t-no-concat`).
|
|
9
|
+
*/
|
|
10
|
+
export declare const rule: Rule;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { collectTLocalNames } from "../shared/t-import.js";
|
|
2
|
+
const rule = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "problem",
|
|
5
|
+
docs: {
|
|
6
|
+
description: "require `t()` first argument to be a string literal so neokapi-i18n can extract it",
|
|
7
|
+
recommended: true
|
|
8
|
+
},
|
|
9
|
+
schema: [],
|
|
10
|
+
messages: {
|
|
11
|
+
notLiteral: "`t()` first argument must be a string literal \u2014 neokapi-i18n reads it statically at build time. Use a literal, or compute the value elsewhere.",
|
|
12
|
+
emptyString: '`t("")` \u2014 empty string has nothing to translate. Remove the call or pass real source text.'
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
const tNames = collectTLocalNames(context);
|
|
17
|
+
if (tNames.size === 0) return {};
|
|
18
|
+
return {
|
|
19
|
+
CallExpression(node) {
|
|
20
|
+
if (node.callee.type !== "Identifier") return;
|
|
21
|
+
if (!tNames.has(node.callee.name)) return;
|
|
22
|
+
const first = node.arguments[0];
|
|
23
|
+
if (!first) return;
|
|
24
|
+
if (first.type === "Literal" && typeof first.value === "string") {
|
|
25
|
+
if (first.value === "") {
|
|
26
|
+
context.report({ node: first, messageId: "emptyString" });
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (first.type === "TemplateLiteral" && first.expressions.length === 0) {
|
|
31
|
+
if (first.quasis[0]?.value.cooked === "") {
|
|
32
|
+
context.report({ node: first, messageId: "emptyString" });
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (first.type === "BinaryExpression" && first.operator === "+") return;
|
|
37
|
+
if (first.type === "TemplateLiteral" && first.expressions.length > 0) return;
|
|
38
|
+
context.report({ node: first, messageId: "notLiteral" });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
export {
|
|
44
|
+
rule
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=t-literal-first-arg.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/rules/t-literal-first-arg.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Rule } from \"@oxlint/plugins\";\nimport { collectTLocalNames } from \"../shared/t-import.js\";\n\n/**\n * Flags `t(variable)` / `t(someExpr)` \u2014 the first argument to `t()`\n * must be a string literal so the extractor can see it at build time.\n *\n * Allows template literals **only** if they have no expressions\n * (`t(\\`Hello world\\`)` is fine; `t(\\`Hello ${name}\\`)` is not \u2014 that\n * case is caught by `t-no-concat`).\n */\nexport const rule: Rule = {\n meta: {\n type: \"problem\",\n docs: {\n description:\n \"require `t()` first argument to be a string literal so neokapi-i18n can extract it\",\n recommended: true,\n },\n schema: [],\n messages: {\n notLiteral:\n \"`t()` first argument must be a string literal \u2014 neokapi-i18n reads it statically at build time. Use a literal, or compute the value elsewhere.\",\n emptyString:\n '`t(\"\")` \u2014 empty string has nothing to translate. Remove the call or pass real source text.',\n },\n },\n create(context) {\n const tNames = collectTLocalNames(context);\n if (tNames.size === 0) return {};\n return {\n CallExpression(node) {\n if (node.callee.type !== \"Identifier\") return;\n if (!tNames.has(node.callee.name)) return;\n const first = node.arguments[0];\n if (!first) return;\n if (first.type === \"Literal\" && typeof first.value === \"string\") {\n if (first.value === \"\") {\n context.report({ node: first, messageId: \"emptyString\" });\n }\n return;\n }\n if (first.type === \"TemplateLiteral\" && first.expressions.length === 0) {\n if (first.quasis[0]?.value.cooked === \"\") {\n context.report({ node: first, messageId: \"emptyString\" });\n }\n return;\n }\n // t-no-concat owns these cases \u2014 avoid duplicate diagnostics.\n if (first.type === \"BinaryExpression\" && first.operator === \"+\") return;\n if (first.type === \"TemplateLiteral\" && first.expressions.length > 0) return;\n context.report({ node: first, messageId: \"notLiteral\" });\n },\n };\n },\n};\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAA0B;AAU5B,MAAM,OAAa;AAAA,EACxB,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,QAAQ,CAAC;AAAA,IACT,UAAU;AAAA,MACR,YACE;AAAA,MACF,aACE;AAAA,IACJ;AAAA,EACF;AAAA,EACA,OAAO,SAAS;AACd,UAAM,SAAS,mBAAmB,OAAO;AACzC,QAAI,OAAO,SAAS,EAAG,QAAO,CAAC;AAC/B,WAAO;AAAA,MACL,eAAe,MAAM;AACnB,YAAI,KAAK,OAAO,SAAS,aAAc;AACvC,YAAI,CAAC,OAAO,IAAI,KAAK,OAAO,IAAI,EAAG;AACnC,cAAM,QAAQ,KAAK,UAAU,CAAC;AAC9B,YAAI,CAAC,MAAO;AACZ,YAAI,MAAM,SAAS,aAAa,OAAO,MAAM,UAAU,UAAU;AAC/D,cAAI,MAAM,UAAU,IAAI;AACtB,oBAAQ,OAAO,EAAE,MAAM,OAAO,WAAW,cAAc,CAAC;AAAA,UAC1D;AACA;AAAA,QACF;AACA,YAAI,MAAM,SAAS,qBAAqB,MAAM,YAAY,WAAW,GAAG;AACtE,cAAI,MAAM,OAAO,CAAC,GAAG,MAAM,WAAW,IAAI;AACxC,oBAAQ,OAAO,EAAE,MAAM,OAAO,WAAW,cAAc,CAAC;AAAA,UAC1D;AACA;AAAA,QACF;AAEA,YAAI,MAAM,SAAS,sBAAsB,MAAM,aAAa,IAAK;AACjE,YAAI,MAAM,SAAS,qBAAqB,MAAM,YAAY,SAAS,EAAG;AACtE,gBAAQ,OAAO,EAAE,MAAM,OAAO,WAAW,aAAa,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Rule } from "@oxlint/plugins";
|
|
2
|
+
/**
|
|
3
|
+
* Flags `t('Hello ' + name)` and `` t(`Hello ${name}`) `` — neither
|
|
4
|
+
* extracts because the runtime value isn't visible at build time.
|
|
5
|
+
*
|
|
6
|
+
* Paired with `t-literal-first-arg`: that rule fires for bare
|
|
7
|
+
* expressions, this one fires for concatenations that *look* like
|
|
8
|
+
* strings but aren't a single literal.
|
|
9
|
+
*/
|
|
10
|
+
export declare const rule: Rule;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { collectTLocalNames } from "../shared/t-import.js";
|
|
2
|
+
const rule = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "problem",
|
|
5
|
+
docs: {
|
|
6
|
+
description: "reject string concatenation and template expressions as the first argument of `t()`",
|
|
7
|
+
recommended: true
|
|
8
|
+
},
|
|
9
|
+
schema: [],
|
|
10
|
+
messages: {
|
|
11
|
+
concat: '`t()` cannot translate concatenated strings \u2014 the extractor only sees a single literal. Use a placeholder: `t("Hello {name}")` and bind values at render.',
|
|
12
|
+
template: '`t()` cannot translate template literals with interpolations. Use a placeholder: `t("Hello {name}")` and bind values at render.'
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
const tNames = collectTLocalNames(context);
|
|
17
|
+
if (tNames.size === 0) return {};
|
|
18
|
+
return {
|
|
19
|
+
CallExpression(node) {
|
|
20
|
+
if (node.callee.type !== "Identifier") return;
|
|
21
|
+
if (!tNames.has(node.callee.name)) return;
|
|
22
|
+
const first = node.arguments[0];
|
|
23
|
+
if (!first) return;
|
|
24
|
+
if (first.type === "BinaryExpression" && first.operator === "+" && looksStringLike(first)) {
|
|
25
|
+
context.report({ node: first, messageId: "concat" });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (first.type === "TemplateLiteral" && first.expressions.length > 0) {
|
|
29
|
+
context.report({ node: first, messageId: "template" });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function looksStringLike(node) {
|
|
36
|
+
return isStringish(node.left) || isStringish(node.right);
|
|
37
|
+
}
|
|
38
|
+
function isStringish(node) {
|
|
39
|
+
if (node.type === "Literal" && typeof node.value === "string") return true;
|
|
40
|
+
if (node.type === "TemplateLiteral") return true;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
rule
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=t-no-concat.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/rules/t-no-concat.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Rule } from \"@oxlint/plugins\";\nimport { collectTLocalNames } from \"../shared/t-import.js\";\n\n/**\n * Flags `t('Hello ' + name)` and `` t(`Hello ${name}`) `` \u2014 neither\n * extracts because the runtime value isn't visible at build time.\n *\n * Paired with `t-literal-first-arg`: that rule fires for bare\n * expressions, this one fires for concatenations that *look* like\n * strings but aren't a single literal.\n */\nexport const rule: Rule = {\n meta: {\n type: \"problem\",\n docs: {\n description:\n \"reject string concatenation and template expressions as the first argument of `t()`\",\n recommended: true,\n },\n schema: [],\n messages: {\n concat:\n '`t()` cannot translate concatenated strings \u2014 the extractor only sees a single literal. Use a placeholder: `t(\"Hello {name}\")` and bind values at render.',\n template:\n '`t()` cannot translate template literals with interpolations. Use a placeholder: `t(\"Hello {name}\")` and bind values at render.',\n },\n },\n create(context) {\n const tNames = collectTLocalNames(context);\n if (tNames.size === 0) return {};\n return {\n CallExpression(node) {\n if (node.callee.type !== \"Identifier\") return;\n if (!tNames.has(node.callee.name)) return;\n const first = node.arguments[0];\n if (!first) return;\n if (first.type === \"BinaryExpression\" && first.operator === \"+\" && looksStringLike(first)) {\n context.report({ node: first, messageId: \"concat\" });\n return;\n }\n if (first.type === \"TemplateLiteral\" && first.expressions.length > 0) {\n context.report({ node: first, messageId: \"template\" });\n }\n },\n };\n },\n};\n\n/**\n * Heuristic: either side is a string-producing node. Keeps the rule\n * specific to the \"developer was trying to build a translatable\n * message\" case and avoids flagging `t(1 + 2)` which `t-literal-first-arg`\n * already catches.\n */\nfunction looksStringLike(node: {\n type: string;\n left: { type: string; value?: unknown };\n right: { type: string; value?: unknown };\n}): boolean {\n return isStringish(node.left) || isStringish(node.right);\n}\n\nfunction isStringish(node: { type: string; value?: unknown }): boolean {\n if (node.type === \"Literal\" && typeof node.value === \"string\") return true;\n if (node.type === \"TemplateLiteral\") return true;\n return false;\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAA0B;AAU5B,MAAM,OAAa;AAAA,EACxB,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,QAAQ,CAAC;AAAA,IACT,UAAU;AAAA,MACR,QACE;AAAA,MACF,UACE;AAAA,IACJ;AAAA,EACF;AAAA,EACA,OAAO,SAAS;AACd,UAAM,SAAS,mBAAmB,OAAO;AACzC,QAAI,OAAO,SAAS,EAAG,QAAO,CAAC;AAC/B,WAAO;AAAA,MACL,eAAe,MAAM;AACnB,YAAI,KAAK,OAAO,SAAS,aAAc;AACvC,YAAI,CAAC,OAAO,IAAI,KAAK,OAAO,IAAI,EAAG;AACnC,cAAM,QAAQ,KAAK,UAAU,CAAC;AAC9B,YAAI,CAAC,MAAO;AACZ,YAAI,MAAM,SAAS,sBAAsB,MAAM,aAAa,OAAO,gBAAgB,KAAK,GAAG;AACzF,kBAAQ,OAAO,EAAE,MAAM,OAAO,WAAW,SAAS,CAAC;AACnD;AAAA,QACF;AACA,YAAI,MAAM,SAAS,qBAAqB,MAAM,YAAY,SAAS,GAAG;AACpE,kBAAQ,OAAO,EAAE,MAAM,OAAO,WAAW,WAAW,CAAC;AAAA,QACvD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAQA,SAAS,gBAAgB,MAIb;AACV,SAAO,YAAY,KAAK,IAAI,KAAK,YAAY,KAAK,KAAK;AACzD;AAEA,SAAS,YAAY,MAAkD;AACrE,MAAI,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU,SAAU,QAAO;AACtE,MAAI,KAAK,SAAS,kBAAmB,QAAO;AAC5C,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Context } from "@oxlint/plugins";
|
|
2
|
+
/**
|
|
3
|
+
* Walks an ESTree program and returns the set of local identifier
|
|
4
|
+
* names bound to the `t` function from `@neokapi/i18n-react/runtime`.
|
|
5
|
+
*
|
|
6
|
+
* Handles:
|
|
7
|
+
* import { t } from '@neokapi/i18n-react/runtime'
|
|
8
|
+
* import { t as translate } from '@neokapi/i18n-react/runtime'
|
|
9
|
+
*
|
|
10
|
+
* Returns a Set because a file could in principle import `t` twice
|
|
11
|
+
* under different aliases, and we want to catch all of them.
|
|
12
|
+
*/
|
|
13
|
+
export declare function collectTLocalNames(context: Context): Set<string>;
|