@linguanahq/vite 0.1.0 → 0.1.1
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/dist/adapters.test.d.ts +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +131 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +19 -0
- package/dist/shared.d.ts +34 -0
- package/dist/solid.d.ts +2 -0
- package/dist/solid.js +21 -0
- package/dist/svelte.d.ts +2 -0
- package/dist/svelte.js +282 -0
- package/dist/vue.d.ts +2 -0
- package/dist/vue.js +241 -0
- package/package.json +63 -49
- package/src/index.ts +0 -173
- package/src/react.ts +0 -19
- package/src/shared.ts +0 -145
- package/src/solid.ts +0 -21
- package/src/svelte.ts +0 -226
- package/src/vue.ts +0 -191
package/dist/vue.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// src/vue.ts
|
|
2
|
+
import { NodeTypes } from "@vue/compiler-dom";
|
|
3
|
+
import { parse } from "@vue/compiler-sfc";
|
|
4
|
+
import MagicString from "magic-string";
|
|
5
|
+
|
|
6
|
+
// src/shared.ts
|
|
7
|
+
import { relative } from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
contentHash,
|
|
10
|
+
normalizeSource,
|
|
11
|
+
parseMessagePlaceholders,
|
|
12
|
+
stableMessageId
|
|
13
|
+
} from "@linguanahq/catalog";
|
|
14
|
+
var EXCLUDED_ELEMENTS = /* @__PURE__ */ new Set(["script", "style", "code", "pre"]);
|
|
15
|
+
var TRANSLATABLE_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
16
|
+
"title",
|
|
17
|
+
"placeholder",
|
|
18
|
+
"aria-label",
|
|
19
|
+
"alt",
|
|
20
|
+
"label"
|
|
21
|
+
]);
|
|
22
|
+
function renderedText(value) {
|
|
23
|
+
return value.replace(/\s+/g, " ").trim();
|
|
24
|
+
}
|
|
25
|
+
function escapeAttribute(value) {
|
|
26
|
+
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<");
|
|
27
|
+
}
|
|
28
|
+
function message(input) {
|
|
29
|
+
const context = { kind: "text", element: input.element };
|
|
30
|
+
return {
|
|
31
|
+
id: stableMessageId({
|
|
32
|
+
sourceLocale: input.sourceLocale,
|
|
33
|
+
source: input.source,
|
|
34
|
+
context
|
|
35
|
+
}),
|
|
36
|
+
source: input.source,
|
|
37
|
+
normalized: normalizeSource(input.source),
|
|
38
|
+
contentHash: contentHash(input.source),
|
|
39
|
+
context,
|
|
40
|
+
placeholders: parseMessagePlaceholders(input.source),
|
|
41
|
+
confidence: "high",
|
|
42
|
+
locations: [
|
|
43
|
+
{
|
|
44
|
+
file: relative(input.root, input.filename).replaceAll("\\", "/"),
|
|
45
|
+
line: input.line,
|
|
46
|
+
column: input.column
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function attributeMessage(input) {
|
|
52
|
+
const context = {
|
|
53
|
+
kind: "attribute",
|
|
54
|
+
element: input.element,
|
|
55
|
+
attribute: input.attribute
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
id: stableMessageId({
|
|
59
|
+
sourceLocale: input.sourceLocale,
|
|
60
|
+
source: input.source,
|
|
61
|
+
context
|
|
62
|
+
}),
|
|
63
|
+
source: input.source,
|
|
64
|
+
normalized: normalizeSource(input.source),
|
|
65
|
+
contentHash: contentHash(input.source),
|
|
66
|
+
context,
|
|
67
|
+
placeholders: parseMessagePlaceholders(input.source),
|
|
68
|
+
confidence: "high",
|
|
69
|
+
locations: [
|
|
70
|
+
{
|
|
71
|
+
file: relative(input.root, input.filename).replaceAll("\\", "/"),
|
|
72
|
+
line: input.line,
|
|
73
|
+
column: input.column
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function explicitMessage(input) {
|
|
79
|
+
const context = { kind: "explicit" };
|
|
80
|
+
return {
|
|
81
|
+
id: stableMessageId({
|
|
82
|
+
sourceLocale: input.sourceLocale,
|
|
83
|
+
source: input.source,
|
|
84
|
+
context,
|
|
85
|
+
explicitId: input.explicitId
|
|
86
|
+
}),
|
|
87
|
+
source: input.source,
|
|
88
|
+
normalized: normalizeSource(input.source),
|
|
89
|
+
contentHash: contentHash(input.source),
|
|
90
|
+
context,
|
|
91
|
+
placeholders: parseMessagePlaceholders(input.source),
|
|
92
|
+
confidence: "explicit",
|
|
93
|
+
locations: [
|
|
94
|
+
{
|
|
95
|
+
file: relative(input.root, input.filename).replaceAll("\\", "/"),
|
|
96
|
+
line: input.line,
|
|
97
|
+
column: input.column
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function unsafeDiagnostic(item) {
|
|
103
|
+
if (/^(?:https?:\/\/|mailto:|tel:|www\.)/i.test(item.normalized))
|
|
104
|
+
return {
|
|
105
|
+
code: "unsafe-static-copy",
|
|
106
|
+
severity: "info",
|
|
107
|
+
message: "URL-like or machine-readable text was excluded from extraction.",
|
|
108
|
+
location: item.locations[0]
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/vue.ts
|
|
113
|
+
function vue() {
|
|
114
|
+
return {
|
|
115
|
+
id: "vue",
|
|
116
|
+
version: "0.1.1",
|
|
117
|
+
extensions: [".vue"],
|
|
118
|
+
runtimeModule: "@linguanahq/vue/compiler-runtime",
|
|
119
|
+
async transform(input) {
|
|
120
|
+
const parsed = parse(input.code, {
|
|
121
|
+
filename: input.filename,
|
|
122
|
+
sourceMap: true
|
|
123
|
+
});
|
|
124
|
+
const template = parsed.descriptor.template;
|
|
125
|
+
if (!template?.ast) return null;
|
|
126
|
+
const output = new MagicString(input.code);
|
|
127
|
+
const messages = [];
|
|
128
|
+
const diagnostics = [];
|
|
129
|
+
const visit = (node, excluded = false, parentTag = "template") => {
|
|
130
|
+
const tag = node.type === NodeTypes.ELEMENT ? String(node.tag) : parentTag;
|
|
131
|
+
const nextExcluded = excluded || EXCLUDED_ELEMENTS.has(tag.toLowerCase()) || (node.props ?? []).some(
|
|
132
|
+
(prop) => prop.type === NodeTypes.ATTRIBUTE && ["data-no-translate", "hidden"].includes(prop.name)
|
|
133
|
+
);
|
|
134
|
+
if (node.type === NodeTypes.ELEMENT && ["T", "LinguanaMessage"].includes(tag)) {
|
|
135
|
+
const source = (node.props ?? []).find(
|
|
136
|
+
(prop) => prop.type === NodeTypes.ATTRIBUTE && ["default-message", "defaultMessage"].includes(prop.name)
|
|
137
|
+
)?.value?.content;
|
|
138
|
+
const explicitId = (node.props ?? []).find(
|
|
139
|
+
(prop) => prop.type === NodeTypes.ATTRIBUTE && prop.name === "id"
|
|
140
|
+
)?.value?.content;
|
|
141
|
+
if (source)
|
|
142
|
+
messages.push(
|
|
143
|
+
explicitMessage({
|
|
144
|
+
source,
|
|
145
|
+
explicitId,
|
|
146
|
+
filename: input.filename,
|
|
147
|
+
root: input.root,
|
|
148
|
+
line: node.loc.start.line,
|
|
149
|
+
column: node.loc.start.column - 1,
|
|
150
|
+
sourceLocale: input.sourceLocale
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (node.type === NodeTypes.ELEMENT && (node.children ?? []).some(
|
|
156
|
+
(child) => child.type === NodeTypes.TEXT && Boolean(renderedText(child.content ?? ""))
|
|
157
|
+
) && (node.children ?? []).some(
|
|
158
|
+
(child) => child.type !== NodeTypes.TEXT
|
|
159
|
+
)) {
|
|
160
|
+
diagnostics.push({
|
|
161
|
+
code: "ambiguous-rich-text",
|
|
162
|
+
severity: "warning",
|
|
163
|
+
message: "Mixed text and Vue template nodes require an explicit <T> message.",
|
|
164
|
+
location: {
|
|
165
|
+
file: input.filename.slice(input.root.length + 1),
|
|
166
|
+
line: node.loc.start.line,
|
|
167
|
+
column: node.loc.start.column - 1
|
|
168
|
+
},
|
|
169
|
+
remediation: "Use <T default-message> or data-no-translate."
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (node.type === NodeTypes.ELEMENT && !nextExcluded) {
|
|
174
|
+
for (const prop of node.props ?? []) {
|
|
175
|
+
if (prop.type !== NodeTypes.ATTRIBUTE || !prop.value || !TRANSLATABLE_ATTRIBUTES.has(prop.name))
|
|
176
|
+
continue;
|
|
177
|
+
const item = attributeMessage({
|
|
178
|
+
source: prop.value.content,
|
|
179
|
+
filename: input.filename,
|
|
180
|
+
root: input.root,
|
|
181
|
+
line: prop.loc.start.line,
|
|
182
|
+
column: prop.loc.start.column - 1,
|
|
183
|
+
sourceLocale: input.sourceLocale,
|
|
184
|
+
element: tag,
|
|
185
|
+
attribute: prop.name
|
|
186
|
+
});
|
|
187
|
+
messages.push(item);
|
|
188
|
+
const expression = `$tAttr(${JSON.stringify(item.id)}, ${JSON.stringify(prop.value.content)})`;
|
|
189
|
+
const replacement = `:${prop.name}='${expression.replaceAll("&", "&").replaceAll("'", "'")}'`;
|
|
190
|
+
output.overwrite(
|
|
191
|
+
prop.loc.start.offset,
|
|
192
|
+
prop.loc.end.offset,
|
|
193
|
+
replacement
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (node.type === NodeTypes.TEXT && !nextExcluded) {
|
|
198
|
+
const source = renderedText(node.content ?? "");
|
|
199
|
+
if (source) {
|
|
200
|
+
const item = message({
|
|
201
|
+
source,
|
|
202
|
+
filename: input.filename,
|
|
203
|
+
root: input.root,
|
|
204
|
+
line: node.loc.start.line,
|
|
205
|
+
column: node.loc.start.column - 1,
|
|
206
|
+
sourceLocale: input.sourceLocale,
|
|
207
|
+
element: parentTag
|
|
208
|
+
});
|
|
209
|
+
const unsafe = unsafeDiagnostic(item);
|
|
210
|
+
if (unsafe) diagnostics.push(unsafe);
|
|
211
|
+
else {
|
|
212
|
+
messages.push(item);
|
|
213
|
+
output.overwrite(
|
|
214
|
+
node.loc.start.offset,
|
|
215
|
+
node.loc.end.offset,
|
|
216
|
+
`<LinguanaMessage id="${item.id}" default-message="${escapeAttribute(source)}" />`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const child of node.children ?? [])
|
|
222
|
+
visit(child, nextExcluded, tag);
|
|
223
|
+
};
|
|
224
|
+
visit(template.ast);
|
|
225
|
+
if (!messages.length && !diagnostics.length) return null;
|
|
226
|
+
return {
|
|
227
|
+
code: output.toString(),
|
|
228
|
+
map: output.generateMap({
|
|
229
|
+
hires: true,
|
|
230
|
+
source: input.filename,
|
|
231
|
+
includeContent: true
|
|
232
|
+
}),
|
|
233
|
+
messages,
|
|
234
|
+
diagnostics
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
export {
|
|
240
|
+
vue
|
|
241
|
+
};
|
package/package.json
CHANGED
|
@@ -1,51 +1,65 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
2
|
+
"name": "@linguanahq/vite",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Linguana vite integration",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./react": {
|
|
13
|
+
"types": "./dist/react.d.ts",
|
|
14
|
+
"import": "./dist/react.js",
|
|
15
|
+
"default": "./dist/react.js"
|
|
16
|
+
},
|
|
17
|
+
"./solid": {
|
|
18
|
+
"types": "./dist/solid.d.ts",
|
|
19
|
+
"import": "./dist/solid.js",
|
|
20
|
+
"default": "./dist/solid.js"
|
|
21
|
+
},
|
|
22
|
+
"./vue": {
|
|
23
|
+
"types": "./dist/vue.d.ts",
|
|
24
|
+
"import": "./dist/vue.js",
|
|
25
|
+
"default": "./dist/vue.js"
|
|
26
|
+
},
|
|
27
|
+
"./svelte": {
|
|
28
|
+
"types": "./dist/svelte.d.ts",
|
|
29
|
+
"import": "./dist/svelte.js",
|
|
30
|
+
"default": "./dist/svelte.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.12.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@linguanahq/build-core": "0.1.1",
|
|
41
|
+
"@linguanahq/catalog": "0.1.1",
|
|
42
|
+
"@linguanahq/compiler": "0.1.1",
|
|
43
|
+
"magic-string": "^0.30.21"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"vite": ">=7 <9",
|
|
47
|
+
"@vue/compiler-dom": ">=3.5 <4",
|
|
48
|
+
"@vue/compiler-sfc": ">=3.5 <4",
|
|
49
|
+
"svelte": ">=5 <6"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"@vue/compiler-dom": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"@vue/compiler-sfc": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"svelte": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
}
|
|
51
65
|
}
|
package/src/index.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
BuildSession,
|
|
5
|
-
type SourceAdapter,
|
|
6
|
-
type StrictPolicy,
|
|
7
|
-
} from "@linguanahq/build-core";
|
|
8
|
-
import {
|
|
9
|
-
createFilter,
|
|
10
|
-
normalizePath,
|
|
11
|
-
type Plugin,
|
|
12
|
-
type TransformResult,
|
|
13
|
-
version as viteVersion,
|
|
14
|
-
} from "vite";
|
|
15
|
-
|
|
16
|
-
export type LinguanaViteAdapter = SourceAdapter;
|
|
17
|
-
export type LinguanaViteOptions = {
|
|
18
|
-
adapter: LinguanaViteAdapter;
|
|
19
|
-
projectId: string;
|
|
20
|
-
environment?: string;
|
|
21
|
-
sourceLocale?: string;
|
|
22
|
-
apiUrl?: string;
|
|
23
|
-
token?: string;
|
|
24
|
-
include?: string | string[];
|
|
25
|
-
exclude?: string | string[];
|
|
26
|
-
output?: string;
|
|
27
|
-
upload?: boolean;
|
|
28
|
-
strict?: StrictPolicy;
|
|
29
|
-
translation?:
|
|
30
|
-
| false
|
|
31
|
-
| {
|
|
32
|
-
enabled: true;
|
|
33
|
-
maxChargeMicros: number;
|
|
34
|
-
waitTimeoutMs?: number;
|
|
35
|
-
publish: "development";
|
|
36
|
-
};
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const VERSION = "0.1.0";
|
|
40
|
-
const OFFICIAL_PLUGINS: Record<SourceAdapter["id"], string[]> = {
|
|
41
|
-
react: [
|
|
42
|
-
"vite:react-babel",
|
|
43
|
-
"vite:react-refresh",
|
|
44
|
-
"react-refresh/babel",
|
|
45
|
-
"tanstack-start",
|
|
46
|
-
],
|
|
47
|
-
solid: ["solid"],
|
|
48
|
-
vue: ["vite:vue"],
|
|
49
|
-
svelte: ["vite-plugin-svelte"],
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export function linguana(options: LinguanaViteOptions): Plugin {
|
|
53
|
-
if (!options.adapter)
|
|
54
|
-
throw new Error("Linguana requires exactly one framework adapter.");
|
|
55
|
-
const environment = options.environment ?? "development";
|
|
56
|
-
const sourceLocale = options.sourceLocale ?? "en";
|
|
57
|
-
if (options.translation && (!options.token || environment !== "development"))
|
|
58
|
-
throw new Error(
|
|
59
|
-
"Build translation requires a token and the development environment.",
|
|
60
|
-
);
|
|
61
|
-
if (
|
|
62
|
-
options.translation &&
|
|
63
|
-
(!Number.isSafeInteger(options.translation.maxChargeMicros) ||
|
|
64
|
-
options.translation.maxChargeMicros <= 0)
|
|
65
|
-
)
|
|
66
|
-
throw new Error(
|
|
67
|
-
"translation.maxChargeMicros must be a positive safe integer.",
|
|
68
|
-
);
|
|
69
|
-
let root = process.cwd();
|
|
70
|
-
let session: BuildSession;
|
|
71
|
-
let filter: ReturnType<typeof createFilter>;
|
|
72
|
-
let sawSsr = false;
|
|
73
|
-
let processSource: (
|
|
74
|
-
code: string,
|
|
75
|
-
filename: string,
|
|
76
|
-
ssr?: boolean,
|
|
77
|
-
) => Promise<TransformResult | null>;
|
|
78
|
-
|
|
79
|
-
const plugin: Plugin & { api?: Record<string, unknown> } = {
|
|
80
|
-
name: "linguana:vite",
|
|
81
|
-
enforce: "pre",
|
|
82
|
-
configResolved(config) {
|
|
83
|
-
root = config.root;
|
|
84
|
-
filter = createFilter(
|
|
85
|
-
options.include ??
|
|
86
|
-
options.adapter.extensions.map((extension) => `**/*${extension}`),
|
|
87
|
-
options.exclude ?? ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
|
|
88
|
-
);
|
|
89
|
-
session = new BuildSession({
|
|
90
|
-
projectId: options.projectId,
|
|
91
|
-
environment,
|
|
92
|
-
sourceLocale,
|
|
93
|
-
compilerVersion: VERSION,
|
|
94
|
-
strict: options.strict,
|
|
95
|
-
integration: {
|
|
96
|
-
tool: "vite",
|
|
97
|
-
toolVersion: viteVersion,
|
|
98
|
-
framework: options.adapter.id,
|
|
99
|
-
adapterVersion: options.adapter.version,
|
|
100
|
-
ssr: false,
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
const ownIndex = config.plugins.findIndex(
|
|
104
|
-
(item) => item.name === "linguana:vite",
|
|
105
|
-
);
|
|
106
|
-
const frameworkIndex = config.plugins.findIndex((item) =>
|
|
107
|
-
OFFICIAL_PLUGINS[options.adapter.id].some((name) =>
|
|
108
|
-
item.name.includes(name),
|
|
109
|
-
),
|
|
110
|
-
);
|
|
111
|
-
if (frameworkIndex < 0)
|
|
112
|
-
throw new Error(
|
|
113
|
-
`Linguana ${options.adapter.id} adapter requires the official ${options.adapter.id} Vite plugin.`,
|
|
114
|
-
);
|
|
115
|
-
if (ownIndex > frameworkIndex)
|
|
116
|
-
throw new Error(
|
|
117
|
-
`Place linguana() before the official ${options.adapter.id} plugin in vite.config.ts.`,
|
|
118
|
-
);
|
|
119
|
-
processSource = async (code, filename, ssr = false) => {
|
|
120
|
-
const cleanId = normalizePath(filename.split("?", 1)[0] ?? filename);
|
|
121
|
-
const projectRelativeId = relative(root, cleanId);
|
|
122
|
-
if (
|
|
123
|
-
options.include === undefined &&
|
|
124
|
-
(projectRelativeId === ".." ||
|
|
125
|
-
projectRelativeId.startsWith("../") ||
|
|
126
|
-
projectRelativeId.startsWith("..\\") ||
|
|
127
|
-
isAbsolute(projectRelativeId))
|
|
128
|
-
)
|
|
129
|
-
return null;
|
|
130
|
-
if (!filter(cleanId)) return null;
|
|
131
|
-
if (ssr) {
|
|
132
|
-
sawSsr = true;
|
|
133
|
-
session.options.integration.ssr = true;
|
|
134
|
-
}
|
|
135
|
-
const result = await options.adapter.transform({
|
|
136
|
-
code,
|
|
137
|
-
filename: cleanId,
|
|
138
|
-
root,
|
|
139
|
-
sourceLocale,
|
|
140
|
-
});
|
|
141
|
-
if (!result) return null;
|
|
142
|
-
session.collect(result);
|
|
143
|
-
return {
|
|
144
|
-
code: result.code,
|
|
145
|
-
map: result.map as TransformResult["map"],
|
|
146
|
-
};
|
|
147
|
-
};
|
|
148
|
-
},
|
|
149
|
-
async transform(code, id, transformOptions) {
|
|
150
|
-
return processSource(code, id, Boolean(transformOptions?.ssr));
|
|
151
|
-
},
|
|
152
|
-
async writeBundle() {
|
|
153
|
-
const manifest = await session.write(
|
|
154
|
-
resolve(root, options.output ?? ".linguana/manifest.json"),
|
|
155
|
-
);
|
|
156
|
-
for (const item of session.diagnostics)
|
|
157
|
-
this[item.severity === "error" ? "error" : "warn"](
|
|
158
|
-
`${item.code}: ${item.message}`,
|
|
159
|
-
);
|
|
160
|
-
if ((options.upload ?? Boolean(options.token)) && options.token)
|
|
161
|
-
await session.upload(manifest, {
|
|
162
|
-
apiUrl: options.apiUrl ?? "https://api.linguana.dev",
|
|
163
|
-
token: options.token,
|
|
164
|
-
translation: options.translation,
|
|
165
|
-
onProgress: (message) => this.info(`[linguana] ${message}`),
|
|
166
|
-
});
|
|
167
|
-
void sawSsr;
|
|
168
|
-
},
|
|
169
|
-
};
|
|
170
|
-
return plugin;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export default linguana;
|
package/src/react.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { SourceAdapter } from "@linguanahq/build-core";
|
|
2
|
-
import { transformSource } from "@linguanahq/compiler";
|
|
3
|
-
|
|
4
|
-
export function react(): SourceAdapter {
|
|
5
|
-
return {
|
|
6
|
-
id: "react",
|
|
7
|
-
version: "0.1.0",
|
|
8
|
-
extensions: [".jsx", ".tsx"],
|
|
9
|
-
runtimeModule: "@linguanahq/react/compiler-runtime",
|
|
10
|
-
async transform(input) {
|
|
11
|
-
return transformSource(input.code, input.filename, {
|
|
12
|
-
sourceLocale: input.sourceLocale,
|
|
13
|
-
root: input.root,
|
|
14
|
-
}) as ReturnType<SourceAdapter["transform"]> extends Promise<infer T>
|
|
15
|
-
? T
|
|
16
|
-
: never;
|
|
17
|
-
},
|
|
18
|
-
};
|
|
19
|
-
}
|
package/src/shared.ts
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
contentHash,
|
|
5
|
-
type Diagnostic,
|
|
6
|
-
type ManifestMessage,
|
|
7
|
-
normalizeSource,
|
|
8
|
-
parseMessagePlaceholders,
|
|
9
|
-
stableMessageId,
|
|
10
|
-
} from "@linguanahq/catalog";
|
|
11
|
-
|
|
12
|
-
export const EXCLUDED_ELEMENTS = new Set(["script", "style", "code", "pre"]);
|
|
13
|
-
export const TRANSLATABLE_ATTRIBUTES = new Set([
|
|
14
|
-
"title",
|
|
15
|
-
"placeholder",
|
|
16
|
-
"aria-label",
|
|
17
|
-
"alt",
|
|
18
|
-
"label",
|
|
19
|
-
]);
|
|
20
|
-
|
|
21
|
-
export function renderedText(value: string) {
|
|
22
|
-
return value.replace(/\s+/g, " ").trim();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function escapeAttribute(value: string) {
|
|
26
|
-
return value
|
|
27
|
-
.replaceAll("&", "&")
|
|
28
|
-
.replaceAll('"', """)
|
|
29
|
-
.replaceAll("<", "<");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function message(input: {
|
|
33
|
-
source: string;
|
|
34
|
-
filename: string;
|
|
35
|
-
root: string;
|
|
36
|
-
line: number;
|
|
37
|
-
column: number;
|
|
38
|
-
sourceLocale: string;
|
|
39
|
-
element: string;
|
|
40
|
-
}): ManifestMessage {
|
|
41
|
-
const context = { kind: "text" as const, element: input.element };
|
|
42
|
-
return {
|
|
43
|
-
id: stableMessageId({
|
|
44
|
-
sourceLocale: input.sourceLocale,
|
|
45
|
-
source: input.source,
|
|
46
|
-
context,
|
|
47
|
-
}),
|
|
48
|
-
source: input.source,
|
|
49
|
-
normalized: normalizeSource(input.source),
|
|
50
|
-
contentHash: contentHash(input.source),
|
|
51
|
-
context,
|
|
52
|
-
placeholders: parseMessagePlaceholders(input.source),
|
|
53
|
-
confidence: "high",
|
|
54
|
-
locations: [
|
|
55
|
-
{
|
|
56
|
-
file: relative(input.root, input.filename).replaceAll("\\", "/"),
|
|
57
|
-
line: input.line,
|
|
58
|
-
column: input.column,
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function attributeMessage(input: {
|
|
65
|
-
source: string;
|
|
66
|
-
filename: string;
|
|
67
|
-
root: string;
|
|
68
|
-
line: number;
|
|
69
|
-
column: number;
|
|
70
|
-
sourceLocale: string;
|
|
71
|
-
element: string;
|
|
72
|
-
attribute: string;
|
|
73
|
-
}): ManifestMessage {
|
|
74
|
-
const context = {
|
|
75
|
-
kind: "attribute" as const,
|
|
76
|
-
element: input.element,
|
|
77
|
-
attribute: input.attribute,
|
|
78
|
-
};
|
|
79
|
-
return {
|
|
80
|
-
id: stableMessageId({
|
|
81
|
-
sourceLocale: input.sourceLocale,
|
|
82
|
-
source: input.source,
|
|
83
|
-
context,
|
|
84
|
-
}),
|
|
85
|
-
source: input.source,
|
|
86
|
-
normalized: normalizeSource(input.source),
|
|
87
|
-
contentHash: contentHash(input.source),
|
|
88
|
-
context,
|
|
89
|
-
placeholders: parseMessagePlaceholders(input.source),
|
|
90
|
-
confidence: "high",
|
|
91
|
-
locations: [
|
|
92
|
-
{
|
|
93
|
-
file: relative(input.root, input.filename).replaceAll("\\", "/"),
|
|
94
|
-
line: input.line,
|
|
95
|
-
column: input.column,
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function explicitMessage(input: {
|
|
102
|
-
source: string;
|
|
103
|
-
explicitId?: string;
|
|
104
|
-
filename: string;
|
|
105
|
-
root: string;
|
|
106
|
-
line: number;
|
|
107
|
-
column: number;
|
|
108
|
-
sourceLocale: string;
|
|
109
|
-
}): ManifestMessage {
|
|
110
|
-
const context = { kind: "explicit" as const };
|
|
111
|
-
return {
|
|
112
|
-
id: stableMessageId({
|
|
113
|
-
sourceLocale: input.sourceLocale,
|
|
114
|
-
source: input.source,
|
|
115
|
-
context,
|
|
116
|
-
explicitId: input.explicitId,
|
|
117
|
-
}),
|
|
118
|
-
source: input.source,
|
|
119
|
-
normalized: normalizeSource(input.source),
|
|
120
|
-
contentHash: contentHash(input.source),
|
|
121
|
-
context,
|
|
122
|
-
placeholders: parseMessagePlaceholders(input.source),
|
|
123
|
-
confidence: "explicit",
|
|
124
|
-
locations: [
|
|
125
|
-
{
|
|
126
|
-
file: relative(input.root, input.filename).replaceAll("\\", "/"),
|
|
127
|
-
line: input.line,
|
|
128
|
-
column: input.column,
|
|
129
|
-
},
|
|
130
|
-
],
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function unsafeDiagnostic(
|
|
135
|
-
item: ManifestMessage,
|
|
136
|
-
): Diagnostic | undefined {
|
|
137
|
-
if (/^(?:https?:\/\/|mailto:|tel:|www\.)/i.test(item.normalized))
|
|
138
|
-
return {
|
|
139
|
-
code: "unsafe-static-copy",
|
|
140
|
-
severity: "info",
|
|
141
|
-
message:
|
|
142
|
-
"URL-like or machine-readable text was excluded from extraction.",
|
|
143
|
-
location: item.locations[0],
|
|
144
|
-
};
|
|
145
|
-
}
|