wuchale 0.4.2 → 0.5.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/README.md CHANGED
@@ -50,9 +50,9 @@ Messages are compiled into arrays with index-based lookups. Runtime only
50
50
  concatenates and renders — no regex, replace, or complex logic. And the
51
51
  compiled bundles are as small as possible, they don't even have keys.
52
52
 
53
- ### 🔁 HMR & Dev Translations Live updates during development.
53
+ ### 🔁 Optional HMR & Dev Translations Live updates during development.
54
54
 
55
- Translation files and source changes trigger updates instantly — including
55
+ Translation files and source changes can trigger updates instantly — including
56
56
  optional Gemini-based auto-translation. This means you can write the code in
57
57
  English and have it shown in another language in the browser while in dev mode.
58
58
 
@@ -69,12 +69,12 @@ some existing solutions.
69
69
 
70
70
  ### ✨ Ready for Svelte 5
71
71
 
72
- Works with Svelte 5's new architecture and snippets. Future-proof and tightly
73
- integrated
72
+ Works with Svelte 5's new architecture with runes and snippets. Future-proof
73
+ and tightly integrated
74
74
 
75
75
  ## 🚀 Getting Started
76
76
 
77
- Install:
77
+ ### Installation
78
78
 
79
79
  ```bash
80
80
  npm install wuchale
@@ -97,80 +97,138 @@ export default {
97
97
 
98
98
  ```
99
99
 
100
- Create `/src/locales/` (relative to the projects root) if it doesn't exist, and
101
- then set it up in your main component. Assuming `/src/App.svelte`:
100
+ ### Configuration
102
101
 
103
- ```svelte
104
- <script>
105
- import {setTranslations} from 'wuchale/runtime.svelte.js'
102
+ To configure `wuchale`, you can do one of:
106
103
 
107
- let locale = $state('en')
104
+ - Pass an object `wuchale()` in your `vite.config.js` `vite.config.ts`
105
+ - Create a `wuchale.config.js` file that exports the config object as `default` in your project root directory.
108
106
 
109
- $effect.pre(() => {
110
- // IMPORTANT! The path should be relative to the current file (vite restriction).
111
- // for the default sveltekit template for example, it's `../locales/${locale}.json`
112
- // because the default main component is located at /src/routes/+page.svelte
113
- import(`./locales/${locale}.json`).then(mod => {
114
- setTranslations(mod.default)
115
- })
116
- // but this only applies if you want to do lazy loading.
117
- // Otherwise you can do an absolute import
118
- })
119
- </script>
107
+ But the latter is recommended as it is also read when using the CLI command to extract items.
108
+
109
+ The config object should look like the following (the default):
110
+
111
+ ```javascript
112
+ export const defaultOptions: Config = {
113
+ sourceLocale: 'en',
114
+ otherLocales: [],
115
+ localesDir: './src/locales',
116
+ srcDirs: ['src'],
117
+ heuristic: defaultHeuristic,
118
+ hmr: true,
119
+ geminiAPIKey: 'env',
120
+ }
120
121
  ```
121
122
 
122
- Note that you manage the state of which locale is active and how to download
123
- the compiled `.json`. This is to allow maximum flexibility, meaning you can use
124
- lazy loading (like this example) or you can import it directly and it will be
125
- bundled by Vite. After that, you notify `wuchale` to set it as the current one.
123
+ Note that you have to provide `otherLocales`, otherwise it doesn't have any
124
+ effect.
126
125
 
127
- Then finally you write your Svelte files naturally:
126
+ While the others are self explanatory, the `heuristic` is a function that
127
+ globally decides what text to extract and what not to. The `defaultHeuristic`
128
+ is the implementation of the default rules explained below, but you can roll
129
+ your own and provide it here. The function should receive the following
130
+ arguments:
128
131
 
129
- ```svelte
132
+ - `text`: The candidate text
133
+ - `scope`: Where the text is located, i.e. it can be one of `markup`, `script`, and `attribute`
130
134
 
131
- <!-- you write -->
132
- <p>Hello</p>
135
+ And it should return boolean to indicate whether to extract it.
133
136
 
134
- <!-- it becomes -->
137
+ The `hmr` can be used to turn off the live updates during dev. It will disable
138
+ all text extraction from source files and modification of the `.po` files as
139
+ you modify the source files. This may be desired because the language files are
140
+ imported by the top components and frequent modification of the language files
141
+ can trigger big updates of the DOM, which may cause states not depending on the
142
+ URL to be lost. If you choose to disable the `hmr` extraction, you can still
143
+ extract (and translate with Gemini) using the CLI command.
135
144
 
136
- <script>
137
- import WuchaleTrans, { wuchaleTrans } from 'wuchale/runtime.svelte'
138
- </script>
139
- <p>{wuchaleTrans(0)}</p> <!-- Extracted "Hello" as index 0 -->
145
+ ### Register CLI
146
+
147
+ You must add entries to run the CLI from `npm`.
148
+
149
+ ```javascript
150
+ // package.json
151
+ {
152
+ // ...
153
+ "scripts": {
154
+ // ...
155
+ "extract": "wuchale",
156
+ "clean": "wuchale --clean"
157
+ },
158
+ // ...
159
+ }
140
160
  ```
141
161
 
142
- Full example below.
162
+ You can read more about the CLI below
143
163
 
144
- ## 📦 How It Works
164
+ ### Setup
145
165
 
146
- ### Process
166
+ Create `/src/locales/` (or the directory you set up in the configuration,
167
+ relative to the projects root) if it doesn't exist, and then set it up in your
168
+ main component. How you set it up depends on whether you use SvelteKit or not.
147
169
 
148
- ![Diagram](https://raw.githubusercontent.com/K1DV5/wuchale/main/images/diagram.svg)
170
+ #### SvelteKit
149
171
 
150
- 1. All text nodes are extracted using AST traversal.
151
- 1. Replaced with index-based lookups `wuchaleTrans(n)`, which is minifiable for production builds.
152
- 1. Catalog is updated
153
- 1. If Gemini integration is enabled, it fetches translations automatically.
154
- 1. Messages are compiled and written
155
- 1. In dev mode: Vite picks the write and does HMR during dev
156
- 1. In production mode: unused messages are marked obsolete
157
- 1. On next run, obsolete ones are purged unless reused.
158
- 1. Final build contains only minified catalogs and the runtime.
172
+ If you use SvelteKit you likely use SSR/SSG too. And this is how you make
173
+ `wuchale` work with SSR/SSG. If you instead want to use normal client side
174
+ state for the locale, feel free to adapt the method explained below for Svelte.
159
175
 
160
- ### Catalogs:
176
+ You have to put the following in yout top load function. Taking
177
+ the default template as an example, the main load function would be inside
178
+ `src/routes/+page.js`. Have the following content in it (TypeScript):
161
179
 
162
- - Stored as PO files (.po).
163
- - Compatible with tools like Poedit or translation.io.
164
- - Includes source references and obsolete flags for cleaning.
180
+ ```typescript
181
+ import { setTranslations } from 'wuchale/runtime.svelte.js'
165
182
 
166
- ## 🌐 Gemini Integration (optional)
183
+ interface Params {
184
+ url: URL,
185
+ }
167
186
 
168
- To enable the Gemini live translation, set the environment variable
169
- `GEMINI_API_KEY` to your API key beforehand. The integration is:
187
+ export async function load({ url }: Params): Promise<{}> {
188
+ const locale = url.searchParams.get('locale') ?? 'es'
189
+ // IMPORTANT! The path should be relative to the current file
190
+ const mod = await import(`../locales/${locale}.js`)
191
+ setTranslations(mod.default)
192
+ return {}
193
+ }
194
+ ```
170
195
 
171
- - Rate-limit friendly (bundles messages to be translated into one request)
172
- - Only translates new/changed messages
173
- - Keeps original source intact
196
+ What it does is it makes the locale dependent on the URL search param `locale`
197
+ and loads the appropriate language js based on it. You can now adapt it to
198
+ any state such as the `[locale]` slug in the URL path based on your own needs.
199
+
200
+ Now you can start the dev server and see it in action. You can change the URL
201
+ search params like `?locale=es` (if you set up `es` in `otherLocales`)
202
+
203
+ #### Svelte
204
+
205
+ For Svelte you can set up lazy loading and code splitting (recommended). Taking
206
+ the default template as an example, the main component is located in
207
+ `src/App.svelte`.
208
+
209
+ ```svelte
210
+ <script>
211
+ import {setTranslations} from 'wuchale/runtime.svelte.js'
212
+
213
+ let locale = $state('en')
214
+
215
+ $effect.pre(() => {
216
+ // IMPORTANT! The path should be relative to the current file (vite restriction).
217
+ import(`../locales/${locale}.js`).then(mod => {
218
+ setTranslations(mod.default)
219
+ })
220
+ // but this only applies if you want to do lazy loading.
221
+ // Otherwise you can do an absolute import
222
+ })
223
+ </script>
224
+ ```
225
+
226
+ Note that you manage the state of which locale is active and how to download
227
+ the compiled `.js`. This is to allow maximum flexibility, meaning you can use
228
+ lazy loading (like this example) or you can import it directly and it will be
229
+ bundled by Vite. After that, you notify `wuchale` to set it as the current one.
230
+
231
+ Then finally you write your Svelte files naturally.
174
232
 
175
233
  ## 🧪 Example
176
234
 
@@ -188,7 +246,8 @@ Output:
188
246
 
189
247
  ```svelte
190
248
  <script>
191
- import WuchaleTrans, { wuchaleTrans } from 'wuchale/runtime.svelte'
249
+ import {wuchaleTrans} from "wuchale/runtime.svelte.js"
250
+ import WuchaleTrans from "wuchale/runtime.svelte"
192
251
  </script>
193
252
 
194
253
  <p>{wuchaleTrans(0)}</p>
@@ -228,8 +287,8 @@ msgstr "Hola {0}"
228
287
 
229
288
  Which is then automatically compiled to:
230
289
 
231
- ```json
232
- [
290
+ ```javascript
291
+ export default [
233
292
  "¡Hola!",
234
293
  [
235
294
  "Hola ",
@@ -241,7 +300,36 @@ Which is then automatically compiled to:
241
300
  ]
242
301
  ```
243
302
 
244
- This is what you import when you set it up above in `App.svelte`.
303
+ This is what is included in the imported module above in `+page.ts` or `App.svelte`.
304
+
305
+ ## 📦 How It Works
306
+
307
+ ### Process
308
+
309
+ ![Diagram](https://raw.githubusercontent.com/K1DV5/wuchale/main/images/diagram.svg)
310
+
311
+ 1. All text nodes are extracted using AST traversal.
312
+ 1. Replaced with index-based lookups `wuchaleTrans(n)`, which is minifiable for production builds.
313
+ 1. Catalog is updated
314
+ 1. If Gemini integration is enabled, it fetches translations automatically.
315
+ 1. Messages are compiled
316
+ 1. In dev mode: Vite picks the write and does HMR during dev
317
+ 1. Final build contains only minified catalogs and the runtime.
318
+
319
+ ### Catalogs:
320
+
321
+ - Stored as PO files (.po).
322
+ - Compatible with tools like Poedit or translation.io.
323
+ - Includes source references and obsolete flags for cleaning.
324
+
325
+ ## 🌐 Gemini Integration (optional)
326
+
327
+ To enable the Gemini live translation, set the environment variable
328
+ `GEMINI_API_KEY` to your API key beforehand. The integration is:
329
+
330
+ - Rate-limit friendly (bundles messages to be translated into one request)
331
+ - Only translates new/changed messages
332
+ - Keeps original source intact
245
333
 
246
334
  ## Supported syntax
247
335
 
@@ -343,30 +431,13 @@ necessary to have a separate plurals support because you can do something like:
343
431
  And they will be extracted separately. You can also make a reusable function
344
432
  yourself.
345
433
 
346
- ## Configuration
347
-
348
- To configure `wuchale`, you pass an object that looks like the following (the
349
- default) to `wuchale()` in your `vite.config.js` `vite.config.ts`:
350
-
351
- ```javascript
352
- export const defaultOptions: Options = {
353
- sourceLocale: 'en',
354
- otherLocales: [],
355
- localesDir: './src/locales',
356
- heuristic: defaultHeuristic,
357
- geminiAPIKey: 'env',
358
- }
359
- ```
434
+ ## CLI
360
435
 
361
- While the others are self explanatory, the `heuristic` is a function that
362
- globally decides what text to extract and what not to. The `defaultHeuristic`
363
- is the implementation of the above rules, but you can roll your own and provide
364
- it here. The function should receive the following arguments:
365
-
366
- - `text`: The candidate text
367
- - `scope`: Where the text is located, i.e. it can be one of `markup`, `script`, and `attribute`
436
+ A simple command line interface is also provided, just `wuchale` with an optional `--clean` argument.
368
437
 
369
- And it should return boolean to indicate whether to extract it.
438
+ By default, it looks inside all svelte sources and extracts new texts into the
439
+ `.po` files. If the `--clean` argument is provided, it additionally removes
440
+ unused texts from the `.po` files.
370
441
 
371
442
  ## Files management
372
443
 
@@ -383,17 +454,12 @@ This is a `gettext` file that is used to exchange the text fragments with transl
383
454
  **Note**: You should commit these files, they are where the translated
384
455
  fragments are stored and are the source of truth for the translations.
385
456
 
386
- ### Compiled `.json` file
387
-
388
- This is the compiled version of the `.po` file. It is generated at the startup
389
- of the dev server and at build time. As it has to be generated every time, you
390
- should consider it as a temp file and therefore you should **not** commit it.
391
-
392
- ## 🧹 Cleaning
457
+ ### Compiled data in `.js` file
393
458
 
394
- Unused keys are marked as obsolete during a production build. Obsoletes are
395
- purged on the next run (build or dev). Essentially this means cleaning needs
396
- two passes. This is because of how vite/rollup works.
459
+ This is the module that imports the compiled version of the `.po` file. It is
460
+ generated at the startup of the dev server and at build time. As can be
461
+ generated every time, you should consider it as a temp file and therefore you
462
+ should not commit it.
397
463
 
398
464
  ## 🧪 Tests
399
465
 
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import createPlugin from "./plugin/index.js";
3
+ let clean = false;
4
+ if (process.argv[2] === '--clean') {
5
+ clean = true;
6
+ }
7
+ const plugin = await createPlugin();
8
+ await plugin.configResolved({ env: { EXTRACT: true }, root: process.cwd() });
9
+ console.log('Extracting...');
10
+ if (clean) {
11
+ for (const loc of plugin.locales) {
12
+ for (const item of Object.values(plugin._translations[loc])) {
13
+ item.references = [];
14
+ }
15
+ }
16
+ }
17
+ await plugin.directExtract();
18
+ if (clean) {
19
+ for (const loc of plugin.locales) {
20
+ for (const [key, item] of Object.entries(plugin._translations[loc])) {
21
+ if (item.references.length === 0) {
22
+ delete plugin._translations[loc][key];
23
+ }
24
+ }
25
+ plugin.afterExtract(loc);
26
+ }
27
+ }
28
+ console.log('Extraction finished.');
29
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,YAAY,MAAM,mBAAmB,CAAA;AAE5C,IAAI,KAAK,GAAG,KAAK,CAAA;AACjB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;IAChC,KAAK,GAAG,IAAI,CAAA;AAChB,CAAC;AAED,MAAM,MAAM,GAAG,MAAM,YAAY,EAAE,CAAA;AACnC,MAAM,MAAM,CAAC,cAAc,CAAC,EAAC,GAAG,EAAE,EAAC,OAAO,EAAE,IAAI,EAAC,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,EAAE,EAAC,CAAC,CAAA;AAExE,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAA;AAE5B,IAAI,KAAK,EAAE,CAAC;IACR,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QAC/B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YAC1D,IAAI,CAAC,UAAU,GAAG,EAAE,CAAA;QACxB,CAAC;IACL,CAAC;AACL,CAAC;AAED,MAAM,MAAM,CAAC,aAAa,EAAE,CAAA;AAE5B,IAAI,KAAK,EAAE,CAAC;IACR,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QAC/B,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YAClE,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC/B,OAAO,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;YACzC,CAAC;QACL,CAAC;QACD,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAA;IAC5B,CAAC;AACL,CAAC;AAED,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAA"}
@@ -0,0 +1,12 @@
1
+ import { type HeuristicFunc } from "./plugin/prep.js";
2
+ export interface Config {
3
+ sourceLocale?: string;
4
+ otherLocales?: string[];
5
+ localesDir?: string;
6
+ srcDirs?: string[];
7
+ heuristic?: HeuristicFunc;
8
+ hmr?: boolean;
9
+ geminiAPIKey?: string;
10
+ }
11
+ export declare const defaultOptions: Config;
12
+ export declare function getOptions(codeOptions?: Config): Promise<Config>;
package/dist/config.js ADDED
@@ -0,0 +1,26 @@
1
+ import { defaultHeuristic } from "./plugin/prep.js";
2
+ export const defaultOptions = {
3
+ sourceLocale: 'en',
4
+ otherLocales: [],
5
+ localesDir: './src/locales',
6
+ srcDirs: ['src'],
7
+ heuristic: defaultHeuristic,
8
+ hmr: true,
9
+ geminiAPIKey: 'env',
10
+ };
11
+ function mergeOptions(fromOpt, toOpt) {
12
+ for (const key of Object.keys(fromOpt)) {
13
+ toOpt[key] = fromOpt[key];
14
+ }
15
+ }
16
+ export async function getOptions(codeOptions = {}) {
17
+ const options = defaultOptions;
18
+ try {
19
+ const module = await import(process.cwd() + '/wuchale.config.js');
20
+ mergeOptions(module.default, options);
21
+ }
22
+ catch { }
23
+ mergeOptions(codeOptions, options);
24
+ return options;
25
+ }
26
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAsB,MAAM,kBAAkB,CAAA;AAYvE,MAAM,CAAC,MAAM,cAAc,GAAW;IAClC,YAAY,EAAE,IAAI;IAClB,YAAY,EAAE,EAAE;IAChB,UAAU,EAAE,eAAe;IAC3B,OAAO,EAAE,CAAC,KAAK,CAAC;IAChB,SAAS,EAAE,gBAAgB;IAC3B,GAAG,EAAE,IAAI;IACT,YAAY,EAAE,KAAK;CACtB,CAAA;AAED,SAAS,YAAY,CAAC,OAAe,EAAE,KAAa;IAChD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACrC,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;IAC7B,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,cAAsB,EAAE;IACrD,MAAM,OAAO,GAAW,cAAc,CAAA;IACtC,IAAI,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,oBAAoB,CAAC,CAAA;QACjE,YAAY,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;IACzC,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IACV,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAA;IAClC,OAAO,OAAO,CAAA;AAClB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import plugin from "./preprocess/index.js";
1
+ import plugin from "./plugin/index.js";
2
2
  export declare const wuchale: typeof plugin;
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import plugin from "./preprocess/index.js";
1
+ import plugin from "./plugin/index.js";
2
2
  export const wuchale = plugin;
3
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,uBAAuB,CAAA;AAE1C,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,mBAAmB,CAAA;AAEtC,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAA"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile.js","sourceRoot":"","sources":["../../src/plugin/compile.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AAOvC,SAAS,gBAAgB,CAAC,IAAgG;IACtH,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACvB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACtB,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC1B,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YACjC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACvB,CAAC;QACD,OAAO,EAAE,CAAA;IACb,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;QAChC,OAAO,gBAAgB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IAC5C,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAC5B,OAAO,CAAC;gBACJ,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAC1B,GAAG,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC;aACrC,CAAC,CAAA;IACN,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,EAAE,CAAA;QAChB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAA;QAC1C,CAAC;QACD,OAAO,KAAK,CAAA;IAChB,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,IAAI,CAAC,CAAA;IAC3C,OAAO,EAAE,CAAA;AACb,CAAC;AAED,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,IAAY,EAAE,QAA0B;IAC/E,IAAI,CAAC,IAAI,EAAE,CAAC;QACR,OAAO,QAAQ,CAAA;IACnB,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7C,OAAO,IAAI,CAAA;IACf,CAAC;IACD,+CAA+C;IAC/C,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAA;IAC/C,IAAI,CAAC;QACD,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QACzC,OAAO,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACzC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAClB,OAAO,QAAQ,CAAA;IACnB,CAAC;AACL,CAAC"}
@@ -0,0 +1,28 @@
1
+ import PO from 'pofile';
2
+ export type ItemType = InstanceType<typeof PO.Item>;
3
+ export default class GeminiQueue {
4
+ batches: ItemType[][];
5
+ running: Promise<void> | null;
6
+ sourceLocale: string;
7
+ targetLocale: string;
8
+ url: string;
9
+ instruction: string;
10
+ onComplete: () => Promise<void>;
11
+ constructor(sourceLocale: string, targetLocale: string, apiKey: string | null, onComplete: () => Promise<void>);
12
+ prepareData(fragments: ItemType[]): {
13
+ system_instruction: {
14
+ parts: {
15
+ text: string;
16
+ }[];
17
+ };
18
+ contents: {
19
+ parts: {
20
+ text: string;
21
+ }[];
22
+ }[];
23
+ };
24
+ translate(fragments: ItemType[]): Promise<void>;
25
+ getBatches(): Generator<ItemType[], void, unknown>;
26
+ run(): Promise<void>;
27
+ add(items: ItemType[]): boolean;
28
+ }
@@ -0,0 +1,102 @@
1
+ // $$ cd .. && npm run test
2
+ // $$ node %f
3
+ import PO from 'pofile';
4
+ const baseURL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=';
5
+ const h = { 'Content-Type': 'application/json' };
6
+ function codeStandard(locale) {
7
+ return `ISO 639-${locale.length === 2 ? 1 : 3}`;
8
+ }
9
+ // implements a queue for a sequential translation useful for vite's transform during dev
10
+ // as vite can do async transform
11
+ export default class GeminiQueue {
12
+ batches = [];
13
+ running = null;
14
+ sourceLocale;
15
+ targetLocale;
16
+ url;
17
+ instruction;
18
+ onComplete;
19
+ constructor(sourceLocale, targetLocale, apiKey, onComplete) {
20
+ if (apiKey === 'env') {
21
+ apiKey = process.env.GEMINI_API_KEY;
22
+ }
23
+ if (!apiKey) {
24
+ return;
25
+ }
26
+ this.sourceLocale = sourceLocale;
27
+ this.targetLocale = targetLocale;
28
+ this.url = `${baseURL}${apiKey}`;
29
+ this.instruction = `
30
+ You will be given the contents of a gettext .po file for a web app.
31
+ Translate each of the items from the source to the target language.
32
+ The source language ${codeStandard(this.sourceLocale)} code is: ${this.sourceLocale}.
33
+ The target language ${codeStandard(this.targetLocale)} code is: ${this.targetLocale}.
34
+ You can read all of the information for the items including contexts,
35
+ comments and references to get the appropriate context about each item.
36
+ Provide the same content with the only difference being that the
37
+ empty msgstr quotes should be filled with the appropriate translations,
38
+ preserving all placeholders.
39
+ The placeholder format is like the following examples:
40
+ - {0}: means arbitrary values.
41
+ - <0>something</0>: means something enclosed in some tags, like HTML tags
42
+ - <0/>: means a self closing tag, like in HTML
43
+ In all of the examples, 0 is an example for any integer.
44
+ `;
45
+ this.onComplete = onComplete;
46
+ }
47
+ prepareData(fragments) {
48
+ const po = new PO();
49
+ po.items = fragments;
50
+ return {
51
+ system_instruction: {
52
+ parts: [{ text: this.instruction }]
53
+ },
54
+ contents: [{ parts: [{ text: po.toString() }] }]
55
+ };
56
+ }
57
+ async translate(fragments) {
58
+ const data = this.prepareData(fragments);
59
+ const res = await fetch(this.url, { method: 'POST', headers: h, body: JSON.stringify(data) });
60
+ const json = await res.json();
61
+ if (json.error) {
62
+ console.error('Gemini error', json.error.code, json.error.message);
63
+ return;
64
+ }
65
+ const resText = json.candidates[0]?.content.parts[0].text;
66
+ for (const [i, item] of PO.parse(resText).items.entries()) {
67
+ if (item.msgstr[0]) {
68
+ fragments[i].msgstr = item.msgstr;
69
+ }
70
+ }
71
+ }
72
+ *getBatches() {
73
+ while (this.batches.length > 0) {
74
+ yield this.batches.pop(); // order doesn't matter, because they are given by ref
75
+ }
76
+ }
77
+ async run() {
78
+ for (const batch of this.getBatches()) {
79
+ await this.translate(batch);
80
+ }
81
+ await this.onComplete();
82
+ this.running = null;
83
+ }
84
+ add(items) {
85
+ if (!this.url) {
86
+ return;
87
+ }
88
+ let newRequest = false;
89
+ if (this.batches.length > 0) {
90
+ this.batches[0].push(...items);
91
+ }
92
+ else {
93
+ this.batches.push(items);
94
+ newRequest = true;
95
+ }
96
+ if (!this.running) {
97
+ this.running = this.run();
98
+ }
99
+ return newRequest;
100
+ }
101
+ }
102
+ //# sourceMappingURL=gemini.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../src/plugin/gemini.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import { type CompiledFragment } from "./compile.js";
2
+ import { type ItemType } from "./gemini.js";
3
+ import { type Config as Config } from "../config.js";
4
+ type ViteHotUpdateCTX = {
5
+ file: string;
6
+ server: {
7
+ moduleGraph: {
8
+ getModuleById: Function;
9
+ invalidateModule: Function;
10
+ };
11
+ };
12
+ timestamp: number;
13
+ };
14
+ declare class Plugin {
15
+ #private;
16
+ name: string;
17
+ locales: string[];
18
+ _translations: {
19
+ [loc: string]: {
20
+ [key: string]: ItemType;
21
+ };
22
+ };
23
+ _compiled: {
24
+ [locale: string]: (CompiledFragment | number)[];
25
+ };
26
+ transform: {
27
+ order: 'pre';
28
+ handler: any;
29
+ };
30
+ constructor();
31
+ init: (configRaw: Config) => Promise<void>;
32
+ directExtract: () => Promise<void>;
33
+ afterExtract: (loc: string) => Promise<void>;
34
+ configResolved: (config: {
35
+ env: {
36
+ PROD?: boolean;
37
+ EXTRACT?: boolean;
38
+ };
39
+ root: string;
40
+ }) => Promise<void>;
41
+ handleHotUpdate: (ctx: ViteHotUpdateCTX) => Promise<any[]>;
42
+ resolveId: (source: string) => string;
43
+ load: (id: string) => string;
44
+ transformHandler: (code: string, id: string) => Promise<{
45
+ code?: undefined;
46
+ map?: undefined;
47
+ } | {
48
+ code: string;
49
+ map: import("magic-string").SourceMap;
50
+ }>;
51
+ }
52
+ export default function wuchale(config?: Config): Promise<Plugin>;
53
+ export {};