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