@feugene/fint-i18n 0.1.0 → 0.2.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
@@ -1,5 +1,11 @@
1
1
  # @feugene/fint-i18n
2
2
 
3
+ [![codecov](https://codecov.io/gh/efureev/fint-i18n/branch/main/graph/badge.svg)](https://codecov.io/gh/efureev/fint-i18n)
4
+ [![npm version](https://badge.fury.io/js/@feugene%2Ffint-i18n.svg)](https://badge.fury.io/js/@feugene%2Ffint-i18n)
5
+ [![bundle size](https://badgen.net/bundlephobia/minzip/@feugene/fint-i18n)](https://bundlephobia.com/package/@feugene/fint-i18n)
6
+ [![dependency count](https://badgen.net/bundlephobia/dependency-count/@feugene/fint-i18n)](https://bundlephobia.com/package/@feugene/fint-i18n)
7
+
8
+
3
9
  [Русская версия (Russian version)](./README.ru.md)
4
10
 
5
11
  Localization library for Vue 3 with lazy-loading blocks, template compilation, and extensible plugins.
package/README.ru.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # @feugene/fint-i18n
2
2
 
3
+ [![codecov](https://codecov.io/gh/efureev/fint-i18n/branch/main/graph/badge.svg)](https://codecov.io/gh/efureev/fint-i18n)
4
+ [![npm version](https://badge.fury.io/js/@feugene%2Ffint-i18n.svg)](https://badge.fury.io/js/@feugene%2Ffint-i18n)
5
+
3
6
  Библиотека локализации для Vue 3 с ленивой загрузкой блоков, компиляцией шаблонов и расширяемыми плагинами.
4
7
 
5
8
  ## Особенности
@@ -59,10 +59,46 @@ var a = class {
59
59
  }
60
60
  return r;
61
61
  }
62
- }, o = class {
62
+ }, o = ".**", s = ".*";
63
+ function c(e) {
64
+ return e.endsWith(s) || e.endsWith(o);
65
+ }
66
+ var l = class {
63
67
  loaders;
68
+ knownBlockNames;
64
69
  constructor(e) {
65
- this.loaders = this.normalize(e);
70
+ this.loaders = this.normalize(e), this.knownBlockNames = this.collectKnownBlockNames(this.loaders);
71
+ }
72
+ getKnownBlockNames() {
73
+ return this.knownBlockNames;
74
+ }
75
+ expandPattern(e) {
76
+ let t = e.endsWith(o), n = !t && e.endsWith(s);
77
+ if (!t && !n) return [];
78
+ let r = e.slice(0, -(t ? 3 : 2));
79
+ if (!r) return [];
80
+ let i = `${r}.`, a = i.length, c = [];
81
+ for (let e = 0; e < this.knownBlockNames.length; e++) {
82
+ let n = this.knownBlockNames[e];
83
+ if (n.length <= a) continue;
84
+ let r = !0;
85
+ for (let e = 0; e < a; e++) if (n.charCodeAt(e) !== i.charCodeAt(e)) {
86
+ r = !1;
87
+ break;
88
+ }
89
+ if (r) {
90
+ if (!t) {
91
+ let e = !1;
92
+ for (let t = a; t < n.length; t++) if (n.charCodeAt(t) === 46) {
93
+ e = !0;
94
+ break;
95
+ }
96
+ if (e) continue;
97
+ }
98
+ c.push(n);
99
+ }
100
+ }
101
+ return c;
66
102
  }
67
103
  resolve(e, t) {
68
104
  let n = this.loaders[e];
@@ -106,23 +142,31 @@ var a = class {
106
142
  normalizeEntry(e) {
107
143
  return Array.isArray(e) ? e : [e];
108
144
  }
145
+ collectKnownBlockNames(e) {
146
+ let t = /* @__PURE__ */ new Set();
147
+ for (let n in e) {
148
+ let r = e[n];
149
+ for (let e in r) t.add(e);
150
+ }
151
+ return Array.from(t);
152
+ }
109
153
  };
110
154
  //#endregion
111
155
  //#region src/core/message-utils.ts
112
- function s(e) {
156
+ function u(e) {
113
157
  return !!e && typeof e == "object" && !Array.isArray(e);
114
158
  }
115
- function c(e, t) {
116
- for (let n in t) s(t[n]) ? (s(e[n]) || (e[n] = {}), c(e[n], t[n])) : e[n] = t[n];
159
+ function d(e, t) {
160
+ for (let n in t) u(t[n]) ? (u(e[n]) || (e[n] = {}), d(e[n], t[n])) : e[n] = t[n];
117
161
  }
118
- function l(e, n) {
119
- if (s(e) && s(n)) {
162
+ function f(e, n) {
163
+ if (u(e) && u(n)) {
120
164
  let r = t({});
121
- return c(r, e), c(r, n), r;
165
+ return d(r, e), d(r, n), r;
122
166
  }
123
167
  return n;
124
168
  }
125
- function u(e, t) {
169
+ function p(e, t) {
126
170
  let n = e, r = 0;
127
171
  for (; n && typeof n == "object";) {
128
172
  let e = t.indexOf(".", r), i = e === -1 ? t.slice(r) : t.slice(r, e);
@@ -132,7 +176,7 @@ function u(e, t) {
132
176
  }
133
177
  //#endregion
134
178
  //#region src/core/translate-params.ts
135
- function d(t) {
179
+ function m(t) {
136
180
  if (!t) return;
137
181
  let n;
138
182
  for (let r in t) {
@@ -143,7 +187,7 @@ function d(t) {
143
187
  }
144
188
  //#endregion
145
189
  //#region src/core/instance.ts
146
- var f = class {
190
+ var h = class {
147
191
  locale;
148
192
  fallbackLocale;
149
193
  globalInstall;
@@ -153,11 +197,12 @@ var f = class {
153
197
  loadingBlocks = /* @__PURE__ */ new Map();
154
198
  loadedBlocks = /* @__PURE__ */ new Map();
155
199
  blockUsageCounters = /* @__PURE__ */ new Map();
200
+ patternExpansionCache = /* @__PURE__ */ new Map();
156
201
  pendingUsedBlockLoads = /* @__PURE__ */ new Map();
157
202
  skipNextUsedBlockLoadLocale = null;
158
203
  hooks = new a();
159
204
  constructor(e) {
160
- this.locale = n(e.locale), this.fallbackLocale = e.fallbackLocale || "", this.loaderRegistry = new o(e.loaders), this.globalInstall = e.globalInstall !== !1, e.plugins && e.plugins.forEach((e) => e.install(this)), r(this.locale, (e, t) => {
205
+ this.locale = n(e.locale), this.fallbackLocale = e.fallbackLocale || "", this.loaderRegistry = new l(e.loaders), this.globalInstall = e.globalInstall !== !1, e.plugins && e.plugins.forEach((e) => e.install(this)), r(this.locale, (e, t) => {
161
206
  if (e !== t) {
162
207
  if (this.skipNextUsedBlockLoadLocale === e) {
163
208
  this.skipNextUsedBlockLoadLocale = null;
@@ -168,11 +213,11 @@ var f = class {
168
213
  }), this.hooks.emitSync("afterInit", void 0);
169
214
  }
170
215
  t = (e, t, n) => {
171
- let r = this.locale.value, i = d(t), a = this.hooks.emitSync("onTranslate", {
216
+ let r = this.locale.value, i = m(t), a = this.hooks.emitSync("onTranslate", {
172
217
  key: e,
173
218
  params: i,
174
- result: this.resolve(r, e, i) || e
175
- }).result;
219
+ result: this.resolve(r, e, i)
220
+ }).result ?? e;
176
221
  if (a === e) {
177
222
  let t = n?.fallbackLocale || this.fallbackLocale;
178
223
  if (t && t !== r) {
@@ -193,7 +238,7 @@ var f = class {
193
238
  if (r) return r(n);
194
239
  let a = this.messages[e];
195
240
  if (!a) return;
196
- let o = u(a, t);
241
+ let o = p(a, t);
197
242
  if (typeof o == "string") {
198
243
  let r = i(o);
199
244
  return this.setCompiled(e, t, r), r(n);
@@ -207,8 +252,24 @@ var f = class {
207
252
  setCompiled = (e, t, n) => {
208
253
  this.compiledMessages[e] || (this.compiledMessages[e] = Object.create(null)), this.compiledMessages[e][t] = n;
209
254
  };
255
+ expandPattern = (e) => {
256
+ let t = this.patternExpansionCache.get(e);
257
+ if (t) return t;
258
+ let n = this.loaderRegistry.expandPattern(e);
259
+ return this.patternExpansionCache.set(e, n), n;
260
+ };
210
261
  loadBlock = async (e, t) => {
211
- let n = t || this.locale.value, r = `${n}:${e}`;
262
+ let n = t || this.locale.value;
263
+ if (c(e)) {
264
+ let t = this.expandPattern(e);
265
+ if (t.length === 0) {
266
+ console.warn(`[fint-i18n] Pattern "${e}" did not match any registered block (locale "${n}")`);
267
+ return;
268
+ }
269
+ await Promise.all(t.map((e) => this.loadBlock(e, n)));
270
+ return;
271
+ }
272
+ let r = `${n}:${e}`;
212
273
  if (this.isBlockLoaded(e, n)) return;
213
274
  if (this.loadingBlocks.has(r)) return this.loadingBlocks.get(r);
214
275
  await this.hooks.emit("beforeLoadBlock", e);
@@ -222,7 +283,7 @@ var f = class {
222
283
  let r;
223
284
  for (let e of t.loaders) {
224
285
  let i = await e(), a = i.default || i;
225
- this.mergeMessages(n, t.resolvedBlockName, a), r = r === void 0 ? a : l(r, a);
286
+ this.mergeMessages(n, t.resolvedBlockName, a), r = r === void 0 ? a : f(r, a);
226
287
  }
227
288
  this.markBlockLoaded(t.resolvedBlockName, n), await this.hooks.emit("afterLoadBlock", {
228
289
  block: t.resolvedBlockName,
@@ -238,7 +299,7 @@ var f = class {
238
299
  mergeMessages = (e, n, r) => {
239
300
  this.messages[e] || (this.messages[e] = t({}));
240
301
  let i = n.split("."), a = i[0];
241
- if (i.length === 1) s(r) ? ((!this.messages[e][a] || typeof this.messages[e][a] != "object") && (this.messages[e][a] = t({})), c(this.messages[e][a], r)) : this.messages[e][a] = r;
302
+ if (i.length === 1) u(r) ? ((!this.messages[e][a] || typeof this.messages[e][a] != "object") && (this.messages[e][a] = t({})), d(this.messages[e][a], r)) : this.messages[e][a] = r;
242
303
  else {
243
304
  (!this.messages[e][a] || typeof this.messages[e][a] != "object") && (this.messages[e][a] = t({}));
244
305
  let n = this.messages[e][a];
@@ -246,7 +307,7 @@ var f = class {
246
307
  let r = i[e];
247
308
  (!n[r] || typeof n[r] != "object") && (n[r] = t({})), n = n[r];
248
309
  }
249
- c(n, r);
310
+ d(n, r);
250
311
  }
251
312
  this.precompileBlock(e, n, r);
252
313
  };
@@ -260,12 +321,12 @@ var f = class {
260
321
  this.setCompiled(e, t, n);
261
322
  return;
262
323
  }
263
- if (s(n)) for (let r in n) {
324
+ if (u(n)) for (let r in n) {
264
325
  let a = n[r], o = `${t}.${r}`;
265
326
  if (typeof a == "string") {
266
327
  let t = i(a);
267
328
  this.setCompiled(e, o, t);
268
- } else typeof a == "function" ? this.setCompiled(e, o, a) : s(a) && this.precompileBlock(e, o, a);
329
+ } else typeof a == "function" ? this.setCompiled(e, o, a) : u(a) && this.precompileBlock(e, o, a);
269
330
  }
270
331
  };
271
332
  isBlockLoaded = (e, t) => {
@@ -305,19 +366,39 @@ var f = class {
305
366
  }));
306
367
  };
307
368
  registerUsage = (e) => {
308
- let t = this.blockUsageCounters.get(e) || 0;
309
- this.blockUsageCounters.set(e, t + 1);
369
+ if (c(e)) {
370
+ let t = this.expandPattern(e);
371
+ if (t.length === 0) {
372
+ console.warn(`[fint-i18n] Pattern "${e}" did not match any registered block`);
373
+ return;
374
+ }
375
+ for (let e = 0; e < t.length; e++) this.incrementUsage(t[e]);
376
+ return;
377
+ }
378
+ this.incrementUsage(e);
310
379
  };
311
380
  registerBlocks = (e) => {
312
- e.forEach((e) => this.registerUsage(e));
381
+ for (let t = 0; t < e.length; t++) this.registerUsage(e[t]);
313
382
  };
314
383
  unregisterUsage = (e) => {
384
+ if (c(e)) {
385
+ let t = this.expandPattern(e);
386
+ for (let e = 0; e < t.length; e++) this.decrementUsage(t[e]);
387
+ return;
388
+ }
389
+ this.decrementUsage(e);
390
+ };
391
+ incrementUsage = (e) => {
392
+ let t = this.blockUsageCounters.get(e) || 0;
393
+ this.blockUsageCounters.set(e, t + 1);
394
+ };
395
+ decrementUsage = (e) => {
315
396
  let t = this.blockUsageCounters.get(e) || 0;
316
397
  t <= 1 ? this.blockUsageCounters.delete(e) : this.blockUsageCounters.set(e, t - 1);
317
398
  };
318
399
  };
319
- function p(e) {
320
- return new f(e);
400
+ function g(e) {
401
+ return new h(e);
321
402
  }
322
403
  //#endregion
323
- export { i, p as n, a as r, f as t };
404
+ export { i, g as n, a as r, h as t };
package/dist/core.js CHANGED
@@ -1,2 +1,2 @@
1
- import { i as e, n as t, r as n, t as r } from "./chunks/core-Dbw0feTs.js";
1
+ import { i as e, n as t, r as n, t as r } from "./chunks/core-gGqMvw8y.js";
2
2
  export { r as FintI18n, n as HookManager, e as compileTemplate, t as createFintI18n };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { i as e, n as t, r as n, t as r } from "./chunks/core-Dbw0feTs.js";
1
+ import { i as e, n as t, r as n, t as r } from "./chunks/core-gGqMvw8y.js";
2
2
  import { FINT_I18N_KEY as i, createVTDirective as a, installI18n as o, useFintI18n as s, useI18nScope as c } from "./vue.js";
3
3
  import { BridgePlugin as l, HookLoggerPlugin as u, PersistencePlugin as d } from "./plugins.js";
4
4
  export { l as BridgePlugin, i as FINT_I18N_KEY, r as FintI18n, u as HookLoggerPlugin, n as HookManager, d as PersistencePlugin, e as compileTemplate, t as createFintI18n, a as createVTDirective, o as installI18n, s as useFintI18n, c as useI18nScope };
package/dist/plugins.js CHANGED
@@ -12,7 +12,7 @@ var t = class {
12
12
  }
13
13
  install(e) {
14
14
  let t = this.options.key, n = this.options.storage || (typeof window < "u" ? window.localStorage : void 0);
15
- if (!n) return;
15
+ if (!n || typeof n.getItem != "function") return;
16
16
  let r = n.getItem(t);
17
17
  r && (e.locale.value = r), e.hooks.on("onLocaleChange", ({ locale: e }) => {
18
18
  n.setItem(t, e);
@@ -21,7 +21,7 @@ export interface FintI18nHooks {
21
21
  'onTranslate': HookFn<{
22
22
  key: string;
23
23
  params?: any;
24
- result: string;
24
+ result: string | undefined;
25
25
  }>;
26
26
  }
27
27
  export declare class HookManager {
@@ -11,6 +11,7 @@ export declare class FintI18n {
11
11
  private loadingBlocks;
12
12
  private loadedBlocks;
13
13
  private blockUsageCounters;
14
+ private patternExpansionCache;
14
15
  private pendingUsedBlockLoads;
15
16
  private skipNextUsedBlockLoadLocale;
16
17
  hooks: HookManager;
@@ -18,6 +19,12 @@ export declare class FintI18n {
18
19
  t: (key: string, params?: Record<string, any>, options?: TranslateOptions) => string;
19
20
  private resolve;
20
21
  private setCompiled;
22
+ /**
23
+ * Развернуть wildcard-паттерн в список конкретных имён блоков.
24
+ * Результат кэшируется по строке паттерна (набор лоадеров неизменен).
25
+ * Не-паттерны возвращают пустой массив.
26
+ */
27
+ private expandPattern;
21
28
  loadBlock: (blockName: string, locale?: Locale) => Promise<void>;
22
29
  mergeMessages: (locale: Locale, blockName: string, messages: MessageValue) => void;
23
30
  private precompileBlock;
@@ -25,8 +32,22 @@ export declare class FintI18n {
25
32
  markBlockLoaded: (blockName: string, locale: Locale) => void;
26
33
  loadUsedBlocks: (locale: Locale) => Promise<void>;
27
34
  setLocale: (newLocale: Locale) => Promise<void>;
35
+ /**
36
+ * Зарегистрировать использование блока.
37
+ *
38
+ * Поддерживается wildcard-паттерн (`prefix.*`, `prefix.**`) — он разворачивается в
39
+ * конкретные имена зарегистрированных блоков (один раз, с кэшированием), и счётчик
40
+ * увеличивается для каждого из них. Если паттерн не дал совпадений — выводится warning.
41
+ */
28
42
  registerUsage: (blockName: string) => void;
29
43
  registerBlocks: (blockNames: string[]) => void;
44
+ /**
45
+ * Снять регистрацию использования блока.
46
+ * Wildcard-паттерн раскрывается тем же кэшем, что и в `registerUsage`,
47
+ * поэтому снимаются счётчики ровно у тех же child-блоков.
48
+ */
30
49
  unregisterUsage: (blockName: string) => void;
50
+ private incrementUsage;
51
+ private decrementUsage;
31
52
  }
32
53
  export declare function createFintI18n(options: FintI18nOptions): FintI18n;
@@ -3,11 +3,26 @@ export interface ResolvedLocaleBlockLoaders {
3
3
  resolvedBlockName: string;
4
4
  loaders: LocaleBlockLoader[];
5
5
  }
6
+ export declare function isBlockPattern(name: string): boolean;
6
7
  export declare class LocaleLoaderRegistry {
7
8
  private readonly loaders;
9
+ private readonly knownBlockNames;
8
10
  constructor(source?: LocaleLoaderSource);
11
+ getKnownBlockNames(): readonly string[];
12
+ /**
13
+ * Развернуть wildcard-паттерн в список конкретных имён блоков.
14
+ *
15
+ * Поддерживаемые формы:
16
+ * - `prefix.*` — все блоки, у которых после `prefix.` ровно один сегмент (без точек).
17
+ * - `prefix.**` — все блоки, начинающиеся с `prefix.` (любая глубина).
18
+ *
19
+ * Если паттерн некорректный или ничего не совпало — возвращается пустой массив.
20
+ * Сам литерал родителя (`prefix`) в результат НЕ включается.
21
+ */
22
+ expandPattern(pattern: string): string[];
9
23
  resolve(locale: Locale, blockName: string): ResolvedLocaleBlockLoaders | null;
10
24
  private normalize;
11
25
  private mergeCollection;
12
26
  private normalizeEntry;
27
+ private collectKnownBlockNames;
13
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feugene/fint-i18n",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Lightweight Vue 3 i18n library with lazy-loading blocks and template caching",
5
5
  "author": "feugene <feugene@example.com>",
6
6
  "license": "MIT",
@@ -21,10 +21,6 @@
21
21
  "translation"
22
22
  ],
23
23
  "type": "module",
24
- "publishConfig": {
25
- "access": "public",
26
- "registry": "https://registry.npmjs.org/"
27
- },
28
24
  "files": [
29
25
  "dist"
30
26
  ],
@@ -51,21 +47,23 @@
51
47
  "vue": "^3.5.32"
52
48
  },
53
49
  "devDependencies": {
54
- "@antfu/eslint-config": "^7.7.3",
50
+ "@antfu/eslint-config": "^8.1.0",
51
+ "@codecov/vite-plugin": "^1.9.1",
55
52
  "@iconify-json/lucide": "^1.2.101",
56
53
  "@types/node": "^25.5.2",
57
- "@unocss/eslint-plugin": "^66.6.7",
58
- "@unocss/reset": "^66.6.7",
54
+ "@unocss/eslint-plugin": "^66.6.8",
55
+ "@unocss/reset": "^66.6.8",
59
56
  "@vitejs/plugin-vue": "^6.0.5",
57
+ "@vitest/coverage-v8": "^4.1.3",
60
58
  "@vue/test-utils": "^2.4.6",
61
59
  "eslint": "^10.2.0",
62
60
  "globals": "^17.4.0",
63
- "jsdom": "^29.0.1",
61
+ "jsdom": "^29.0.2",
64
62
  "rollup-plugin-visualizer": "^7.0.1",
65
- "typescript": "^5.9.3",
66
- "unocss": "^66.6.7",
63
+ "typescript": "^6.0.2",
64
+ "unocss": "^66.6.8",
67
65
  "vite": "^8.0.5",
68
- "vitest": "^4.1.2",
66
+ "vitest": "^4.1.3",
69
67
  "vue": "^3.5.32",
70
68
  "vue-tsc": "^3.2.6"
71
69
  },
@@ -76,10 +74,13 @@
76
74
  "build:analyze": "ANALYZE=true vite build && vue-tsc -p tsconfig.build.json",
77
75
  "build": "vite build && vue-tsc -p tsconfig.build.json",
78
76
  "prepublishOnly": "yarn build",
79
- "bench": "mkdir -p dist/analysis && vitest bench --run --config vite.config.ts --environment node --outputJson dist/analysis/bench-results.json bench/core.bench.ts",
77
+ "bench": "mkdir -p dist/analysis && vitest bench --run --config vitest.config.ts --environment node --outputJson dist/analysis/bench-results.json bench/core.bench.ts",
80
78
  "type-check": "vue-tsc --noEmit",
81
79
  "test": "vitest",
82
80
  "test:run": "vitest run",
81
+ "test:coverage": "yarn test:coverage:package && yarn test:coverage:playground",
82
+ "test:coverage:package": "vitest run --coverage",
83
+ "test:coverage:playground": "vitest run --config playground/vitest.config.ts --coverage",
83
84
  "lint": "eslint .",
84
85
  "lint:fix": "eslint . --fix"
85
86
  }