@awsless/i18n 0.0.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/README.MD +87 -0
- package/dist/index.cjs +305 -0
- package/dist/index.d.cts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +266 -0
- package/dist/svelte.cjs +42 -0
- package/dist/svelte.d.cts +9 -0
- package/dist/svelte.d.ts +9 -0
- package/dist/svelte.js +16 -0
- package/package.json +61 -0
package/README.MD
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# AI-generated internationalization made easy
|
|
2
|
+
|
|
3
|
+
The @awsless/i18n package is a Vite plugin that automatically translates your text in build time using AI or any other tool you prefer. The plugin will inline the translations so you don't have to worry about loading the translations at the right time.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Automatic text translation
|
|
8
|
+
- Inlines translations
|
|
9
|
+
- Extremely lightweight (373 bytes uncompressed)
|
|
10
|
+
- Svelte support
|
|
11
|
+
|
|
12
|
+
_A simple wrapper can be made for any framework._
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
Install with (NPM):
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
npm i @awsless/i18n
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Vite installation
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { i18n, chatgpt } from '@awsless/i18n'
|
|
26
|
+
|
|
27
|
+
export defineConfig({
|
|
28
|
+
plugins: [
|
|
29
|
+
i18n({
|
|
30
|
+
default: 'en',
|
|
31
|
+
locales: ['es', 'jp'],
|
|
32
|
+
translate: chatgpt({
|
|
33
|
+
apiKey: '...',
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
]
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Svelte example
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { t } from '@awsless/i18n/svelte'
|
|
44
|
+
|
|
45
|
+
const count = 1
|
|
46
|
+
|
|
47
|
+
$t`${count} count`
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The plugin will find all instances where you want text to be translated.
|
|
51
|
+
The text is translated automatically during build time to produce
|
|
52
|
+
a bundled output something like this:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { t } from '@awsless/i18n/svelte'
|
|
56
|
+
|
|
57
|
+
const count = 1
|
|
58
|
+
|
|
59
|
+
$t.get({
|
|
60
|
+
en: `${count} count`,
|
|
61
|
+
es: `${count} contar`,
|
|
62
|
+
jp: `${count} カウント`,
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
To change the locale that is being rendered simply call the `locale.set` function.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { locale } from '@awsless/i18n/svelte'
|
|
70
|
+
|
|
71
|
+
locale.set('jp')
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Changing the AI-generated text
|
|
75
|
+
|
|
76
|
+
A `i18n.json` file with all the translations will be generated in the root of your project the first time you run a build. We use this file as a cache to not translate any text that has already been translated before.
|
|
77
|
+
|
|
78
|
+
If you don't like the auto-generated translations by AI, you can safely change any of the ai generated translations here.
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"${count} count": {
|
|
83
|
+
"es": "${count} contar",
|
|
84
|
+
"jp": "${count} カウント"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
chatgpt: () => chatgpt,
|
|
34
|
+
i18n: () => createI18nPlugin,
|
|
35
|
+
mock: () => mock
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(src_exports);
|
|
38
|
+
|
|
39
|
+
// src/cache.ts
|
|
40
|
+
var import_promises = require("fs/promises");
|
|
41
|
+
var import_path = require("path");
|
|
42
|
+
var loadCache = async (cwd) => {
|
|
43
|
+
try {
|
|
44
|
+
const data = await (0, import_promises.readFile)((0, import_path.join)(cwd, "i18n.json"), "utf8");
|
|
45
|
+
return new Cache(JSON.parse(data));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return new Cache();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var saveCache = async (cwd, cache) => {
|
|
51
|
+
await (0, import_promises.writeFile)((0, import_path.join)(cwd, "i18n.json"), JSON.stringify(cache, void 0, 2));
|
|
52
|
+
};
|
|
53
|
+
var Cache = class {
|
|
54
|
+
constructor(data = {}) {
|
|
55
|
+
this.data = data;
|
|
56
|
+
}
|
|
57
|
+
set(original, locale, translation) {
|
|
58
|
+
if (!this.data[original]) {
|
|
59
|
+
this.data[original] = {};
|
|
60
|
+
}
|
|
61
|
+
if (!this.data[original][locale]) {
|
|
62
|
+
this.data[original][locale] = translation;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
get(original, locale) {
|
|
66
|
+
return this.data[original]?.[locale];
|
|
67
|
+
}
|
|
68
|
+
has(original, locale) {
|
|
69
|
+
return typeof this.get(original, locale) === "string";
|
|
70
|
+
}
|
|
71
|
+
delete(original, locale) {
|
|
72
|
+
if (this.data[original]?.[locale]) {
|
|
73
|
+
delete this.data[original][locale];
|
|
74
|
+
}
|
|
75
|
+
if (this.data[original] && Object.keys(this.data[original]).length === 0) {
|
|
76
|
+
delete this.data[original];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
*entries() {
|
|
80
|
+
for (const [original, locales] of Object.entries(this.data)) {
|
|
81
|
+
for (const [locale, translation] of Object.entries(locales)) {
|
|
82
|
+
yield { original, locale, translation };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
toJSON() {
|
|
87
|
+
return this.data;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/diff.ts
|
|
92
|
+
var findNewTranslations = (cache, originals, locales) => {
|
|
93
|
+
const list = [];
|
|
94
|
+
for (const original of originals) {
|
|
95
|
+
for (const locale of locales) {
|
|
96
|
+
if (!cache.has(original, locale)) {
|
|
97
|
+
list.push({ original, locale });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return list;
|
|
102
|
+
};
|
|
103
|
+
var removeUnusedTranslations = (cache, originals, locales) => {
|
|
104
|
+
for (const item of cache.entries()) {
|
|
105
|
+
if (!locales.includes(item.locale) || !originals.includes(item.original)) {
|
|
106
|
+
cache.delete(item.original, item.locale);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/find.ts
|
|
112
|
+
var import_promises2 = require("fs/promises");
|
|
113
|
+
var import_glob = require("glob");
|
|
114
|
+
var import_path2 = require("path");
|
|
115
|
+
|
|
116
|
+
// src/find/svelte.ts
|
|
117
|
+
var import_line_column = __toESM(require("line-column"), 1);
|
|
118
|
+
var import_compiler = require("svelte/compiler");
|
|
119
|
+
var findSvelteTranslatable = (code) => {
|
|
120
|
+
const found = [];
|
|
121
|
+
const origin = (0, import_line_column.default)(code);
|
|
122
|
+
const ast = (0, import_compiler.parse)(code, {
|
|
123
|
+
css: false
|
|
124
|
+
});
|
|
125
|
+
const enter = (node) => {
|
|
126
|
+
if (
|
|
127
|
+
//
|
|
128
|
+
node.type === "TaggedTemplateExpression" && node.tag.type === "Identifier" && node.tag.name === "$t" && node.quasi.type === "TemplateLiteral" && node.quasi.loc
|
|
129
|
+
) {
|
|
130
|
+
const start = node.quasi.loc.start;
|
|
131
|
+
const end = node.quasi.loc.end;
|
|
132
|
+
const content = code.substring(
|
|
133
|
+
origin.toIndex(start.line, start.column) + 2,
|
|
134
|
+
origin.toIndex(end.line, end.column)
|
|
135
|
+
);
|
|
136
|
+
found.push(content);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
(0, import_compiler.walk)(ast.html, { enter });
|
|
140
|
+
if (ast.instance) {
|
|
141
|
+
(0, import_compiler.walk)(ast.instance.content, { enter });
|
|
142
|
+
}
|
|
143
|
+
if (ast.module) {
|
|
144
|
+
(0, import_compiler.walk)(ast.module.content, { enter });
|
|
145
|
+
}
|
|
146
|
+
return found;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/find/typescript.ts
|
|
150
|
+
var import_core = require("@swc/core");
|
|
151
|
+
var import_swc_walk = require("swc-walk");
|
|
152
|
+
var findTypescriptTranslatable = async (code) => {
|
|
153
|
+
const found = [];
|
|
154
|
+
const ast = await (0, import_core.parse)(code, {
|
|
155
|
+
syntax: "typescript"
|
|
156
|
+
});
|
|
157
|
+
(0, import_swc_walk.simple)(ast, {
|
|
158
|
+
TaggedTemplateExpression(node) {
|
|
159
|
+
if (node.tag.type === "CallExpression" && node.tag.callee.type === "Identifier" && node.tag.callee.value === "get" && node.template.type === "TemplateLiteral") {
|
|
160
|
+
const content = code.substring(
|
|
161
|
+
node.template.span.start - ast.span.start + 1,
|
|
162
|
+
node.template.span.end - ast.span.start - 1
|
|
163
|
+
);
|
|
164
|
+
found.push(content);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return found;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// src/find.ts
|
|
172
|
+
var findTranslatable = async (cwd) => {
|
|
173
|
+
const files = await (0, import_glob.glob)("**/*.{js,ts,svelte}", {
|
|
174
|
+
cwd,
|
|
175
|
+
ignore: ["**/node_modules"]
|
|
176
|
+
});
|
|
177
|
+
const found = [];
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
const code = await (0, import_promises2.readFile)((0, import_path2.join)(cwd, file), "utf8");
|
|
180
|
+
if (code.includes("$t`")) {
|
|
181
|
+
if ((0, import_path2.extname)(file) === ".svelte") {
|
|
182
|
+
found.push(...findSvelteTranslatable(code));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (code.includes("get(t)`")) {
|
|
186
|
+
found.push(...await findTypescriptTranslatable(code));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return found;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// src/vite.ts
|
|
193
|
+
var createI18nPlugin = (props) => {
|
|
194
|
+
let cache;
|
|
195
|
+
return [
|
|
196
|
+
{
|
|
197
|
+
name: "awsless/i18n",
|
|
198
|
+
enforce: "pre",
|
|
199
|
+
async buildStart() {
|
|
200
|
+
const cwd = process.cwd();
|
|
201
|
+
const originals = await findTranslatable(cwd);
|
|
202
|
+
cache = await loadCache(cwd);
|
|
203
|
+
removeUnusedTranslations(cache, originals, props.locales);
|
|
204
|
+
const newOriginals = findNewTranslations(cache, originals, props.locales);
|
|
205
|
+
if (newOriginals.length > 0) {
|
|
206
|
+
const translations = await props.translate(props.default ?? "en", newOriginals);
|
|
207
|
+
for (const item of translations) {
|
|
208
|
+
cache.set(item.original, item.locale, item.translation);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
await saveCache(cwd, cache);
|
|
212
|
+
},
|
|
213
|
+
transform(code) {
|
|
214
|
+
let replaced = false;
|
|
215
|
+
if (code.includes(`$t\``)) {
|
|
216
|
+
for (const item of cache.entries()) {
|
|
217
|
+
code = code.replaceAll(`$t\`${item.original}\``, (_, original) => {
|
|
218
|
+
replaced = true;
|
|
219
|
+
return `$t.get(\`${original}\`, {${props.locales.map((locale) => {
|
|
220
|
+
return `"${locale}":\`${cache.get(original, locale)}\``;
|
|
221
|
+
}).join(",")}})`;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (code.includes(`get(t)\``)) {
|
|
226
|
+
for (const item of cache.entries()) {
|
|
227
|
+
code = code.replaceAll(`get(t)\`${item.original}\``, (_, original) => {
|
|
228
|
+
replaced = true;
|
|
229
|
+
return `get(t).get(\`${original}\`, {${props.locales.map((locale) => {
|
|
230
|
+
return `"${locale}":\`${cache.get(original, locale)}\``;
|
|
231
|
+
}).join(",")}})`;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!replaced) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
code
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
];
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// src/translate/chat-gpt.ts
|
|
247
|
+
var import_openai = __toESM(require("openai"), 1);
|
|
248
|
+
var import_zod = require("openai/helpers/zod");
|
|
249
|
+
var import_zod2 = require("zod");
|
|
250
|
+
var chatgpt = (props) => {
|
|
251
|
+
const format = (0, import_zod.zodResponseFormat)(
|
|
252
|
+
import_zod2.z.object({
|
|
253
|
+
translations: import_zod2.z.object({
|
|
254
|
+
original: import_zod2.z.string(),
|
|
255
|
+
locale: import_zod2.z.string(),
|
|
256
|
+
translation: import_zod2.z.string()
|
|
257
|
+
}).array()
|
|
258
|
+
}),
|
|
259
|
+
"final_schema"
|
|
260
|
+
);
|
|
261
|
+
return async (originalLocale, list) => {
|
|
262
|
+
const client = new import_openai.default(props);
|
|
263
|
+
const response = await client.chat.completions.create({
|
|
264
|
+
model: "gpt-4o-2024-08-06",
|
|
265
|
+
max_tokens: 4095,
|
|
266
|
+
n: 1,
|
|
267
|
+
temperature: 1,
|
|
268
|
+
response_format: format,
|
|
269
|
+
messages: [
|
|
270
|
+
{ role: "system", content: "You are a helpful translator." },
|
|
271
|
+
{ role: "user", content: prompt(originalLocale, list, props?.rules) }
|
|
272
|
+
]
|
|
273
|
+
});
|
|
274
|
+
const json = response.choices[0]?.message.content;
|
|
275
|
+
if (!json) {
|
|
276
|
+
throw new Error("Invalid chat gpt response");
|
|
277
|
+
}
|
|
278
|
+
const data = JSON.parse(json);
|
|
279
|
+
return data.translations;
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
var prompt = (originalLocale, list, rules) => {
|
|
283
|
+
return `You have to translate the text inside the JSON file below from ${originalLocale} to the provided locale.
|
|
284
|
+
${rules?.join("\n") ?? ""}
|
|
285
|
+
|
|
286
|
+
JSON FILE:
|
|
287
|
+
${JSON.stringify(list)}`;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// src/translate/mock.ts
|
|
291
|
+
var mock = (translation = "REPLACED") => {
|
|
292
|
+
return (_, list) => {
|
|
293
|
+
const response = [];
|
|
294
|
+
for (const item of list) {
|
|
295
|
+
response.push({ ...item, translation });
|
|
296
|
+
}
|
|
297
|
+
return response;
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
301
|
+
0 && (module.exports = {
|
|
302
|
+
chatgpt,
|
|
303
|
+
i18n,
|
|
304
|
+
mock
|
|
305
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
import { ClientOptions } from 'openai';
|
|
3
|
+
|
|
4
|
+
type Translator = (defaultLocale: string, list: {
|
|
5
|
+
original: string;
|
|
6
|
+
locale: string;
|
|
7
|
+
}[]) => TranslationResponse[] | Promise<TranslationResponse[]>;
|
|
8
|
+
type TranslationResponse = {
|
|
9
|
+
original: string;
|
|
10
|
+
locale: string;
|
|
11
|
+
translation: string;
|
|
12
|
+
};
|
|
13
|
+
type I18nPluginProps = {
|
|
14
|
+
/** The default locale that your original text is writen in. */
|
|
15
|
+
default?: string;
|
|
16
|
+
/** A list of locales that you want your text translated too. */
|
|
17
|
+
locales: string[];
|
|
18
|
+
/** The callback that is responsible for translating the text. */
|
|
19
|
+
translate: (defaultLocale: string, list: {
|
|
20
|
+
original: string;
|
|
21
|
+
locale: string;
|
|
22
|
+
}[]) => TranslationResponse[] | Promise<TranslationResponse[]>;
|
|
23
|
+
};
|
|
24
|
+
declare const createI18nPlugin: (props: I18nPluginProps) => Plugin[];
|
|
25
|
+
|
|
26
|
+
declare const chatgpt: (props?: ClientOptions & {
|
|
27
|
+
rules?: string[];
|
|
28
|
+
}) => Translator;
|
|
29
|
+
|
|
30
|
+
declare const mock: (translation?: string) => Translator;
|
|
31
|
+
|
|
32
|
+
export { chatgpt, createI18nPlugin as i18n, mock };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
import { ClientOptions } from 'openai';
|
|
3
|
+
|
|
4
|
+
type Translator = (defaultLocale: string, list: {
|
|
5
|
+
original: string;
|
|
6
|
+
locale: string;
|
|
7
|
+
}[]) => TranslationResponse[] | Promise<TranslationResponse[]>;
|
|
8
|
+
type TranslationResponse = {
|
|
9
|
+
original: string;
|
|
10
|
+
locale: string;
|
|
11
|
+
translation: string;
|
|
12
|
+
};
|
|
13
|
+
type I18nPluginProps = {
|
|
14
|
+
/** The default locale that your original text is writen in. */
|
|
15
|
+
default?: string;
|
|
16
|
+
/** A list of locales that you want your text translated too. */
|
|
17
|
+
locales: string[];
|
|
18
|
+
/** The callback that is responsible for translating the text. */
|
|
19
|
+
translate: (defaultLocale: string, list: {
|
|
20
|
+
original: string;
|
|
21
|
+
locale: string;
|
|
22
|
+
}[]) => TranslationResponse[] | Promise<TranslationResponse[]>;
|
|
23
|
+
};
|
|
24
|
+
declare const createI18nPlugin: (props: I18nPluginProps) => Plugin[];
|
|
25
|
+
|
|
26
|
+
declare const chatgpt: (props?: ClientOptions & {
|
|
27
|
+
rules?: string[];
|
|
28
|
+
}) => Translator;
|
|
29
|
+
|
|
30
|
+
declare const mock: (translation?: string) => Translator;
|
|
31
|
+
|
|
32
|
+
export { chatgpt, createI18nPlugin as i18n, mock };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// src/cache.ts
|
|
2
|
+
import { readFile, writeFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
var loadCache = async (cwd) => {
|
|
5
|
+
try {
|
|
6
|
+
const data = await readFile(join(cwd, "i18n.json"), "utf8");
|
|
7
|
+
return new Cache(JSON.parse(data));
|
|
8
|
+
} catch (error) {
|
|
9
|
+
return new Cache();
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var saveCache = async (cwd, cache) => {
|
|
13
|
+
await writeFile(join(cwd, "i18n.json"), JSON.stringify(cache, void 0, 2));
|
|
14
|
+
};
|
|
15
|
+
var Cache = class {
|
|
16
|
+
constructor(data = {}) {
|
|
17
|
+
this.data = data;
|
|
18
|
+
}
|
|
19
|
+
set(original, locale, translation) {
|
|
20
|
+
if (!this.data[original]) {
|
|
21
|
+
this.data[original] = {};
|
|
22
|
+
}
|
|
23
|
+
if (!this.data[original][locale]) {
|
|
24
|
+
this.data[original][locale] = translation;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
get(original, locale) {
|
|
28
|
+
return this.data[original]?.[locale];
|
|
29
|
+
}
|
|
30
|
+
has(original, locale) {
|
|
31
|
+
return typeof this.get(original, locale) === "string";
|
|
32
|
+
}
|
|
33
|
+
delete(original, locale) {
|
|
34
|
+
if (this.data[original]?.[locale]) {
|
|
35
|
+
delete this.data[original][locale];
|
|
36
|
+
}
|
|
37
|
+
if (this.data[original] && Object.keys(this.data[original]).length === 0) {
|
|
38
|
+
delete this.data[original];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
*entries() {
|
|
42
|
+
for (const [original, locales] of Object.entries(this.data)) {
|
|
43
|
+
for (const [locale, translation] of Object.entries(locales)) {
|
|
44
|
+
yield { original, locale, translation };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
toJSON() {
|
|
49
|
+
return this.data;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/diff.ts
|
|
54
|
+
var findNewTranslations = (cache, originals, locales) => {
|
|
55
|
+
const list = [];
|
|
56
|
+
for (const original of originals) {
|
|
57
|
+
for (const locale of locales) {
|
|
58
|
+
if (!cache.has(original, locale)) {
|
|
59
|
+
list.push({ original, locale });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return list;
|
|
64
|
+
};
|
|
65
|
+
var removeUnusedTranslations = (cache, originals, locales) => {
|
|
66
|
+
for (const item of cache.entries()) {
|
|
67
|
+
if (!locales.includes(item.locale) || !originals.includes(item.original)) {
|
|
68
|
+
cache.delete(item.original, item.locale);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/find.ts
|
|
74
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
75
|
+
import { glob } from "glob";
|
|
76
|
+
import { extname, join as join2 } from "path";
|
|
77
|
+
|
|
78
|
+
// src/find/svelte.ts
|
|
79
|
+
import lineColumn from "line-column";
|
|
80
|
+
import { parse as parseSvelte, walk } from "svelte/compiler";
|
|
81
|
+
var findSvelteTranslatable = (code) => {
|
|
82
|
+
const found = [];
|
|
83
|
+
const origin = lineColumn(code);
|
|
84
|
+
const ast = parseSvelte(code, {
|
|
85
|
+
css: false
|
|
86
|
+
});
|
|
87
|
+
const enter = (node) => {
|
|
88
|
+
if (
|
|
89
|
+
//
|
|
90
|
+
node.type === "TaggedTemplateExpression" && node.tag.type === "Identifier" && node.tag.name === "$t" && node.quasi.type === "TemplateLiteral" && node.quasi.loc
|
|
91
|
+
) {
|
|
92
|
+
const start = node.quasi.loc.start;
|
|
93
|
+
const end = node.quasi.loc.end;
|
|
94
|
+
const content = code.substring(
|
|
95
|
+
origin.toIndex(start.line, start.column) + 2,
|
|
96
|
+
origin.toIndex(end.line, end.column)
|
|
97
|
+
);
|
|
98
|
+
found.push(content);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
walk(ast.html, { enter });
|
|
102
|
+
if (ast.instance) {
|
|
103
|
+
walk(ast.instance.content, { enter });
|
|
104
|
+
}
|
|
105
|
+
if (ast.module) {
|
|
106
|
+
walk(ast.module.content, { enter });
|
|
107
|
+
}
|
|
108
|
+
return found;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/find/typescript.ts
|
|
112
|
+
import { parse } from "@swc/core";
|
|
113
|
+
import { simple } from "swc-walk";
|
|
114
|
+
var findTypescriptTranslatable = async (code) => {
|
|
115
|
+
const found = [];
|
|
116
|
+
const ast = await parse(code, {
|
|
117
|
+
syntax: "typescript"
|
|
118
|
+
});
|
|
119
|
+
simple(ast, {
|
|
120
|
+
TaggedTemplateExpression(node) {
|
|
121
|
+
if (node.tag.type === "CallExpression" && node.tag.callee.type === "Identifier" && node.tag.callee.value === "get" && node.template.type === "TemplateLiteral") {
|
|
122
|
+
const content = code.substring(
|
|
123
|
+
node.template.span.start - ast.span.start + 1,
|
|
124
|
+
node.template.span.end - ast.span.start - 1
|
|
125
|
+
);
|
|
126
|
+
found.push(content);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return found;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/find.ts
|
|
134
|
+
var findTranslatable = async (cwd) => {
|
|
135
|
+
const files = await glob("**/*.{js,ts,svelte}", {
|
|
136
|
+
cwd,
|
|
137
|
+
ignore: ["**/node_modules"]
|
|
138
|
+
});
|
|
139
|
+
const found = [];
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
const code = await readFile2(join2(cwd, file), "utf8");
|
|
142
|
+
if (code.includes("$t`")) {
|
|
143
|
+
if (extname(file) === ".svelte") {
|
|
144
|
+
found.push(...findSvelteTranslatable(code));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (code.includes("get(t)`")) {
|
|
148
|
+
found.push(...await findTypescriptTranslatable(code));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return found;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// src/vite.ts
|
|
155
|
+
var createI18nPlugin = (props) => {
|
|
156
|
+
let cache;
|
|
157
|
+
return [
|
|
158
|
+
{
|
|
159
|
+
name: "awsless/i18n",
|
|
160
|
+
enforce: "pre",
|
|
161
|
+
async buildStart() {
|
|
162
|
+
const cwd = process.cwd();
|
|
163
|
+
const originals = await findTranslatable(cwd);
|
|
164
|
+
cache = await loadCache(cwd);
|
|
165
|
+
removeUnusedTranslations(cache, originals, props.locales);
|
|
166
|
+
const newOriginals = findNewTranslations(cache, originals, props.locales);
|
|
167
|
+
if (newOriginals.length > 0) {
|
|
168
|
+
const translations = await props.translate(props.default ?? "en", newOriginals);
|
|
169
|
+
for (const item of translations) {
|
|
170
|
+
cache.set(item.original, item.locale, item.translation);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
await saveCache(cwd, cache);
|
|
174
|
+
},
|
|
175
|
+
transform(code) {
|
|
176
|
+
let replaced = false;
|
|
177
|
+
if (code.includes(`$t\``)) {
|
|
178
|
+
for (const item of cache.entries()) {
|
|
179
|
+
code = code.replaceAll(`$t\`${item.original}\``, (_, original) => {
|
|
180
|
+
replaced = true;
|
|
181
|
+
return `$t.get(\`${original}\`, {${props.locales.map((locale) => {
|
|
182
|
+
return `"${locale}":\`${cache.get(original, locale)}\``;
|
|
183
|
+
}).join(",")}})`;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (code.includes(`get(t)\``)) {
|
|
188
|
+
for (const item of cache.entries()) {
|
|
189
|
+
code = code.replaceAll(`get(t)\`${item.original}\``, (_, original) => {
|
|
190
|
+
replaced = true;
|
|
191
|
+
return `get(t).get(\`${original}\`, {${props.locales.map((locale) => {
|
|
192
|
+
return `"${locale}":\`${cache.get(original, locale)}\``;
|
|
193
|
+
}).join(",")}})`;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!replaced) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
code
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
];
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// src/translate/chat-gpt.ts
|
|
209
|
+
import OpenAI from "openai";
|
|
210
|
+
import { zodResponseFormat } from "openai/helpers/zod";
|
|
211
|
+
import { z } from "zod";
|
|
212
|
+
var chatgpt = (props) => {
|
|
213
|
+
const format = zodResponseFormat(
|
|
214
|
+
z.object({
|
|
215
|
+
translations: z.object({
|
|
216
|
+
original: z.string(),
|
|
217
|
+
locale: z.string(),
|
|
218
|
+
translation: z.string()
|
|
219
|
+
}).array()
|
|
220
|
+
}),
|
|
221
|
+
"final_schema"
|
|
222
|
+
);
|
|
223
|
+
return async (originalLocale, list) => {
|
|
224
|
+
const client = new OpenAI(props);
|
|
225
|
+
const response = await client.chat.completions.create({
|
|
226
|
+
model: "gpt-4o-2024-08-06",
|
|
227
|
+
max_tokens: 4095,
|
|
228
|
+
n: 1,
|
|
229
|
+
temperature: 1,
|
|
230
|
+
response_format: format,
|
|
231
|
+
messages: [
|
|
232
|
+
{ role: "system", content: "You are a helpful translator." },
|
|
233
|
+
{ role: "user", content: prompt(originalLocale, list, props?.rules) }
|
|
234
|
+
]
|
|
235
|
+
});
|
|
236
|
+
const json = response.choices[0]?.message.content;
|
|
237
|
+
if (!json) {
|
|
238
|
+
throw new Error("Invalid chat gpt response");
|
|
239
|
+
}
|
|
240
|
+
const data = JSON.parse(json);
|
|
241
|
+
return data.translations;
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
var prompt = (originalLocale, list, rules) => {
|
|
245
|
+
return `You have to translate the text inside the JSON file below from ${originalLocale} to the provided locale.
|
|
246
|
+
${rules?.join("\n") ?? ""}
|
|
247
|
+
|
|
248
|
+
JSON FILE:
|
|
249
|
+
${JSON.stringify(list)}`;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// src/translate/mock.ts
|
|
253
|
+
var mock = (translation = "REPLACED") => {
|
|
254
|
+
return (_, list) => {
|
|
255
|
+
const response = [];
|
|
256
|
+
for (const item of list) {
|
|
257
|
+
response.push({ ...item, translation });
|
|
258
|
+
}
|
|
259
|
+
return response;
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
export {
|
|
263
|
+
chatgpt,
|
|
264
|
+
createI18nPlugin as i18n,
|
|
265
|
+
mock
|
|
266
|
+
};
|
package/dist/svelte.cjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/framework/svelte.ts
|
|
21
|
+
var svelte_exports = {};
|
|
22
|
+
__export(svelte_exports, {
|
|
23
|
+
locale: () => locale,
|
|
24
|
+
t: () => t
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(svelte_exports);
|
|
27
|
+
var import_store = require("svelte/store");
|
|
28
|
+
var locale = (0, import_store.writable)("en");
|
|
29
|
+
var t = (0, import_store.derived)([locale], ([locale2]) => {
|
|
30
|
+
const api = (template, ...args) => {
|
|
31
|
+
return String.raw({ raw: template.raw }, ...args);
|
|
32
|
+
};
|
|
33
|
+
api.get = (og, translations) => {
|
|
34
|
+
return translations[locale2] ?? og;
|
|
35
|
+
};
|
|
36
|
+
return api;
|
|
37
|
+
});
|
|
38
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
39
|
+
0 && (module.exports = {
|
|
40
|
+
locale,
|
|
41
|
+
t
|
|
42
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as svelte_store from 'svelte/store';
|
|
2
|
+
|
|
3
|
+
declare const locale: svelte_store.Writable<string>;
|
|
4
|
+
declare const t: svelte_store.Readable<{
|
|
5
|
+
(template: TemplateStringsArray, ...args: Array<string | number>): string;
|
|
6
|
+
get(og: string, translations: Record<string, string>): string;
|
|
7
|
+
}>;
|
|
8
|
+
|
|
9
|
+
export { locale, t };
|
package/dist/svelte.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as svelte_store from 'svelte/store';
|
|
2
|
+
|
|
3
|
+
declare const locale: svelte_store.Writable<string>;
|
|
4
|
+
declare const t: svelte_store.Readable<{
|
|
5
|
+
(template: TemplateStringsArray, ...args: Array<string | number>): string;
|
|
6
|
+
get(og: string, translations: Record<string, string>): string;
|
|
7
|
+
}>;
|
|
8
|
+
|
|
9
|
+
export { locale, t };
|
package/dist/svelte.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/framework/svelte.ts
|
|
2
|
+
import { derived, writable } from "svelte/store";
|
|
3
|
+
var locale = writable("en");
|
|
4
|
+
var t = derived([locale], ([locale2]) => {
|
|
5
|
+
const api = (template, ...args) => {
|
|
6
|
+
return String.raw({ raw: template.raw }, ...args);
|
|
7
|
+
};
|
|
8
|
+
api.get = (og, translations) => {
|
|
9
|
+
return translations[locale2] ?? og;
|
|
10
|
+
};
|
|
11
|
+
return api;
|
|
12
|
+
});
|
|
13
|
+
export {
|
|
14
|
+
locale,
|
|
15
|
+
t
|
|
16
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@awsless/i18n",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/awsless/awsless.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/awsless/awsless/issues"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"main": "./dist/index.cjs",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"require": "./dist/index.cjs",
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts"
|
|
24
|
+
},
|
|
25
|
+
"./svelte": {
|
|
26
|
+
"require": "./dist/svelte.cjs",
|
|
27
|
+
"import": "./dist/svelte.js",
|
|
28
|
+
"types": "./dist/svelte.d.ts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"vitest": {
|
|
32
|
+
"exclude": [
|
|
33
|
+
"test/_site"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
|
38
|
+
"svelte": "^4.2.19",
|
|
39
|
+
"svelte-preprocess": "^6.0.2",
|
|
40
|
+
"vite": "^5.4.2"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"svelte": "^4.2.19",
|
|
44
|
+
"vite": "^5.4.2"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@swc/core": "^1.3.70",
|
|
48
|
+
"@types/line-column": "^1.0.2",
|
|
49
|
+
"glob": "^10.3.9",
|
|
50
|
+
"line-column": "^1.0.2",
|
|
51
|
+
"openai": "^4.57.0",
|
|
52
|
+
"swc-walk": "1.0.0-rc.2",
|
|
53
|
+
"zod": "^3.21.4"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"test": "pnpm code test",
|
|
57
|
+
"build": "pnpm tsup src/index.ts --format cjs,esm --dts --clean",
|
|
58
|
+
"build-svelte": "pnpm tsup src/framework/svelte.ts --format cjs,esm --dts",
|
|
59
|
+
"prepublish": "if pnpm test; then pnpm build; pnpm build-svelte; else exit; fi"
|
|
60
|
+
}
|
|
61
|
+
}
|