wuchale 0.20.0 → 0.21.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.
@@ -3,6 +3,7 @@ import type { HeuristicResultChecked } from '../adapters.js';
3
3
  export declare const varNames: {
4
4
  rt: string;
5
5
  hmrUpdate: string;
6
+ urlLocalize: string;
6
7
  };
7
8
  export declare function runtimeVars(wrapFunc: (expr: string) => string, base?: string): {
8
9
  rtTrans: string;
@@ -21,6 +22,7 @@ export declare function loaderPathResolver(importMetaUrl: string, baseDir: strin
21
22
  export declare const commentPrefix = "@wc-";
22
23
  export type CommentDirectives = {
23
24
  ignoreFile?: boolean;
25
+ unit?: boolean;
24
26
  forceType?: HeuristicResultChecked;
25
27
  context?: string;
26
28
  };
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  export const varNames = {
5
5
  rt: '_w_runtime_',
6
6
  hmrUpdate: '_w_hmrUpdate_',
7
+ urlLocalize: '_w_localize_',
7
8
  };
8
9
  export function runtimeVars(wrapFunc, base = varNames.rt) {
9
10
  return {
@@ -34,19 +35,23 @@ const commentDirectives = {
34
35
  ignore: `${commentPrefix}ignore`,
35
36
  ignoreFile: `${commentPrefix}ignore-file`,
36
37
  include: `${commentPrefix}include`,
38
+ unit: `${commentPrefix}unit`,
37
39
  url: `${commentPrefix}url`,
38
40
  context: `${commentPrefix}context:`,
39
41
  };
40
42
  export function updateCommentDirectives(data, directives) {
41
- if (data === commentDirectives.ignore) {
42
- directives.forceType = false;
43
- }
44
43
  if (data === commentDirectives.include) {
45
44
  directives.forceType = 'message';
46
45
  }
47
46
  if (data === commentDirectives.url) {
48
47
  directives.forceType = 'url';
49
48
  }
49
+ if (data === commentDirectives.unit) {
50
+ directives.unit = true;
51
+ }
52
+ if (data === commentDirectives.ignore) {
53
+ directives.forceType = false;
54
+ }
50
55
  if (data === commentDirectives.ignoreFile) {
51
56
  directives.ignoreFile = true;
52
57
  }
@@ -1,5 +1,5 @@
1
1
  import type MagicString from 'magic-string';
2
- import { type HeuristicDetails, type HeuristicDetailsBase, type HeuristicFunc, type IndexTracker, Message, type MessageType } from '../adapters.js';
2
+ import { type HeuristicDetails, type HeuristicDetailsBase, type HeuristicFunc, type IndexTracker, type Message, type MessageType } from '../adapters.js';
3
3
  import { type CommentDirectives, type RuntimeVars } from './index.js';
4
4
  type NestedRanges = [number, number, boolean][];
5
5
  type InitProps<NodeT> = {
@@ -1,6 +1,6 @@
1
1
  // Shared logic between adapters for handling nested / mixed elements within elements / fragments
2
- import { Message, } from '../adapters.js';
3
- import { commentPrefix, nonWhitespaceText, updateCommentDirectives, } from './index.js';
2
+ import { getKey, newMessage, } from '../adapters.js';
3
+ import { commentPrefix, nonWhitespaceText, updateCommentDirectives, varNames, } from './index.js';
4
4
  export class MixedVisitor {
5
5
  constructor(props) {
6
6
  Object.assign(this, props);
@@ -30,16 +30,19 @@ export class MixedVisitor {
30
30
  }
31
31
  }
32
32
  heurStr = heurStr.trimEnd();
33
- const msg = new Message(heurStr, this.fullHeuristicDetails({
34
- scope: props.scope,
35
- element: props.element,
36
- attribute: props.attribute,
37
- }));
33
+ const msg = newMessage({
34
+ msgStr: [heurStr],
35
+ details: this.fullHeuristicDetails({
36
+ scope: props.scope,
37
+ element: props.element,
38
+ attribute: props.attribute,
39
+ }),
40
+ });
38
41
  const heurMsgType = this.checkHeuristic(msg);
39
- if (heurMsgType) {
42
+ if (heurMsgType || props.commentDirectives.unit) {
40
43
  const hasCompoundText = hasTextChild && hasNonTextChild;
41
- if (props.inCompoundText || (hasCompoundText && !hasCommentDirectives)) {
42
- return [false, hasTextChild, hasCompoundText, heurMsgType, []];
44
+ if (props.inCompoundText || props.commentDirectives.unit || (hasCompoundText && !hasCommentDirectives)) {
45
+ return [false, hasTextChild, hasCompoundText, heurMsgType || 'message', []];
43
46
  }
44
47
  }
45
48
  // can't be extracted as one; visit each separately if markup
@@ -99,7 +102,11 @@ export class MixedVisitor {
99
102
  const chRange = this.getRange(child);
100
103
  if (this.isText(child)) {
101
104
  const [startWh, trimmed, endWh] = nonWhitespaceText(this.getTextContent(child));
102
- const msgInfo = new Message(trimmed, this.fullHeuristicDetails({ scope: props.scope }), props.commentDirectives.context);
105
+ const msgInfo = newMessage({
106
+ msgStr: [trimmed],
107
+ details: this.fullHeuristicDetails({ scope: props.scope }),
108
+ context: props.commentDirectives.context,
109
+ });
103
110
  if (startWh && !msgStr.endsWith(' ')) {
104
111
  msgStr += ' ';
105
112
  }
@@ -169,7 +176,11 @@ export class MixedVisitor {
169
176
  if (!msgStr) {
170
177
  return msgs;
171
178
  }
172
- const msgInfo = new Message(msgStr, this.fullHeuristicDetails({ scope: props.scope }), props.commentDirectives.context);
179
+ const msgInfo = newMessage({
180
+ msgStr: [msgStr],
181
+ details: this.fullHeuristicDetails({ scope: props.scope }),
182
+ context: props.commentDirectives.context,
183
+ });
173
184
  msgInfo.type = heurMsgType;
174
185
  msgInfo.placeholders = placeholders;
175
186
  if (hasTextChild || hasTextDescendants) {
@@ -189,7 +200,11 @@ export class MixedVisitor {
189
200
  begin += `${this.vars().rtTransCtx}(${this.vars().nestCtx}`;
190
201
  }
191
202
  else {
192
- begin += `${this.vars().rtTrans}(${this.index.get(msgInfo.toKey())}`;
203
+ if (msgInfo.type === 'url') {
204
+ begin += `${varNames.urlLocalize}(`;
205
+ end = `), ${this.vars().rtLocale}${end}`;
206
+ }
207
+ begin += `${this.vars().rtTrans}(${this.index.get(getKey(msgInfo.msgStr, msgInfo.context))}`;
193
208
  }
194
209
  if (iArg > 0) {
195
210
  begin += ', [';
@@ -2,6 +2,7 @@
2
2
  import { loaderPathResolver } from '../adapter-utils/index.js';
3
3
  import { defaultGenerateLoadID, defaultHeuristicFuncOnly } from '../adapters.js';
4
4
  import { deepMergeObjects } from '../config.js';
5
+ import { pofile } from '../pofile.js';
5
6
  import { Transformer } from './transformer.js';
6
7
  export { Transformer };
7
8
  export { parseScript, scriptParseOptions, scriptParseOptionsWithComments } from './transformer.js';
@@ -11,7 +12,7 @@ export const pluralPattern = {
11
12
  };
12
13
  export const defaultArgs = {
13
14
  files: { include: 'src/**/*.{js,ts}', ignore: '**/*.d.ts' },
14
- localesDir: './src/locales',
15
+ storage: pofile(),
15
16
  patterns: [pluralPattern],
16
17
  heuristic: defaultHeuristicFuncOnly,
17
18
  granularLoad: false,
@@ -1,8 +1,8 @@
1
1
  import type * as Estree from 'acorn';
2
2
  import MagicString from 'magic-string';
3
3
  import { type CommentDirectives, type RuntimeVars } from '../adapter-utils/index.js';
4
- import type { CatalogExpr, CodePattern, HeuristicDetails, HeuristicDetailsBase, HeuristicFunc, HeuristicResultChecked, IndexTracker, MessageType, RuntimeConf, TransformOutput, UrlMatcher } from '../adapters.js';
5
- import { Message } from '../adapters.js';
4
+ import type { CodePattern, HeuristicDetails, HeuristicDetailsBase, HeuristicFunc, HeuristicResultChecked, IndexTracker, MessageType, RuntimeConf, RuntimeExpr, TransformOutput, UrlMatcher } from '../adapters.js';
5
+ import { type Message } from '../adapters.js';
6
6
  export declare const scriptParseOptions: Estree.Options;
7
7
  export declare function scriptParseOptionsWithComments(): [Estree.Options, Estree.Comment[][]];
8
8
  export declare function parseScript(content: string): [Estree.Program, Estree.Comment[][]];
@@ -24,10 +24,11 @@ export declare class Transformer<RTCtxT = {}> {
24
24
  realBodyStarts: Set<number>;
25
25
  /** will be passed to decide which runtime variable to use */
26
26
  runtimeCtx: RTCtxT;
27
- constructor(content: string, filename: string, index: IndexTracker, heuristic: HeuristicFunc, patterns: CodePattern[], catalogExpr: CatalogExpr, rtConf: RuntimeConf<RTCtxT>, matchUrl: UrlMatcher, rtBaseVars?: string[]);
27
+ constructor(content: string, filename: string, index: IndexTracker, heuristic: HeuristicFunc, patterns: CodePattern[], catalogExpr: RuntimeExpr, rtConf: RuntimeConf<RTCtxT>, matchUrl: UrlMatcher, rtBaseVars?: string[]);
28
28
  fullHeuristicDetails: (detailsBase: HeuristicDetailsBase) => HeuristicDetails;
29
29
  getHeuristicMessageType: (msg: Message) => HeuristicResultChecked;
30
30
  checkHeuristic: (msgStr: string, detailsBase: HeuristicDetailsBase) => [MessageType, Message] | [false, null];
31
+ literalRepl(msgInfo: Message): string;
31
32
  visitLiteral: (node: Estree.Literal, heuristicDetailsBase?: HeuristicDetailsBase) => Message[];
32
33
  visitArrayExpression: (node: Estree.ArrayExpression) => Message[];
33
34
  visitSequenceExpression: (node: Estree.SequenceExpression) => Message[];
@@ -3,7 +3,7 @@ import { tsPlugin } from '@sveltejs/acorn-typescript';
3
3
  import { Parser } from 'acorn';
4
4
  import MagicString from 'magic-string';
5
5
  import { runtimeVars, updateCommentDirectives, varNames, } from '../adapter-utils/index.js';
6
- import { defaultHeuristicFuncOnly, Message } from '../adapters.js';
6
+ import { defaultHeuristicFuncOnly, getKey, newMessage } from '../adapters.js';
7
7
  export const scriptParseOptions = {
8
8
  sourceType: 'module',
9
9
  ecmaVersion: 'latest',
@@ -143,7 +143,11 @@ export class Transformer {
143
143
  // nothing to ask
144
144
  return [false, null];
145
145
  }
146
- const msg = new Message(msgStr, this.fullHeuristicDetails(detailsBase), this.commentDirectives.context);
146
+ const msg = newMessage({
147
+ msgStr: [msgStr],
148
+ details: this.fullHeuristicDetails(detailsBase),
149
+ context: this.commentDirectives.context,
150
+ });
147
151
  const heuRes = this.getHeuristicMessageType(msg);
148
152
  if (!heuRes) {
149
153
  return [false, null];
@@ -151,6 +155,13 @@ export class Transformer {
151
155
  msg.type = heuRes;
152
156
  return [heuRes, msg];
153
157
  };
158
+ literalRepl(msgInfo) {
159
+ let repl = `${this.vars().rtTrans}(${this.index.get(getKey(msgInfo.msgStr, msgInfo.context))})`;
160
+ if (msgInfo.type !== 'url') {
161
+ return repl;
162
+ }
163
+ return `${varNames.urlLocalize}(${repl}, ${this.vars().rtLocale})`;
164
+ }
154
165
  visitLiteral = (node, heuristicDetailsBase) => {
155
166
  if (typeof node.value !== 'string') {
156
167
  return [];
@@ -160,7 +171,7 @@ export class Transformer {
160
171
  if (!pass) {
161
172
  return [];
162
173
  }
163
- this.mstr.update(start, end, `${this.vars().rtTrans}(${this.index.get(msgInfo.toKey())})`);
174
+ this.mstr.update(start, end, this.literalRepl(msgInfo));
164
175
  return [msgInfo];
165
176
  };
166
177
  visitArrayExpression = (node) => node.elements.flatMap(elm => (elm ? this.visit(elm) : []));
@@ -261,8 +272,12 @@ export class Transformer {
261
272
  if (typeof argVal.value !== 'string') {
262
273
  return this.defaultVisitCallExpression(node);
263
274
  }
264
- const msgInfo = new Message(argVal.value, this.fullHeuristicDetails({ scope: 'script' }), this.commentDirectives.context);
265
- updates.push([argVal.start, argVal.end, `${this.vars().rtTrans}(${this.index.get(msgInfo.toKey())})`]);
275
+ const msgInfo = newMessage({
276
+ msgStr: [argVal.value],
277
+ details: this.fullHeuristicDetails({ scope: 'script' }),
278
+ context: this.commentDirectives.context,
279
+ });
280
+ updates.push([argVal.start, argVal.end, this.literalRepl(msgInfo)]);
266
281
  msgs.push(msgInfo);
267
282
  continue;
268
283
  }
@@ -281,9 +296,13 @@ export class Transformer {
281
296
  candidates.push(elm.value);
282
297
  }
283
298
  // plural(num, ['Form one', 'Form two'])
284
- const msgInfo = new Message(candidates, this.fullHeuristicDetails({ scope: 'script' }), this.commentDirectives.context);
299
+ const msgInfo = newMessage({
300
+ msgStr: candidates,
301
+ details: this.fullHeuristicDetails({ scope: 'script' }),
302
+ context: this.commentDirectives.context,
303
+ });
285
304
  msgInfo.plural = true;
286
- const index = this.index.get(msgInfo.toKey());
305
+ const index = this.index.get(getKey(msgInfo.msgStr, msgInfo.context));
287
306
  msgs.push(msgInfo);
288
307
  updates.push([argVal.start, argVal.end, `${this.vars().rtTPlural}(${index})`]);
289
308
  }
@@ -542,10 +561,14 @@ export class Transformer {
542
561
  }
543
562
  this.mstr.update(end, end + 2, ', ');
544
563
  }
545
- const msgInfo = new Message(msgStr, this.fullHeuristicDetails({ scope: 'script' }), this.commentDirectives.context);
564
+ const msgInfo = newMessage({
565
+ msgStr: [msgStr],
566
+ details: this.fullHeuristicDetails({ scope: 'script' }),
567
+ context: this.commentDirectives.context,
568
+ });
546
569
  msgInfo.type = msgTyp;
547
570
  msgInfo.placeholders = placeholders;
548
- const index = this.index.get(msgInfo.toKey());
571
+ const index = this.index.get(getKey(msgInfo.msgStr, msgInfo.context));
549
572
  msgs.push(msgInfo);
550
573
  return [index, msgs];
551
574
  };
@@ -562,6 +585,10 @@ export class Transformer {
562
585
  const { start: start0, end: end0 } = node.quasis[0];
563
586
  let begin = `${this.vars().rtTrans}(${index}`;
564
587
  let end = ')';
588
+ if (msgTyp === 'url') {
589
+ begin = `${varNames.urlLocalize}(${begin}`;
590
+ end += `, ${this.vars().rtLocale})`;
591
+ }
565
592
  if (node.expressions.length) {
566
593
  begin += ', [';
567
594
  end = ']' + end;
@@ -1,5 +1,5 @@
1
1
  import type { CompiledElement } from './compile.js';
2
- import type { URLLocalizer } from './url.js';
2
+ import type { StorageFactory } from './storage.js';
3
3
  type TxtScope = 'script' | 'markup' | 'attribute';
4
4
  export type HeuristicDetailsBase = {
5
5
  scope: TxtScope;
@@ -17,16 +17,16 @@ export type HeuristicDetails = HeuristicDetailsBase & {
17
17
  call?: string;
18
18
  };
19
19
  export type MessageType = 'message' | 'url';
20
- export declare class Message {
20
+ export type Message = {
21
21
  msgStr: string[];
22
22
  plural: boolean;
23
23
  context?: string;
24
24
  placeholders: [number, string][];
25
25
  details: HeuristicDetails;
26
26
  type: MessageType;
27
- constructor(msgStr: string | (string | null | undefined)[], heuristicDetails?: HeuristicDetails, context?: string);
28
- toKey: () => string;
29
- }
27
+ };
28
+ export declare function newMessage(init: Partial<Message>): Message;
29
+ export declare const getKey: (text: string[], context?: string) => string;
30
30
  export type HeuristicResultChecked = MessageType | false;
31
31
  export type HeuristicResult = HeuristicResultChecked | null | undefined;
32
32
  export type HeuristicFunc = (msg: Message) => HeuristicResult;
@@ -53,7 +53,7 @@ export type GlobConf = string | string[] | {
53
53
  include: string | string[];
54
54
  ignore: string | string[];
55
55
  };
56
- export type CatalogExpr = {
56
+ export type RuntimeExpr = {
57
57
  plain: string;
58
58
  reactive: string;
59
59
  };
@@ -62,17 +62,18 @@ type TransformCtx = {
62
62
  content: string;
63
63
  filename: string;
64
64
  index: IndexTracker;
65
- expr: CatalogExpr;
65
+ expr: RuntimeExpr;
66
66
  matchUrl: UrlMatcher;
67
67
  };
68
68
  export type HMRData = {
69
69
  version: number;
70
70
  data: Record<string, [number, CompiledElement][]>;
71
71
  };
72
- export type TransformOutputFunc = (header: string) => {
72
+ export type TransformOutputCode = {
73
73
  code?: string;
74
74
  map?: any;
75
75
  };
76
+ export type TransformOutputFunc = (header: string) => TransformOutputCode;
76
77
  export type TransformOutput = {
77
78
  output: TransformOutputFunc;
78
79
  msgs: Message[];
@@ -102,12 +103,12 @@ export type LoaderPath = {
102
103
  };
103
104
  export type URLConf = {
104
105
  patterns?: string[];
105
- localize?: boolean | URLLocalizer;
106
+ localize?: boolean | string;
106
107
  };
107
108
  export type AdapterPassThruOpts<RTCtxT extends {} = {}> = {
108
109
  sourceLocale?: string;
109
110
  files: GlobConf;
110
- localesDir: string;
111
+ storage: StorageFactory;
111
112
  /** if writing transformed code to a directory is desired, specify this */
112
113
  outDir?: string;
113
114
  granularLoad: boolean;
@@ -123,7 +124,7 @@ export type Adapter<RTCtxT extends {} = {}> = AdapterPassThruOpts<RTCtxT> & {
123
124
  /** default loaders to copy, `null` means custom */
124
125
  defaultLoaderPath: LoaderPath | string | null;
125
126
  /** names to import from loaders, should avoid collision with code variables */
126
- getRuntimeVars?: Partial<CatalogExpr>;
127
+ getRuntimeVars?: Partial<RuntimeExpr>;
127
128
  };
128
129
  export type CodePattern = {
129
130
  name: string;
package/dist/adapters.js CHANGED
@@ -1,27 +1,22 @@
1
- const someHeuDet = { file: '', scope: 'markup', insideProgram: true };
2
- export class Message {
3
- msgStr; // array for plurals
4
- plural = false;
5
- context;
6
- placeholders = [];
7
- details;
8
- type = 'message';
9
- constructor(msgStr, heuristicDetails = someHeuDet, context) {
10
- if (typeof msgStr === 'string') {
11
- this.msgStr = [msgStr];
12
- }
13
- else {
14
- this.msgStr = msgStr.filter(str => str != null);
15
- }
16
- this.msgStr = this.msgStr.map(msg => msg
17
- .split('\n')
18
- .map(line => line.trim())
19
- .join('\n'));
20
- this.details = heuristicDetails;
21
- this.context = context;
1
+ export function newMessage(init) {
2
+ init.msgStr = init.msgStr?.filter(str => str != null) ?? [];
3
+ if (init.details?.scope === 'markup') {
4
+ init.msgStr = init.msgStr.map(msg => msg.replace(/\s+/g, ' ').trim());
22
5
  }
23
- toKey = () => `${this.msgStr.slice(0, 2).join('\n')}\n${this.context ?? ''}`.trim();
6
+ return {
7
+ msgStr: init.msgStr,
8
+ plural: init.plural ?? false,
9
+ placeholders: init.placeholders ?? [],
10
+ type: init.type ?? 'message',
11
+ context: init.context,
12
+ details: init.details ?? {
13
+ file: '',
14
+ scope: 'markup',
15
+ insideProgram: true,
16
+ },
17
+ };
24
18
  }
19
+ export const getKey = (text, context) => `${text.join('\n')}\n${context ?? ''}`.trim();
25
20
  export const defaultHeuristicOpts = {
26
21
  ignoreElements: ['script', 'style', 'path', 'code', 'pre'],
27
22
  ignoreAttribs: [['form', 'method']],
@@ -1,11 +1,9 @@
1
- import type { AI } from './index.js';
2
- type GeminiOpts = {
3
- apiKey?: string;
4
- model?: string;
5
- batchSize?: number;
6
- think?: boolean;
7
- parallel?: number;
8
- };
9
- export declare function gemini({ apiKey, model, batchSize, think, parallel, }?: GeminiOpts): AI | null;
1
+ import type { AI, AIPassThruOpts } from './index.js';
2
+ type GeminiOpts = Partial<AIPassThruOpts & {
3
+ apiKey: string;
4
+ model: string;
5
+ think: boolean;
6
+ }>;
7
+ export declare function gemini({ apiKey, model, group, batchSize, think, parallel, }?: GeminiOpts): AI | null;
10
8
  export declare const defaultGemini: AI | null;
11
9
  export {};
package/dist/ai/gemini.js CHANGED
@@ -15,7 +15,7 @@ function prepareData(content, instruction, think) {
15
15
  },
16
16
  };
17
17
  }
18
- export function gemini({ apiKey = 'env', model = 'gemini-2.5-flash', batchSize = 50, think = false, parallel = 4, } = {}) {
18
+ export function gemini({ apiKey = 'env', model = 'gemini-2.5-flash', group = {}, batchSize = 50, think = false, parallel = 4, } = {}) {
19
19
  if (apiKey === 'env') {
20
20
  apiKey = process.env.GEMINI_API_KEY ?? '';
21
21
  }
@@ -25,6 +25,7 @@ export function gemini({ apiKey = 'env', model = 'gemini-2.5-flash', batchSize =
25
25
  return {
26
26
  name: 'Gemini',
27
27
  batchSize,
28
+ group,
28
29
  parallel,
29
30
  translate: async (content, instruction) => {
30
31
  const data = prepareData(content, instruction, think);
@@ -1,27 +1,30 @@
1
- import { type Item } from '../handler/pofile.js';
2
1
  import { type Logger } from '../log.js';
2
+ import type { Item } from '../storage.js';
3
3
  type Batch = {
4
4
  id: number;
5
+ targetLocales: string[];
5
6
  messages: Item[];
6
7
  };
7
- export type AI = {
8
- name: string;
8
+ export type AIPassThruOpts = {
9
9
  batchSize: number;
10
- translate: (messages: string, instruction: string) => Promise<string>;
11
10
  parallel: number;
11
+ group: Record<string, string[][]>;
12
+ };
13
+ export type AI = AIPassThruOpts & {
14
+ name: string;
15
+ translate: (messages: string, instruction: string) => Promise<string>;
12
16
  };
13
17
  export default class AIQueue {
14
18
  #private;
15
19
  batches: Batch[];
16
20
  nextBatchId: number;
17
21
  running: Promise<void> | null;
18
- sourceLang: string;
19
- targetLang: string;
22
+ sourceLocale: string;
20
23
  ai: AI;
21
24
  instruction: string;
22
25
  onComplete: () => Promise<void>;
23
26
  log: Logger;
24
- constructor(sourceLang: string, targetLang: string, ai: AI, onComplete: () => Promise<void>, log: Logger);
27
+ constructor(sourceLocale: string, ai: AI, onComplete: () => Promise<void>, log: Logger);
25
28
  translate: (batch: Batch, attempt?: number) => Promise<void>;
26
29
  run: () => Promise<void>;
27
30
  add: (messages: Item[]) => void;