wuchale 0.19.4 → 0.20.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.
@@ -91,7 +91,7 @@ export class MixedVisitor {
91
91
  const childrenNestedRanges = [];
92
92
  let hasTextDescendants = false;
93
93
  const msgs = [];
94
- const comments = [];
94
+ const placeholders = [];
95
95
  for (const child of props.children) {
96
96
  if (this.isComment(child)) {
97
97
  continue;
@@ -119,9 +119,8 @@ export class MixedVisitor {
119
119
  if (!hasCompoundText) {
120
120
  continue;
121
121
  }
122
- const placeholder = `{${iArg}}`;
123
- msgStr += placeholder;
124
- comments.push(`placeholder ${placeholder}: ${this.mstr.original.slice(chRange.start + 1, chRange.end - 1)}`);
122
+ msgStr += `{${iArg}}`;
123
+ placeholders.push([iArg, this.mstr.original.slice(chRange.start + 1, chRange.end - 1)]);
125
124
  let moveStart = chRange.start;
126
125
  if (iArg > 0) {
127
126
  this.mstr.update(chRange.start, chRange.start + 1, ', ');
@@ -172,7 +171,7 @@ export class MixedVisitor {
172
171
  }
173
172
  const msgInfo = new Message(msgStr, this.fullHeuristicDetails({ scope: props.scope }), props.commentDirectives.context);
174
173
  msgInfo.type = heurMsgType;
175
- msgInfo.comments = comments;
174
+ msgInfo.placeholders = placeholders;
176
175
  if (hasTextChild || hasTextDescendants) {
177
176
  msgs.push(msgInfo);
178
177
  }
@@ -5,6 +5,7 @@ export { parseScript, scriptParseOptions, scriptParseOptionsWithComments } from
5
5
  export declare const pluralPattern: CodePattern;
6
6
  type LoadersAvailable = 'server' | 'vite';
7
7
  export type VanillaArgs = AdapterArgs<LoadersAvailable>;
8
+ export declare const defaultArgs: VanillaArgs;
8
9
  export declare function getDefaultLoaderPath(loader: LoaderChoice<LoadersAvailable>, bundle: boolean): string | {
9
10
  client: string;
10
11
  server: string;
@@ -9,7 +9,7 @@ export const pluralPattern = {
9
9
  name: 'plural',
10
10
  args: ['other', 'message', 'pluralFunc'],
11
11
  };
12
- const defaultArgs = {
12
+ export const defaultArgs = {
13
13
  files: { include: 'src/**/*.{js,ts}', ignore: '**/*.d.ts' },
14
14
  localesDir: './src/locales',
15
15
  patterns: [pluralPattern],
@@ -529,13 +529,12 @@ export class Transformer {
529
529
  visitTemplateLiteralQuasis = (node, msgTyp) => {
530
530
  const msgs = [];
531
531
  let msgStr = node.quasis[0].value?.cooked ?? '';
532
- const comments = [];
532
+ const placeholders = [];
533
533
  for (const [i, expr] of node.expressions.entries()) {
534
534
  msgs.push(...this.visit(expr));
535
535
  const quasi = node.quasis[i + 1];
536
- const placeholder = `{${i}}`;
537
- msgStr += `${placeholder}${quasi.value.cooked}`;
538
- comments.push(`placeholder ${placeholder}: ${this.content.slice(expr.start, expr.end)}`);
536
+ msgStr += `{${i}}${quasi.value.cooked}`;
537
+ placeholders.push([i, this.content.slice(expr.start, expr.end)]);
539
538
  const { start, end } = quasi;
540
539
  this.mstr.remove(start - 1, end);
541
540
  if (i + 1 === node.expressions.length) {
@@ -545,7 +544,7 @@ export class Transformer {
545
544
  }
546
545
  const msgInfo = new Message(msgStr, this.fullHeuristicDetails({ scope: 'script' }), this.commentDirectives.context);
547
546
  msgInfo.type = msgTyp;
548
- msgInfo.comments = comments;
547
+ msgInfo.placeholders = placeholders;
549
548
  const index = this.index.get(msgInfo.toKey());
550
549
  msgs.push(msgInfo);
551
550
  return [index, msgs];
@@ -21,7 +21,7 @@ export declare class Message {
21
21
  msgStr: string[];
22
22
  plural: boolean;
23
23
  context?: string;
24
- comments: string[];
24
+ placeholders: [number, string][];
25
25
  details: HeuristicDetails;
26
26
  type: MessageType;
27
27
  constructor(msgStr: string | (string | null | undefined)[], heuristicDetails?: HeuristicDetails, context?: string);
@@ -100,6 +100,10 @@ export type LoaderPath = {
100
100
  client: string;
101
101
  server: string;
102
102
  };
103
+ export type URLConf = {
104
+ patterns?: string[];
105
+ localize?: boolean | URLLocalizer;
106
+ };
103
107
  export type AdapterPassThruOpts<RTCtxT extends {} = {}> = {
104
108
  sourceLocale?: string;
105
109
  files: GlobConf;
@@ -108,10 +112,7 @@ export type AdapterPassThruOpts<RTCtxT extends {} = {}> = {
108
112
  outDir?: string;
109
113
  granularLoad: boolean;
110
114
  bundleLoad: boolean;
111
- url?: {
112
- patterns?: string[];
113
- localize?: boolean | URLLocalizer;
114
- };
115
+ url?: URLConf;
115
116
  generateLoadID: (filename: string) => string;
116
117
  runtime: Partial<RuntimeConf<RTCtxT>>;
117
118
  };
package/dist/adapters.js CHANGED
@@ -3,7 +3,7 @@ export class Message {
3
3
  msgStr; // array for plurals
4
4
  plural = false;
5
5
  context;
6
- comments = [];
6
+ placeholders = [];
7
7
  details;
8
8
  type = 'message';
9
9
  constructor(msgStr, heuristicDetails = someHeuDet, context) {
@@ -1,9 +1,8 @@
1
- import PO from 'pofile';
1
+ import { type Item } from '../handler/pofile.js';
2
2
  import { type Logger } from '../log.js';
3
- export type ItemType = InstanceType<typeof PO.Item>;
4
3
  type Batch = {
5
4
  id: number;
6
- messages: ItemType[];
5
+ messages: Item[];
7
6
  };
8
7
  export type AI = {
9
8
  name: string;
@@ -25,6 +24,6 @@ export default class AIQueue {
25
24
  constructor(sourceLang: string, targetLang: string, ai: AI, onComplete: () => Promise<void>, log: Logger);
26
25
  translate: (batch: Batch, attempt?: number) => Promise<void>;
27
26
  run: () => Promise<void>;
28
- add: (messages: ItemType[]) => void;
27
+ add: (messages: Item[]) => void;
29
28
  }
30
29
  export {};
package/dist/ai/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // $$ cd .. && npm run test
2
2
  // $$ node %f
3
3
  import PO from 'pofile';
4
+ import { itemToPOItem, poitemToItem } from '../handler/pofile.js';
4
5
  import { color } from '../log.js';
5
6
  const MAX_RETRIES = 30;
6
7
  // implements a queue for a sequential translation useful for vite's transform during dev
@@ -42,9 +43,9 @@ export default class AIQueue {
42
43
  let translated;
43
44
  try {
44
45
  const po = new PO();
45
- po.items = batch.messages;
46
+ po.items = batch.messages.map(itemToPOItem);
46
47
  const translatedstr = await this.ai.translate(po.toString(), this.instruction);
47
- translated = PO.parse(translatedstr).items;
48
+ translated = PO.parse(translatedstr).items.map(poitemToItem);
48
49
  }
49
50
  catch (err) {
50
51
  this.log.error(`${logStart}: ${color.red(`error: ${err}`)}`);
@@ -53,7 +54,7 @@ export default class AIQueue {
53
54
  const unTranslated = batch.messages.slice(translated.length);
54
55
  for (const [i, item] of translated.entries()) {
55
56
  const destItem = batch.messages[i];
56
- if (item.msgid !== destItem?.msgid) {
57
+ if (item.msgid.join('\n') !== destItem?.msgid?.join('\n')) {
57
58
  unTranslated.push(destItem);
58
59
  continue;
59
60
  }
@@ -1,2 +1,2 @@
1
1
  import type { Config } from '../config.js';
2
- export declare function extract(config: Config, clean: boolean, watch: boolean, sync: boolean): Promise<void>;
2
+ export declare function extract(config: Config, root: string, clean: boolean, watch: boolean, sync: boolean): Promise<void>;
@@ -1,28 +1,77 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { watch as watchFS } from 'chokidar';
3
- import { AdapterHandler } from '../handler.js';
3
+ import { glob } from 'tinyglobby';
4
+ import { globConfToArgs } from '../handler/files.js';
5
+ import { AdapterHandler } from '../handler/index.js';
6
+ import { itemIsObsolete } from '../handler/pofile.js';
7
+ import { SharedStates } from '../handler/state.js';
4
8
  import { color, Logger } from '../log.js';
5
- function extractor(handler, logger) {
6
- const adapterName = color.magenta(handler.key);
7
- return async (filename) => {
8
- logger.info(`${adapterName}: Extract from ${color.cyan(filename)}`);
9
- const contents = await readFile(filename);
10
- await handler.transform(contents.toString(), filename);
11
- };
9
+ async function directScanFS(handler, extract, filePaths, clean, sync, logger) {
10
+ const dumps = new Map();
11
+ for (const loc of handler.allLocales) {
12
+ const catalog = handler.sharedState.poFilesByLoc.get(loc).catalog;
13
+ const items = Array.from(catalog.values());
14
+ dumps.set(loc, JSON.stringify(items));
15
+ if (clean) {
16
+ for (const item of items) {
17
+ item.references = item.references.filter(ref => {
18
+ if (handler.fileMatches(ref.file)) {
19
+ return false;
20
+ }
21
+ if (handler.sharedState.ownerKey !== handler.key) {
22
+ return true;
23
+ }
24
+ return handler.sharedState.otherFileMatches.some(match => match(ref.file));
25
+ });
26
+ }
27
+ }
28
+ await handler.initUrlPatterns(loc, catalog);
29
+ }
30
+ if (sync) {
31
+ for (const fPath of filePaths) {
32
+ await extract(fPath);
33
+ }
34
+ }
35
+ else {
36
+ await Promise.all(filePaths.map(extract));
37
+ }
38
+ if (clean) {
39
+ logger.info('Cleaning...');
40
+ }
41
+ for (const loc of handler.allLocales) {
42
+ const catalog = handler.sharedState.poFilesByLoc.get(loc).catalog;
43
+ if (clean) {
44
+ for (const [key, item] of catalog.entries()) {
45
+ if (itemIsObsolete(item)) {
46
+ catalog.delete(key);
47
+ }
48
+ }
49
+ }
50
+ if (JSON.stringify(Array.from(catalog.values())) !== dumps.get(loc)) {
51
+ await handler.savePO(loc);
52
+ }
53
+ }
12
54
  }
13
- export async function extract(config, clean, watch, sync) {
55
+ export async function extract(config, root, clean, watch, sync) {
14
56
  const logger = new Logger(config.logLevel);
15
57
  !watch && logger.info('Extracting...');
16
58
  const handlers = [];
17
- const sharedState = new Map();
59
+ const sharedState = new SharedStates();
18
60
  for (const [key, adapter] of Object.entries(config.adapters)) {
19
- const handler = new AdapterHandler(adapter, key, config, 'cli', process.cwd(), logger);
61
+ const handler = new AdapterHandler(adapter, key, config, 'cli', root, logger);
20
62
  await handler.init(sharedState);
21
- handlers.push(handler);
63
+ const adapterName = color.magenta(handler.key);
64
+ const extract = async (filename) => {
65
+ logger.info(`${adapterName}: Extract from ${color.cyan(filename)}`);
66
+ const contents = await readFile(filename);
67
+ await handler.transform(contents.toString(), filename);
68
+ };
69
+ const filePaths = await glob(...globConfToArgs(adapter.files, adapter.localesDir, adapter.outDir));
70
+ handlers.push([handler, extract, filePaths]);
22
71
  }
23
- // other loop to make sure that all otherFileMatchers are collected
24
- for (const handler of handlers) {
25
- await handler.directScanFS(clean, sync);
72
+ // separate loop to make sure that all otherFileMatchers are collected
73
+ for (const [handler, extract, files] of handlers) {
74
+ await directScanFS(handler, extract, files, clean, sync, logger);
26
75
  }
27
76
  if (!watch) {
28
77
  logger.info('Extraction finished.');
@@ -30,13 +79,12 @@ export async function extract(config, clean, watch, sync) {
30
79
  }
31
80
  // watch
32
81
  logger.info('Watching for changes');
33
- const handlersWithExtr = handlers.map(h => [h.fileMatches, extractor(h, logger)]);
34
82
  watchFS('.', { ignoreInitial: true }).on('all', async (event, filename) => {
35
83
  if (!['add', 'change'].includes(event)) {
36
84
  return;
37
85
  }
38
- for (const [fileMatches, extract] of handlersWithExtr) {
39
- if (fileMatches(filename)) {
86
+ for (const [handler, extract] of handlers) {
87
+ if (handler.fileMatches(filename)) {
40
88
  await extract(filename);
41
89
  }
42
90
  }
package/dist/cli/index.js CHANGED
@@ -53,20 +53,21 @@ Options:
53
53
  ${color.cyan('--log-level')}, ${color.cyan('-l')} {${Object.keys(logLevels).map(color.cyan)}} (only when no commands) set log level
54
54
  ${color.cyan('--help')}, ${color.cyan('-h')} Show this help
55
55
  `;
56
- async function getConfigNLocales() {
56
+ async function configRootLocales() {
57
57
  const config = await getConfig(values.config);
58
58
  config.logLevel = values['log-level'];
59
- return [config, config.locales];
59
+ return [config, values.config ?? process.cwd(), config.locales];
60
60
  }
61
61
  if (values.help) {
62
62
  console.log('wuchale cli');
63
63
  console.log(help.trimEnd());
64
64
  }
65
65
  else if (cmd == null) {
66
- await extract((await getConfigNLocales())[0], values.clean, values.watch, values.sync);
66
+ const [config, root] = await configRootLocales();
67
+ await extract(config, root, values.clean, values.watch, values.sync);
67
68
  }
68
69
  else if (cmd === 'status') {
69
- await status(...(await getConfigNLocales()));
70
+ await status(...(await configRootLocales()));
70
71
  }
71
72
  else {
72
73
  console.warn(`${color.yellow('Unknown command')}: ${cmd}`);
@@ -1,2 +1,2 @@
1
1
  import { type Config } from '../config.js';
2
- export declare function status(config: Config, locales: string[]): Promise<void>;
2
+ export declare function status(config: Config, root: string, locales: string[]): Promise<void>;
@@ -1,31 +1,36 @@
1
+ import { relative } from 'node:path';
1
2
  import { getLanguageName } from '../config.js';
2
- import { AdapterHandler, loadPOFile } from '../handler.js';
3
+ import { AdapterHandler } from '../handler/index.js';
4
+ import { POFile } from '../handler/pofile.js';
5
+ import { SharedStates } from '../handler/state.js';
3
6
  import { color, Logger } from '../log.js';
4
- async function statPO(filename) {
5
- const po = await loadPOFile(filename);
6
- const stats = { total: 0, untranslated: 0, obsolete: 0 };
7
- for (const item of po.items) {
8
- stats.total++;
7
+ async function statPO(poFile, urlPart) {
8
+ const po = await poFile.loadRaw(urlPart, false);
9
+ const stats = { Total: 0, Untranslated: 0, Obsolete: 0 };
10
+ for (const item of po?.items ?? []) {
11
+ stats.Total++;
9
12
  if (!item.msgstr[0]) {
10
- stats.untranslated++;
13
+ stats.Untranslated++;
11
14
  }
12
15
  if (item.obsolete) {
13
- stats.obsolete++;
16
+ stats.Obsolete++;
14
17
  }
15
18
  }
16
19
  return stats;
17
20
  }
18
- export async function status(config, locales) {
21
+ export async function status(config, root, locales) {
19
22
  // console.log because if the user invokes this command, they want full info regardless of config
20
23
  console.log(`Locales: ${locales.map(l => color.cyan(`${l} (${getLanguageName(l)})`)).join(', ')}`);
24
+ const sharedStates = new SharedStates();
21
25
  for (const [key, adapter] of Object.entries(config.adapters)) {
22
- const handler = new AdapterHandler(adapter, key, config, 'cli', process.cwd(), new Logger(config.logLevel));
23
- const loaderPath = await handler.getLoaderPath();
26
+ const handler = new AdapterHandler(adapter, key, config, 'cli', root, new Logger(config.logLevel));
27
+ handler.initSharedState(sharedStates);
28
+ const loaderPath = await handler.files.getLoaderPath();
24
29
  console.log(`${color.magenta(key)}:`);
25
30
  if (loaderPath) {
26
31
  console.log(` Loader files:`);
27
32
  for (const [side, path] of Object.entries(loaderPath)) {
28
- console.log(` ${color.cyan(side)}: ${color.cyan(path)}`);
33
+ console.log(` ${color.cyan(side)}: ${color.cyan(relative(root, path))}`);
29
34
  }
30
35
  }
31
36
  else {
@@ -34,24 +39,17 @@ export async function status(config, locales) {
34
39
  }
35
40
  const statsData = {};
36
41
  for (const locale of locales) {
37
- let stats;
38
- try {
39
- stats = await statPO(handler.catalogFileName(locale));
40
- }
41
- catch (err) {
42
- if (err.code !== 'ENOENT') {
43
- throw err;
42
+ const locName = getLanguageName(locale);
43
+ for (const [name, url] of [
44
+ [locName, false],
45
+ [`${locName} URL`, true],
46
+ ]) {
47
+ const stats = await statPO(handler.sharedState.poFilesByLoc.get(locale), url);
48
+ if (stats.Total === 0) {
49
+ continue;
44
50
  }
45
- console.warn(color.yellow(' No catalog found.'));
46
- continue;
51
+ statsData[name] = stats;
47
52
  }
48
- const { total, obsolete, untranslated } = stats;
49
- const locName = getLanguageName(locale);
50
- statsData[locName] = {
51
- Total: total,
52
- Untranslated: untranslated,
53
- Obsolete: obsolete,
54
- };
55
53
  }
56
54
  console.table(statsData);
57
55
  }
package/dist/config.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { AI } from './ai/index.js';
3
3
  import type { LogLevel } from './log.js';
4
4
  export type ConfigPartial = {
5
5
  locales: string[];
6
+ fallback: Record<string, string>;
6
7
  ai: AI | null;
7
8
  logLevel: LogLevel;
8
9
  };
package/dist/config.js CHANGED
@@ -2,6 +2,7 @@ import { resolve } from 'node:path';
2
2
  import { defaultGemini } from './ai/gemini.js';
3
3
  export const defaultConfig = {
4
4
  locales: ['en'],
5
+ fallback: {},
5
6
  adapters: {},
6
7
  hmr: true,
7
8
  ai: defaultGemini,
@@ -0,0 +1,31 @@
1
+ import type { Adapter, GlobConf, LoaderPath } from '../adapters.js';
2
+ import type { CompiledElement } from '../compile.js';
3
+ import { type URLManifest } from '../url.js';
4
+ export declare const objKeyLocale: (locale: string) => string;
5
+ export declare function normalizeSep(path: string): string;
6
+ export declare function globConfToArgs(conf: GlobConf, localesDir: string, outDir?: string): [string[], {
7
+ ignore: string[];
8
+ }];
9
+ export declare class Files {
10
+ #private;
11
+ key: string;
12
+ ownerKey: string;
13
+ loaderPath: LoaderPath;
14
+ proxyPath: string;
15
+ proxySyncPath: string;
16
+ generatedDir: string;
17
+ constructor(adapter: Adapter, key: string, root: string);
18
+ getLoaderPaths(): LoaderPath[];
19
+ getLoaderPath(): Promise<LoaderPath>;
20
+ getCompiledFilePath(loc: string, id: string | null): string;
21
+ getImportPath(filename: string, importer?: string): string;
22
+ genProxyContent(catalogs: string[], loadIDs: string[], syncImports?: string[]): string;
23
+ genProxy(locales: string[], loadIDs: string[], loadIDsImport: string[]): string;
24
+ genProxySync(locales: string[], loadIDs: string[], loadIDsImport: string[]): string;
25
+ writeProxies: (locales: string[], loadIDs: string[], loadIDsImport: string[]) => Promise<void>;
26
+ init: (locales: string[], ownerKey: string, sourceLocale: string) => Promise<void>;
27
+ writeUrlFiles: (manifest: URLManifest, fallbackLocale: string) => Promise<void>;
28
+ writeCatalogModule: (compiledData: CompiledElement[], pluralRule: string | null, locale: string, hmrVersion: number | null, loadID: string | null) => Promise<void>;
29
+ writeTransformed: (filename: string, content: string) => Promise<void>;
30
+ getImportLoaderPath(forServer: boolean, relativeTo: string): string;
31
+ }