@mdbase-dev/i18n 0.0.0-stage → 0.1.0-beta.148

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.
@@ -0,0 +1,6 @@
1
+ import type { LinguiConfig } from "@lingui/conf";
2
+ export declare const locales: readonly ["en", "fr", "de", "es", "ja", "ko", "pt", "ru", "zh", "it"];
3
+ export declare function i18nConfig(options?: Omit<LinguiConfig,
4
+ "locales" | "sourceLocale" | "fallbackLocales" | "format" | "catalogs" | "compileNamespace"> & {
5
+ include?: string[]; exclude?: string[]; path?: string;
6
+ }): LinguiConfig;
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "@lingui/conf";
2
+ import { formatter } from "@lingui/format-po";
3
+
4
+ export const locales = ["en", "fr", "de", "es", "ja", "ko", "pt", "ru", "zh", "it"];
5
+ /** Source/context generated IDs are Lingui's default; never supply explicit IDs. */
6
+ export function i18nConfig({ include = ["<rootDir>/src"], exclude = ["**/*.test.*", "**/*.spec.*"],
7
+ path = "<rootDir>/src/i18n/locales/{locale}/messages", ...options } = {}) {
8
+ return defineConfig({
9
+ ...options, locales, sourceLocale: "en", fallbackLocales: { default: "en" },
10
+ format: formatter({ origins: true, lineNumbers: false }),
11
+ catalogs: [{ path, include, exclude }], compileNamespace: "json",
12
+ });
13
+ }
@@ -0,0 +1,297 @@
1
+ import ts from "typescript";
2
+
3
+ const visibleAttributes = new Set([
4
+ "aria-label",
5
+ "ariaLabel",
6
+ "title",
7
+ "label",
8
+ "legend",
9
+ "placeholder",
10
+ "actionLabel",
11
+ "allDayText",
12
+ "noEventsText",
13
+ "description",
14
+ "detail",
15
+ "body",
16
+ "summary",
17
+ "action",
18
+ "recovery",
19
+ "alt",
20
+ "data-swipe-hint",
21
+ ]);
22
+ const visibleProperties = new Set([
23
+ "label",
24
+ "description",
25
+ "ariaLabel",
26
+ "placeholder",
27
+ ]);
28
+ const brands = new Set(["TaskNotes", "mdbase", "Markdown", "ICS"]);
29
+ const macros = new Set([
30
+ "t",
31
+ "translate",
32
+ "msg",
33
+ "defineMessage",
34
+ "plural",
35
+ "select",
36
+ "selectOrdinal",
37
+ ]);
38
+ const comparisons = new Set([
39
+ ts.SyntaxKind.EqualsEqualsEqualsToken,
40
+ ts.SyntaxKind.ExclamationEqualsEqualsToken,
41
+ ts.SyntaxKind.EqualsEqualsToken,
42
+ ts.SyntaxKind.ExclamationEqualsToken,
43
+ ]);
44
+
45
+ /** Structural guard, not an assertion that arbitrary English in every layer is UI copy. */
46
+ export function inspectUiSource(source, file = "component.tsx", options = {}) {
47
+ const allowedBrands = new Set([...brands, ...(options.brands ?? [])]);
48
+ const ast = ts.createSourceFile(
49
+ file,
50
+ source,
51
+ ts.ScriptTarget.Latest,
52
+ true,
53
+ /\.[jt]sx$/.test(file) ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
54
+ );
55
+ const issues = [];
56
+ const add = (node, kind, text) =>
57
+ issues.push({
58
+ file,
59
+ line: ast.getLineAndCharacterOfPosition(node.getStart(ast)).line + 1,
60
+ kind,
61
+ text,
62
+ });
63
+ const tag = (node) => node.tagName?.getText(ast);
64
+ // Core macros may bind an explicit instance: t(i18n)`...` / t(i18n)({...}).
65
+ const macroName = (expression) => {
66
+ if (ts.isIdentifier(expression)) return expression.text;
67
+ if (ts.isCallExpression(expression) && expression.arguments.length === 1 &&
68
+ ["t", "translate"].includes(expression.expression.getText(ast)))
69
+ return expression.expression.getText(ast);
70
+ return undefined;
71
+ };
72
+ const isTranslation = (node) => {
73
+ if (ts.isTaggedTemplateExpression(node))
74
+ return ["t", "translate"].includes(macroName(node.tag));
75
+ if (!ts.isCallExpression(node) ||
76
+ !["t", "translate", "plural", "select", "selectOrdinal"].includes(macroName(node.expression))) return false;
77
+ // The inner t(i18n) selects an instance; it is not itself a translation.
78
+ return !((ts.isTaggedTemplateExpression(node.parent) && node.parent.tag === node) ||
79
+ (ts.isCallExpression(node.parent) && node.parent.expression === node));
80
+ };
81
+ function covered(node) {
82
+ let expression = ts.isJsxExpression(node);
83
+ for (let p = node.parent; p; p = p.parent) {
84
+ if (ts.isJsxExpression(p)) expression = true;
85
+ if (isTranslation(p) ||
86
+ (ts.isCallExpression(p) && macros.has(macroName(p.expression)))) return true;
87
+ if (
88
+ ts.isJsxElement(p) &&
89
+ ["Trans", "Plural", "Select", "SelectOrdinal"].includes(
90
+ tag(p.openingElement),
91
+ ) &&
92
+ !expression
93
+ )
94
+ return true;
95
+ if (
96
+ ts.isJsxSelfClosingElement(p) &&
97
+ ["Plural", "Select", "SelectOrdinal"].includes(tag(p))
98
+ )
99
+ return true;
100
+ }
101
+ return false;
102
+ }
103
+ const human = (text) => /[A-Za-z]/.test(text) && !allowedBrands.has(text.trim());
104
+ const identifiers = new Set([
105
+ "className",
106
+ "id",
107
+ "key",
108
+ "href",
109
+ "src",
110
+ "role",
111
+ "type",
112
+ ]);
113
+ const stateBindings = new Map();
114
+ const owner = (node) => {
115
+ for (let p = node.parent; p; p = p.parent) if (ts.isFunctionLike(p)) return p;
116
+ };
117
+ function collectState(node) {
118
+ if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) &&
119
+ node.initializer && ts.isCallExpression(node.initializer) &&
120
+ /^(React\.)?useState$/.test(node.initializer.expression.getText(ast))) {
121
+ const [value, setter] = node.name.elements;
122
+ if (value && ts.isBindingElement(value) && ts.isIdentifier(value.name) &&
123
+ (!setter || (ts.isBindingElement(setter) && ts.isIdentifier(setter.name)))) {
124
+ const scope = owner(node);
125
+ if (!stateBindings.has(scope)) stateBindings.set(scope, []);
126
+ stateBindings.get(scope).push({ value: value.name.text, setter: setter?.name.text, declaration: node });
127
+ }
128
+ }
129
+ ts.forEachChild(node, collectState);
130
+ }
131
+ collectState(ast);
132
+ // Each binding carries the member paths that reach copy, not the whole object.
133
+ // An empty path means the state value itself is rendered.
134
+ const visibleState = new Map();
135
+ const propertyName = (name) => {
136
+ if (ts.isComputedPropertyName(name)) {
137
+ name = name.expression;
138
+ if (!ts.isStringLiteralLike(name) && !ts.isNumericLiteral(name)) return "*";
139
+ }
140
+ return ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isNumericLiteral(name)
141
+ ? name.text : "*";
142
+ };
143
+ const reachesStateOutput = (binding, path) => visibleState.get(binding)?.some((rendered) =>
144
+ rendered.length <= path.length && rendered.every((key, i) =>
145
+ key === "*" || path[i] === "*" || key === path[i]));
146
+ function stateFor(node, name) {
147
+ for (let p = node; p; p = p.parent) {
148
+ const binding = stateBindings.get(p)?.find((binding) => binding.value === name || binding.setter === name);
149
+ if (binding) return binding;
150
+ }
151
+ }
152
+ const isStateSetter = (node) => ts.isCallExpression(node) && ts.isIdentifier(node.expression) &&
153
+ visibleState.has(stateFor(node, node.expression.text)) &&
154
+ stateFor(node, node.expression.text)?.setter === node.expression.text;
155
+ function output(node) {
156
+ let property = false;
157
+ const path = [];
158
+ for (let p = node.parent; p; p = p.parent) {
159
+ // Types and opaque prop spreads do not render a value at this boundary.
160
+ if (ts.isTypeNode(p) || ts.isJsxSpreadAttribute(p)) return false;
161
+ if (ts.isBinaryExpression(p) && comparisons.has(p.operatorToken.kind))
162
+ return false;
163
+ if (
164
+ ts.isConditionalExpression(p) &&
165
+ p.condition.pos <= node.pos &&
166
+ p.condition.end >= node.end
167
+ )
168
+ return false;
169
+ // The left operand of && controls whether the right operand is rendered;
170
+ // it does not render an object's fields (notice && <p>{notice.text}</p>).
171
+ if (ts.isBinaryExpression(p) && p.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
172
+ p.left.pos <= node.pos && p.left.end >= node.end) return false;
173
+ if (ts.isElementAccessExpression(p) && p.argumentExpression.pos <= node.pos &&
174
+ p.argumentExpression.end >= node.end) return false;
175
+ if (ts.isCallExpression(p)) {
176
+ const binding = isStateSetter(p) ? stateFor(p, p.expression.text) :
177
+ [...visibleState.keys()].find((binding) => binding.declaration.initializer === p);
178
+ return reachesStateOutput(binding, path) ?? false;
179
+ }
180
+ if (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) {
181
+ if (p.name.pos <= node.pos && p.name.end >= node.end && !ts.isShorthandPropertyAssignment(p))
182
+ return false;
183
+ path.unshift(propertyName(p.name));
184
+ property = visibleProperties.has(propertyName(p.name));
185
+ }
186
+ if (ts.isJsxAttribute(p))
187
+ return property || visibleAttributes.has(p.name.getText(ast));
188
+ if (ts.isJsxExpression(p)) {
189
+ if (ts.isJsxElement(p.parent) || ts.isJsxFragment(p.parent))
190
+ return true;
191
+ }
192
+ if (ts.isVariableDeclaration(p)) {
193
+ const binding = [...visibleState.keys()].find((binding) => binding.declaration === p);
194
+ if (binding) return reachesStateOutput(binding, path);
195
+ }
196
+ if (ts.isFunctionLike(p) && !isStateSetter(p.parent)) return false;
197
+ }
198
+ return false;
199
+ }
200
+ // Follow local React state only when its value is actually rendered as copy.
201
+ // Enum comparisons, input values, protocol state and arbitrary calls stay opaque.
202
+ function collectVisibleState(node) {
203
+ if (ts.isIdentifier(node)) {
204
+ const binding = stateFor(node, node.text);
205
+ if (binding?.value === node.text) {
206
+ let reference = node;
207
+ const path = [];
208
+ while ((ts.isPropertyAccessExpression(reference.parent) || ts.isElementAccessExpression(reference.parent)) &&
209
+ reference.parent.expression === reference) {
210
+ reference = reference.parent;
211
+ path.push(ts.isPropertyAccessExpression(reference) ? reference.name.text :
212
+ ts.isStringLiteralLike(reference.argumentExpression) || ts.isNumericLiteral(reference.argumentExpression)
213
+ ? reference.argumentExpression.text : "*");
214
+ }
215
+ // Member names and computed keys are not references to the state value.
216
+ if (!(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) &&
217
+ output(reference)) {
218
+ if (!visibleState.has(binding)) visibleState.set(binding, []);
219
+ visibleState.get(binding).push(path);
220
+ }
221
+ }
222
+ }
223
+ ts.forEachChild(node, collectVisibleState);
224
+ }
225
+ collectVisibleState(ast);
226
+ function visit(node) {
227
+ if (ts.isJsxText(node) && human(node.text.trim()) && !covered(node))
228
+ add(node, "unmarked JSX text", node.text.trim());
229
+ if (
230
+ ts.isStringLiteralLike(node) &&
231
+ human(node.text) &&
232
+ !covered(node) &&
233
+ output(node)
234
+ )
235
+ add(node, "unmarked visible string", node.text);
236
+ if (
237
+ ts.isTemplateExpression(node) &&
238
+ human(
239
+ node.head.text + node.templateSpans.map((s) => s.literal.text).join(""),
240
+ ) &&
241
+ !covered(node) &&
242
+ output(node)
243
+ )
244
+ add(node, "unmarked visible interpolation", node.getText(ast));
245
+ if (isTranslation(node)) {
246
+ const parent = node.parent;
247
+ if (
248
+ ts.isBinaryExpression(parent) &&
249
+ comparisons.has(parent.operatorToken.kind)
250
+ )
251
+ add(node, "translation in control-flow comparison", node.getText(ast));
252
+ if (
253
+ ts.isCallExpression(parent) &&
254
+ /\.(includes|has|get|startsWith|endsWith)$/.test(
255
+ parent.expression.getText(ast),
256
+ )
257
+ )
258
+ add(node, "translation in identifier lookup", node.getText(ast));
259
+ if (
260
+ ts.isCallExpression(parent) &&
261
+ parent.expression.getText(ast) === "countLabel" &&
262
+ parent.arguments[1] === node
263
+ )
264
+ add(node, "translation in count-kind identifier", node.getText(ast));
265
+ for (let p = node.parent; p; p = p.parent) {
266
+ if (
267
+ ts.isTaggedTemplateExpression(p) &&
268
+ ["t", "translate"].includes(macroName(p.tag)) &&
269
+ // Lingui folds plural/select helpers into the enclosing ICU descriptor.
270
+ !(ts.isCallExpression(node) &&
271
+ ["plural", "select", "selectOrdinal"].includes(macroName(node.expression)))
272
+ ) {
273
+ add(node, "uncompiled nested translation macro", node.getText(ast));
274
+ break;
275
+ }
276
+ if (ts.isJsxAttribute(p) && identifiers.has(p.name.getText(ast)))
277
+ add(node, "translation in identifier attribute", p.name.getText(ast));
278
+ if ((ts.isPropertyAssignment(p) || ts.isPropertyDeclaration(p)) && identifiers.has(p.name.getText(ast)))
279
+ add(node, "translation in identifier property", p.name.getText(ast));
280
+ if (
281
+ ts.isVariableDeclaration(p) &&
282
+ /Class(?:Name)?$/.test(p.name.getText(ast))
283
+ )
284
+ add(node, "translation in CSS class variable", p.name.getText(ast));
285
+ if (ts.isFunctionLike(p)) break;
286
+ }
287
+ let fn = node.parent;
288
+ while (fn && !ts.isFunctionLike(fn) &&
289
+ !(ts.isPropertyDeclaration(fn) &&
290
+ !fn.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword))) fn = fn.parent;
291
+ if (!fn) add(node, "eager module-level translation", node.getText(ast));
292
+ }
293
+ ts.forEachChild(node, visit);
294
+ }
295
+ visit(ast);
296
+ return issues;
297
+ }