wuchale 0.22.0 → 0.22.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.
@@ -417,10 +417,13 @@ export class Transformer {
417
417
  this.heuristciDetails.declaring = 'variable';
418
418
  }
419
419
  }
420
+ this.heuristciDetails.leftSide = true;
420
421
  const msgs = this.visit(node.id);
422
+ this.heuristciDetails.leftSide = false;
421
423
  if (topLevel && this.heuristciDetails.declaring === 'variable' && init.type === 'CallExpression') {
422
424
  this.heuristciDetails.topLevelCall = this.getCalleeName(init.callee);
423
425
  }
426
+ delete this.heuristciDetails.leftSide;
424
427
  return [...msgs, ...this.visit(node.init)];
425
428
  });
426
429
  visitExpressionStatement = (node) => this.withUpdateTLDetails(topLevel => {
@@ -9,11 +9,19 @@ export type HeuristicDetailsBase = {
9
9
  export type ScriptDeclType = 'variable' | 'function' | 'class' | 'expression';
10
10
  export type HeuristicDetails = HeuristicDetailsBase & {
11
11
  file: string;
12
+ /** the type of the top level declaration */
12
13
  declaring?: ScriptDeclType;
14
+ /** in assignments, whether the string is on the left side as destructuring default */
15
+ leftSide?: boolean;
16
+ /** the name of the function being defined, '' for arrow or null for global */
13
17
  funcName?: string | null;
18
+ /** whether the function being defined is nested inside another, null for no function */
14
19
  funcIsNested?: boolean;
20
+ /** whether inside a script file/<script> instead of an expression inside markup */
15
21
  insideProgram: boolean;
22
+ /** the name of the call at the top level */
16
23
  topLevelCall?: string;
24
+ /** the name of the nearest call (for arguments) */
17
25
  call?: string;
18
26
  };
19
27
  export type MessageType = 'message' | 'url';
package/dist/ai/index.js CHANGED
@@ -91,16 +91,35 @@ export default class AIQueue {
91
91
  const sourceComp = id.map(i => compileTranslation(i, ''));
92
92
  for (const loc of batch.targetLocales) {
93
93
  const translation = outItem[loc];
94
- if (translation?.length !== id.length) {
94
+ if (translation === undefined) {
95
95
  unTranslated.push(item);
96
96
  break;
97
97
  }
98
+ if (id.length > 1) {
99
+ // plural
100
+ if (translation.length === 0) {
101
+ // TODO: pass pluralRule and check nplurals
102
+ unTranslated.push(item);
103
+ break;
104
+ }
105
+ item.translations.set(loc, translation);
106
+ continue;
107
+ }
108
+ if (translation.length !== id.length) {
109
+ unTranslated.push(item);
110
+ break;
111
+ }
112
+ let equivalent = true;
98
113
  for (const [i, sou] of sourceComp.entries()) {
99
114
  if (!isEquivalent(sou, compileTranslation(translation[i], ''))) {
100
- unTranslated.push(item);
115
+ equivalent = false;
101
116
  break;
102
117
  }
103
118
  }
119
+ if (!equivalent) {
120
+ unTranslated.push(item);
121
+ break;
122
+ }
104
123
  item.translations.set(loc, translation);
105
124
  }
106
125
  }
@@ -1,4 +1,4 @@
1
- export declare function toViteError(err: unknown, adapterKey: string, filename: string): never;
1
+ export declare function toViteError(err: any, adapterKey: string, filename: string): Error;
2
2
  export declare function trimViteQueries(id: string): string;
3
3
  type HotUpdateCtx = {
4
4
  file: string;
@@ -1,29 +1,23 @@
1
1
  import { dirname } from 'node:path';
2
- import { inspect } from 'node:util';
3
2
  import { getConfig } from 'wuchale';
4
3
  import { Hub, pluginName } from '../hub.js';
5
4
  export function toViteError(err, adapterKey, filename) {
6
5
  const prefix = `${adapterKey}: transform failed for ${filename}`;
7
6
  // Ensure we always throw an Error instance with a non-empty message so build tools (e.g. Vite)
8
7
  // don't end up printing only a generic "error during build:" line.
9
- if (err instanceof Error) {
10
- const anyErr = err;
11
- const frame = typeof anyErr.frame === 'string' ? anyErr.frame : undefined;
12
- if (!err.message || !err.message.startsWith(prefix)) {
13
- const details = err.message ? `\n${err.message}` : '';
14
- const frameText = frame ? `\n\n${frame}` : '';
15
- err.message = `${prefix}${details}${frameText}`;
16
- }
17
- // Preserve useful metadata that some tooling expects.
18
- if (anyErr.id == null)
19
- anyErr.id = filename;
20
- if (anyErr.loc == null && anyErr.start?.line != null && anyErr.start?.column != null) {
21
- anyErr.loc = { file: filename, line: anyErr.start.line, column: anyErr.start.column };
22
- }
23
- throw err;
8
+ const frame = typeof err.frame === 'string' ? err.frame : undefined;
9
+ if (!err.message || !err.message.startsWith(prefix)) {
10
+ const details = err.message ? `\n${err.message}` : '';
11
+ const frameText = frame ? `\n\n${frame}` : '';
12
+ err.message = `${prefix}${details}${frameText}`;
24
13
  }
25
- const rendered = typeof err === 'string' ? err : inspect(err, { depth: 5, breakLength: 120, maxStringLength: 10_000 });
26
- throw new Error(`${prefix}\n${rendered}`);
14
+ // Preserve useful metadata that some tooling expects.
15
+ if (err.id == null)
16
+ err.id = filename;
17
+ if (err.loc == null && err.start?.line != null && err.start?.column != null) {
18
+ err.loc = { file: filename, line: err.start.line, column: err.start.column };
19
+ }
20
+ return err;
27
21
  }
28
22
  export function trimViteQueries(id) {
29
23
  let queryIndex = id.indexOf('?v=');
@@ -37,7 +31,7 @@ export function trimViteQueries(id) {
37
31
  return id;
38
32
  }
39
33
  export const wuchale = (configPath, hmrDelayThreshold = 1000) => {
40
- const hub = new Hub(() => getConfig(configPath), dirname(configPath ?? '.'), hmrDelayThreshold);
34
+ const hub = new Hub(() => getConfig(configPath), dirname(configPath ?? '.'), hmrDelayThreshold, undefined, toViteError);
41
35
  return {
42
36
  name: pluginName,
43
37
  async configResolved(config) {
@@ -54,7 +48,7 @@ export const wuchale = (configPath, hmrDelayThreshold = 1000) => {
54
48
  ctx.server.moduleGraph.invalidateModule(module, invalidatedModules, ctx.timestamp, false);
55
49
  }
56
50
  }
57
- if (!changeInfo.sourceTriggered) {
51
+ if (!changeInfo.sourceTriggered && changeInfo.invalidate.size > 0) {
58
52
  ctx.server.ws.send({ type: 'full-reload' });
59
53
  return [];
60
54
  }
package/dist/compile.js CHANGED
@@ -77,7 +77,7 @@ function compile(msgStr, start = 0, parentTag = null) {
77
77
  if (type === CLOSE) {
78
78
  if (currentOpenTag != null) {
79
79
  if (currentOpenTag != n) {
80
- throw Error('Closing a different tag');
80
+ return [compiled, 0, 'Closing a different tag'];
81
81
  }
82
82
  currentOpenTag = null;
83
83
  }
@@ -85,7 +85,7 @@ function compile(msgStr, start = 0, parentTag = null) {
85
85
  break;
86
86
  }
87
87
  else {
88
- throw Error('Closing a different tag');
88
+ return [compiled, 0, 'Closing a different tag'];
89
89
  }
90
90
  }
91
91
  else if (type === SELF_CLOSE) {
@@ -100,24 +100,24 @@ function compile(msgStr, start = 0, parentTag = null) {
100
100
  if (curTxt) {
101
101
  compiled.push(curTxt);
102
102
  }
103
- return [compiled, i];
103
+ if (currentOpenTag !== null) {
104
+ return [compiled, 0, 'Unexpected end'];
105
+ }
106
+ return [compiled, i, null];
104
107
  }
105
108
  export function compileTranslation(msgStr, fallback) {
106
109
  if (!msgStr) {
107
110
  return fallback;
108
111
  }
109
- try {
110
- const [compiled] = compile(msgStr);
111
- if (compiled.length === 1 && typeof compiled[0] === 'string') {
112
- return compiled[0];
113
- }
114
- return compiled;
115
- }
116
- catch (err) {
117
- console.error(err);
118
- console.error(msgStr);
112
+ const [compiled, , err] = compile(msgStr);
113
+ if (err !== null) {
114
+ console.error('Compile error:', err, ':', msgStr);
119
115
  return fallback;
120
116
  }
117
+ if (compiled.length === 1 && typeof compiled[0] === 'string') {
118
+ return compiled[0];
119
+ }
120
+ return compiled;
121
121
  }
122
122
  export function isEquivalent(source, translation) {
123
123
  const sourceStr = typeof source === 'string';
package/dist/fs.d.ts CHANGED
@@ -3,6 +3,7 @@ export type FS = {
3
3
  write(file: string, content: string): void | Promise<void>;
4
4
  mkdir(path: string): void | Promise<void>;
5
5
  exists(path: string): boolean | Promise<boolean>;
6
+ unlink(path: string): void | Promise<void>;
6
7
  };
7
8
  export declare const defaultFS: FS;
8
9
  export declare const readOnlyFS: FS;
package/dist/fs.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, statfs, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, statfs, unlink, writeFile } from 'node:fs/promises';
2
2
  export const defaultFS = {
3
3
  async read(file) {
4
4
  return await readFile(file, 'utf-8');
@@ -21,9 +21,13 @@ export const defaultFS = {
21
21
  return false;
22
22
  }
23
23
  },
24
+ async unlink(path) {
25
+ await unlink(path);
26
+ },
24
27
  };
25
28
  export const readOnlyFS = {
26
29
  ...defaultFS,
27
30
  write: () => { },
28
31
  mkdir: () => { },
32
+ unlink: () => { },
29
33
  };
@@ -12,7 +12,7 @@ export type ManifestEntryObj = {
12
12
  export type ManifestEntry = string | string[] | ManifestEntryObj | null;
13
13
  export declare const objKeyLocale: (locale: string) => string;
14
14
  export declare function normalizeSep(path: string): string;
15
- export declare function globConfToArgs(conf: GlobConf, cwd: string, localesDir: string, outDir?: string): [string[], {
15
+ export declare function globConfToArgs(conf: GlobConf, root: string, localesDir: string, outDir?: string): [string[], {
16
16
  ignore: string[];
17
17
  }];
18
18
  export declare class Files {
@@ -11,10 +11,10 @@ export function normalizeSep(path) {
11
11
  }
12
12
  return path.replaceAll('\\', '/');
13
13
  }
14
- export function globConfToArgs(conf, cwd, localesDir, outDir) {
14
+ export function globConfToArgs(conf, root, localesDir, outDir) {
15
15
  let patterns = [];
16
16
  // ignore generated files
17
- const options = { ignore: [localesDir], cwd };
17
+ const options = { ignore: [`${localesDir}/**/*`], cwd: root };
18
18
  if (outDir) {
19
19
  options.ignore.push(outDir);
20
20
  }
@@ -51,23 +51,23 @@ export class Files {
51
51
  proxySyncPath;
52
52
  #urlManifestFname;
53
53
  #urlsFname;
54
- #localesDir;
54
+ #localesDirAbs;
55
55
  #projectRoot;
56
56
  constructor(adapter, key, localesDir, fs, root) {
57
57
  this.key = key;
58
58
  this.#adapter = adapter;
59
- this.#localesDir = localesDir;
59
+ this.#localesDirAbs = resolve(root, localesDir);
60
60
  this.#fs = fs;
61
61
  this.#projectRoot = root;
62
62
  }
63
63
  getLoaderPaths() {
64
- const loaderPathHead = resolve(this.#localesDir, `${this.key}.loader`);
64
+ const loaderPathHead = resolve(this.#localesDirAbs, `${this.key}.loader`);
65
65
  const paths = [];
66
66
  for (const ext of this.#adapter.loaderExts) {
67
67
  const pathClient = loaderPathHead + ext;
68
68
  const same = { client: pathClient, server: pathClient };
69
69
  const diff = { client: pathClient, server: loaderPathHead + '.server' + ext };
70
- if (this.#adapter.defaultLoaderPath == null) {
70
+ if (this.#adapter.defaultLoaderPath === null) {
71
71
  paths.push(diff, same);
72
72
  }
73
73
  else if (typeof this.#adapter.defaultLoaderPath === 'string') {
@@ -95,6 +95,18 @@ export class Files {
95
95
  }
96
96
  return path;
97
97
  }
98
+ if (this.#adapter.defaultLoaderPath === null) {
99
+ const loaderForms = paths
100
+ .map(p => {
101
+ let f = ` ${relative(this.#projectRoot, p.client)}`;
102
+ if (p.server !== p.client) {
103
+ f += ` (and ${relative(this.#projectRoot, p.server)})`;
104
+ }
105
+ return f;
106
+ })
107
+ .join('\n');
108
+ throw new Error(`Custom loader specified for adapter '${this.key}' but no loader file exists in one of the forms:\n${loaderForms}`);
109
+ }
98
110
  return paths[0];
99
111
  }
100
112
  #proxyFileName(sync = false) {
@@ -106,14 +118,14 @@ export class Files {
106
118
  }
107
119
  async #initPaths() {
108
120
  this.loaderPath = await this.getLoaderPath();
109
- this.proxyPath = resolve(this.#localesDir, generatedDir, this.#proxyFileName());
110
- this.proxySyncPath = resolve(this.#localesDir, generatedDir, this.#proxyFileName(true));
111
- this.#urlManifestFname = resolve(this.#localesDir, generatedDir, `${this.key}.urls.js`);
112
- this.#urlsFname = resolve(this.#localesDir, `${this.key}.url.js`);
121
+ this.proxyPath = resolve(this.#localesDirAbs, generatedDir, this.#proxyFileName());
122
+ this.proxySyncPath = resolve(this.#localesDirAbs, generatedDir, this.#proxyFileName(true));
123
+ this.#urlManifestFname = resolve(this.#localesDirAbs, generatedDir, `${this.key}.urls.js`);
124
+ this.#urlsFname = resolve(this.#localesDirAbs, `${this.key}.url.js`);
113
125
  }
114
126
  getCompiledFilePath(loc, id) {
115
127
  const ownerKey = this.ownerKey;
116
- return resolve(this.#localesDir, generatedDir, `${ownerKey}.${id ?? ownerKey}.${loc}.compiled.js`);
128
+ return resolve(this.#localesDirAbs, generatedDir, `${ownerKey}.${id ?? ownerKey}.${loc}.compiled.js`);
117
129
  }
118
130
  getImportPath(filename, importer) {
119
131
  const relTo = importer ? resolve(this.#projectRoot, importer) : filename;
@@ -211,7 +223,7 @@ export class Files {
211
223
  };
212
224
  getManifestFilePath(id) {
213
225
  const ownerKey = this.ownerKey;
214
- return resolve(this.#localesDir, generatedDir, `${ownerKey}.${id ?? ownerKey}.manifest.js`);
226
+ return resolve(this.#localesDirAbs, generatedDir, `${ownerKey}.${id ?? ownerKey}.manifest.js`);
215
227
  }
216
228
  writeManifest = async (keys, id) => {
217
229
  const content = `/** @type {(string | {text: string | string[], context?: string, isUrl?: boolean} | null)[]} */\n` +
@@ -85,7 +85,9 @@ export class AdapterHandler {
85
85
  await this.sharedState.save();
86
86
  };
87
87
  compile = async (hmrVersion = -1) => {
88
- await Promise.all(this.#config.locales.map(loc => this.#compileForLocale(loc, hmrVersion)));
88
+ // for proper fallback
89
+ const localesOrdered = [this.sourceLocale, ...this.#config.locales.filter(l => l !== this.sourceLocale)];
90
+ await Promise.all(localesOrdered.map(loc => this.#compileForLocale(loc, hmrVersion)));
89
91
  await this.#writeManifests();
90
92
  };
91
93
  #buildManifest = (indices) => {
package/dist/hub.d.ts CHANGED
@@ -43,9 +43,10 @@ type CheckResult = {
43
43
  errors: CheckErrorItem[];
44
44
  syncs: string[];
45
45
  };
46
+ type TransformErrFormatter = (e: Error, adapterKey: string, filename: string) => Error;
46
47
  export declare class Hub {
47
48
  #private;
48
- constructor(loadConfig: ConfigLoader, root: string, hmrDelayThreshold?: number, fs?: FS);
49
+ constructor(loadConfig: ConfigLoader, root: string, hmrDelayThreshold?: number, fs?: FS, formatTransformErr?: TransformErrFormatter);
49
50
  init: (mode: Mode) => Promise<void>;
50
51
  onFileChange: (file: string, read: () => string | Promise<string>) => Promise<FileChangeInfo | undefined>;
51
52
  transform: (code: string, filePath: string, forServer?: boolean) => ReturnType<AdapterHandler["transform"]>;
package/dist/hub.js CHANGED
@@ -6,7 +6,7 @@ import { watch as watchFS } from 'chokidar';
6
6
  import {} from 'picomatch';
7
7
  import { glob } from 'tinyglobby';
8
8
  import { compileTranslation, isEquivalent } from './compile.js';
9
- import { defaultFS, readOnlyFS } from './fs.js';
9
+ import { defaultFS } from './fs.js';
10
10
  import { dataFileName, generatedDir, globConfToArgs, normalizeSep } from './handler/files.js';
11
11
  import { AdapterHandler } from './handler/index.js';
12
12
  import { SharedState } from './handler/state.js';
@@ -33,20 +33,23 @@ export class Hub {
33
33
  #log;
34
34
  #mode;
35
35
  #loadConfig;
36
+ #formatTransformErr = e => e;
36
37
  #hmrVersion = -1;
37
38
  #hmrDelayThreshold;
38
39
  #lastSourceTriggeredCatalogWrite = 0;
39
- constructor(loadConfig, root, hmrDelayThreshold = 1000, fs = defaultFS) {
40
+ constructor(loadConfig, root, hmrDelayThreshold = 1000, fs = defaultFS, formatTransformErr) {
40
41
  this.#loadConfig = loadConfig;
41
42
  this.#fs = fs;
42
43
  this.#projectRoot = root;
43
44
  // threshold to consider po file change is manual edit instead of a sideeffect of editing code
44
45
  this.#hmrDelayThreshold = hmrDelayThreshold;
46
+ this.#formatTransformErr = formatTransformErr ?? this.#formatTransformErr;
45
47
  }
46
48
  async #initGenDirWithData() {
47
- await this.#fs.mkdir(resolve(this.#config.localesDir, generatedDir));
49
+ const localesDirAbs = resolve(this.#projectRoot, this.#config.localesDir);
50
+ await this.#fs.mkdir(resolve(localesDirAbs, generatedDir));
48
51
  // data file
49
- await this.#fs.write(resolve(this.#config.localesDir, dataFileName), [
52
+ await this.#fs.write(resolve(localesDirAbs, dataFileName), [
50
53
  `/** @typedef {('${this.#config.locales.join("'|'")}')} Locale */`,
51
54
  `/** @type {Locale[]} */`,
52
55
  `export const locales = ['${this.#config.locales.join("','")}']`,
@@ -105,7 +108,8 @@ export class Hub {
105
108
  }
106
109
  }
107
110
  }
108
- this.#confUpdateFile = normalizeSep(resolve(this.#config.localesDir, generatedDir, confUpdateName));
111
+ const confUpdateFileAbs = resolve(this.#projectRoot, this.#config.localesDir, generatedDir, confUpdateName);
112
+ this.#confUpdateFile = normalizeSep(confUpdateFileAbs);
109
113
  await this.#fs.write(this.#confUpdateFile, '{}'); // only watch changes so prepare first
110
114
  };
111
115
  #getSharedState = (adapter, key, sourceLocale, fileMatches) => {
@@ -114,10 +118,8 @@ export class Hub {
114
118
  root: this.#projectRoot,
115
119
  sourceLocale: sourceLocale,
116
120
  haveUrl: adapter.url != null,
121
+ fs: this.#fs,
117
122
  });
118
- if (this.#fs === readOnlyFS) {
119
- storage.save = async () => { }; // disable writes
120
- }
121
123
  let sharedState = this.#sharedStates.get(storage.key);
122
124
  if (sharedState == null) {
123
125
  sharedState = new SharedState(storage, key, sourceLocale);
@@ -132,6 +134,7 @@ export class Hub {
132
134
  return sharedState;
133
135
  };
134
136
  onFileChange = async (file, read) => {
137
+ file = normalizeSep(file); // just to be sure
135
138
  if (this.#confUpdateFile === file) {
136
139
  const update = JSON.parse(await read());
137
140
  this.#log.info(`${logPrefix} config update received: ${update}`);
@@ -184,7 +187,7 @@ export class Hub {
184
187
  if (this.#mode === 'dev' && !this.#config.hmr) {
185
188
  return [{}, false];
186
189
  }
187
- const filename = relative(this.#projectRoot, filePath);
190
+ const filename = normalizeSep(relative(this.#projectRoot, filePath));
188
191
  let output = null;
189
192
  let lastAdapterKey = null;
190
193
  for (const adapter of this.#handlers.values()) {
@@ -192,7 +195,12 @@ export class Hub {
192
195
  if (lastAdapterKey != null) {
193
196
  throw new Error(`${logPrefix} ${filename} matches both adapters ${lastAdapterKey} and ${adapter.key}`);
194
197
  }
195
- output = await adapter.transform(code, filename, this.#hmrVersion, forServer);
198
+ try {
199
+ output = await adapter.transform(code, filename, this.#hmrVersion, forServer);
200
+ }
201
+ catch (e) {
202
+ throw this.#formatTransformErr(e, adapter.key, filename);
203
+ }
196
204
  lastAdapterKey = adapter.key;
197
205
  }
198
206
  }
@@ -240,7 +248,8 @@ export class Hub {
240
248
  }
241
249
  }
242
250
  if (updated) {
243
- await handler.sharedState.save();
251
+ await handler.saveStorage();
252
+ await handler.compile();
244
253
  }
245
254
  return updated;
246
255
  }
@@ -248,7 +257,11 @@ export class Hub {
248
257
  !watch && this.#log.info('Extracting...');
249
258
  const handlers = Array.from(this.#handlers.values());
250
259
  // owner adapter handlers should run last for cleanup
251
- handlers.sort(h => (h.sharedState.ownerKey === h.key ? 1 : -1));
260
+ handlers.sort((a, b) => {
261
+ const aOwner = a.sharedState.ownerKey === a.key;
262
+ const bOwner = b.sharedState.ownerKey === b.key;
263
+ return aOwner === bOwner ? 0 : aOwner ? 1 : -1;
264
+ });
252
265
  // separate loop to make sure that all otherFileMatchers are collected
253
266
  for (const handler of handlers) {
254
267
  await this.#directVisitHandler(handler, clean, sync);
package/dist/pofile.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import PO from 'pofile';
2
2
  import { type PluralRule, type SaveData, type StorageFactory, type StorageFactoryOpts } from './storage.js';
3
- type POItem = InstanceType<typeof PO.Item>;
3
+ export type POItem = InstanceType<typeof PO.Item>;
4
4
  export type POFileOptions = {
5
5
  dir: string;
6
6
  separateUrls: boolean;
package/dist/pofile.js CHANGED
@@ -1,10 +1,17 @@
1
- import { mkdir } from 'node:fs/promises';
2
- import { isAbsolute, resolve } from 'node:path';
1
+ import { resolve } from 'node:path';
3
2
  import PO from 'pofile';
4
3
  import { getKey } from './adapters.js';
5
4
  import { deepMergeObjects } from './config.js';
6
5
  import { itemIsObsolete, itemIsUrl, } from './storage.js';
7
6
  const urlAdapterFlagPrefix = 'url:';
7
+ function join(parts, sep) {
8
+ return parts.map(s => s.replaceAll('\\', '\\\\').replaceAll(sep, `\\${sep}`)).join(sep);
9
+ }
10
+ function split(str, sep, count) {
11
+ return str
12
+ .split(new RegExp(`(?<!\\\\)${sep}`), count)
13
+ .map(s => s.replaceAll(`\\${sep}`, sep).replaceAll('\\\\', '\\'));
14
+ }
8
15
  function itemToPOItem(item, locale, sourceLocale) {
9
16
  const poi = new PO.Item();
10
17
  const id = item.translations.get(sourceLocale);
@@ -23,9 +30,9 @@ function itemToPOItem(item, locale, sourceLocale) {
23
30
  comm.push(frEntry.link);
24
31
  }
25
32
  for (const [i, ph] of frEntry.placeholders) {
26
- comm.push(`${i}: ${ph}`);
33
+ comm.push(join([String(i), ph], ': '));
27
34
  }
28
- return comm.join('; ');
35
+ return join(comm, '; ');
29
36
  }))
30
37
  .filter(c => c !== null);
31
38
  const additionals = item.additionals ?? new Map();
@@ -37,15 +44,7 @@ function itemToPOItem(item, locale, sourceLocale) {
37
44
  poi.obsolete = itemIsObsolete(item);
38
45
  return poi;
39
46
  }
40
- function getItemId(poi) {
41
- const msgid = [poi.msgid];
42
- if (poi.msgid_plural) {
43
- msgid.push(poi.msgid_plural);
44
- }
45
- return msgid;
46
- }
47
47
  function poitemToItemCommons(poi) {
48
- const msgid = getItemId(poi);
49
48
  const references = [];
50
49
  let lastRef = { file: '', refs: [] };
51
50
  const urlAdapters = [];
@@ -65,7 +64,7 @@ function poitemToItemCommons(poi) {
65
64
  continue;
66
65
  }
67
66
  const refEnt = { placeholders: [] };
68
- const commSp = comm.split('; ');
67
+ const commSp = split(comm, '; ');
69
68
  let phStart = 0;
70
69
  if (urlAdapters.length) {
71
70
  // url
@@ -73,19 +72,51 @@ function poitemToItemCommons(poi) {
73
72
  phStart++;
74
73
  }
75
74
  for (const c of commSp.slice(phStart)) {
76
- const [i, ph] = c.split(': ', 2);
75
+ const [i, ph] = split(c, ': ', 2);
77
76
  refEnt.placeholders.push([Number(i), ph]);
78
77
  }
79
78
  lastRef.refs.push(refEnt);
80
79
  }
81
80
  return {
82
- id: msgid,
83
81
  translations: new Map(),
84
82
  context: poi.msgctxt,
85
83
  references,
86
84
  urlAdapters,
87
85
  };
88
86
  }
87
+ function getItemId(poItem) {
88
+ const id = [poItem.msgid];
89
+ if (poItem.msgid_plural) {
90
+ id.push(poItem.msgid_plural);
91
+ }
92
+ return id;
93
+ }
94
+ function poitemsToItems(poItems, locales, sourceLocale) {
95
+ // then merge them
96
+ const items = [];
97
+ for (const poIs of poItems) {
98
+ const basePoOtem = poIs.values().next().value;
99
+ const item = poitemToItemCommons(basePoOtem);
100
+ const additionals = new Map();
101
+ for (const loc of locales) {
102
+ const poi = poIs.get(loc);
103
+ item.translations.set(loc, poi?.msgstr ?? (loc === sourceLocale ? getItemId(basePoOtem) : []));
104
+ const add = {
105
+ comments: poi?.comments ?? [],
106
+ flags: {},
107
+ };
108
+ for (const [k, v] of Object.entries(poi?.flags ?? {})) {
109
+ if (!k.startsWith(urlAdapterFlagPrefix)) {
110
+ add.flags[k] = v;
111
+ }
112
+ }
113
+ additionals.set(loc, add);
114
+ }
115
+ item.additionals = additionals;
116
+ items.push(item);
117
+ }
118
+ return items;
119
+ }
89
120
  export const defaultOpts = {
90
121
  dir: 'src/locales',
91
122
  separateUrls: true,
@@ -96,11 +127,9 @@ export class POFile {
96
127
  filesByLoc = new Map(); // main and url
97
128
  files = [];
98
129
  constructor(opts) {
99
- this.key = opts.dir;
100
130
  this.opts = opts;
101
- if (!isAbsolute(opts.dir)) {
102
- opts.dir = resolve(opts.root, opts.dir);
103
- }
131
+ opts.dir = resolve(opts.root, opts.dir);
132
+ this.key = opts.dir;
104
133
  for (const locale of opts.locales) {
105
134
  const locFiles = [resolve(opts.dir, `${locale}.po`), resolve(opts.dir, `${locale}.url.po`)];
106
135
  this.filesByLoc.set(locale, locFiles);
@@ -110,16 +139,7 @@ export class POFile {
110
139
  async loadRaw(locale, url) {
111
140
  const filename = this.filesByLoc.get(locale)[Number(url)];
112
141
  try {
113
- return await new Promise((res, rej) => {
114
- PO.load(filename, (err, po) => {
115
- if (err) {
116
- rej(err);
117
- }
118
- else {
119
- res(po);
120
- }
121
- });
122
- });
142
+ return PO.parse(await this.opts.fs.read(filename));
123
143
  }
124
144
  catch (err) {
125
145
  if (err.code !== 'ENOENT') {
@@ -156,46 +176,24 @@ export class POFile {
156
176
  poItems.get(key)?.set(locale, poItem);
157
177
  }
158
178
  }
159
- // then merge them
160
- const items = [];
161
- for (const poIs of poItems.values()) {
162
- const item = poitemToItemCommons(poIs.get(this.opts.sourceLocale));
163
- const additionals = new Map();
164
- for (const loc of this.opts.locales) {
165
- const poi = poIs.get(loc);
166
- item.translations.set(loc, poi?.msgstr ?? []);
167
- const add = {
168
- comments: poi?.comments ?? [],
169
- flags: {},
170
- };
171
- for (const [k, v] of Object.entries(poi?.flags ?? {})) {
172
- if (!k.startsWith(urlAdapterFlagPrefix)) {
173
- add.flags[k] = v;
174
- }
175
- }
176
- additionals.set(loc, add);
177
- }
178
- item.additionals = additionals;
179
- items.push(item);
180
- }
181
- return { items, pluralRules };
179
+ return {
180
+ items: poitemsToItems(poItems.values(), this.opts.locales, this.opts.sourceLocale),
181
+ pluralRules,
182
+ };
182
183
  }
183
184
  async saveRaw(items, headers, locale, url) {
185
+ const filename = this.filesByLoc.get(locale)[Number(url)];
186
+ if (items.length === 0) {
187
+ if (await this.opts.fs.exists(filename)) {
188
+ await this.opts.fs.unlink(filename);
189
+ }
190
+ return;
191
+ }
184
192
  const po = new PO();
185
193
  po.headers = headers;
186
194
  po.items = items;
187
- const filename = this.filesByLoc.get(locale)[Number(url)];
188
- await mkdir(this.opts.dir, { recursive: true });
189
- await new Promise((res, rej) => {
190
- po.save(filename, err => {
191
- if (err) {
192
- rej(err);
193
- }
194
- else {
195
- res();
196
- }
197
- });
198
- });
195
+ await this.opts.fs.mkdir(this.opts.dir);
196
+ await this.opts.fs.write(filename, po.toString());
199
197
  }
200
198
  async save(data) {
201
199
  for (const locale of this.opts.locales) {
@@ -212,9 +210,7 @@ export class POFile {
212
210
  }
213
211
  const headers = this.getHeaders(locale, data.pluralRules.get(locale));
214
212
  await this.saveRaw(poItems, headers, locale, false);
215
- if (poItemsUrl.length > 0) {
216
- await this.saveRaw(poItemsUrl, headers, locale, true);
217
- }
213
+ await this.saveRaw(poItemsUrl, headers, locale, true);
218
214
  }
219
215
  }
220
216
  getHeaders(locale, pluralRule) {
package/dist/runtime.js CHANGED
@@ -49,22 +49,17 @@ export default function toRuntime(mod = { [catalogVarName]: [] }, locale) {
49
49
  /** for tagged template strings */
50
50
  rt.t = (tag, id, args) => {
51
51
  const ctx = getCompositeContext(id);
52
- const strings = [];
52
+ const strings = [''];
53
53
  const exprs = [];
54
- if (typeof ctx[0] === 'number') {
55
- strings.push('');
56
- }
57
54
  for (const x of ctx) {
58
55
  if (typeof x === 'string') {
59
- strings.push(x);
56
+ strings[strings.length - 1] += x;
60
57
  continue;
61
58
  }
62
59
  exprs.push(args?.[x]);
63
- }
64
- if (typeof ctx.at(-1) === 'number') {
65
60
  strings.push('');
66
61
  }
67
- return tag(strings, ...exprs);
62
+ return tag(Object.assign(strings, { raw: strings }), ...exprs);
68
63
  };
69
64
  /** get translation for plural */
70
65
  rt.p = (id) => catalog[id] ?? [];
package/dist/storage.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { FS } from './fs.js';
1
2
  export type FileRefEntry = {
2
3
  link?: string;
3
4
  placeholders: [number, string][];
@@ -54,5 +55,6 @@ export type StorageFactoryOpts = {
54
55
  /** whether the url is configured, can use to load separate url files */
55
56
  haveUrl: boolean;
56
57
  sourceLocale: string;
58
+ fs: FS;
57
59
  };
58
60
  export type StorageFactory = (opts: StorageFactoryOpts) => CatalogStorage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wuchale",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "description": "Protobuf-like i18n from plain code",
5
5
  "scripts": {
6
6
  "dev": "tsc --watch",