@feugene/fint-i18n 0.1.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 ADDED
@@ -0,0 +1,93 @@
1
+ # @feugene/fint-i18n
2
+
3
+ [Русская версия (Russian version)](./README.ru.md)
4
+
5
+ Localization library for Vue 3 with lazy-loading blocks, template compilation, and extensible plugins.
6
+
7
+ ## Features
8
+
9
+ - **Small surface area**: The package is split into `core`, `vue`, and `plugins` entry points.
10
+ - **Performant runtime**: Templates are compiled into functions and cached.
11
+ - **Async blocks**: Support for splitting translations into blocks and lazy-loading them.
12
+ - **Bridge Mode**: Transparent integration with `vue-i18n`.
13
+ - **Plugins**: Hook system for extending functionality (persistence, logging, etc.).
14
+ - **Simple runtime contract**: The only peer dependency is `vue`.
15
+
16
+ ## Documentation
17
+
18
+ You can find detailed information about the library in the relevant sections:
19
+
20
+ - 📦 **[Installation and Getting Started](./docs/en/installation.md)**: How to install the package and configure it in a Vue application.
21
+ - 📂 **[Defining Messages](./docs/en/defining-messages.md)**: JSON formats, loaders, and dynamic merging.
22
+ - 🚀 **[Usage](./docs/en/usage.md)**: How to use `t()`, `$t`, and the `v-t` directive.
23
+ - 📘 **[API Reference](./docs/en/api.md)**: Detailed description of all functions, methods, and composables.
24
+ - 🔌 **[Plugins](./docs/en/plugins.md)**: Extending functionality via the hook system and built-in plugins.
25
+ - 🧱 **[Translation Blocks](./docs/en/blocks.md)**: Deep dive into the concept of blocks and memory management.
26
+ - ⚡ **[Benchmarks and Bundle Analysis](./docs/en/bundle-analysis.md)**: How to measure the hot path and analyze the `dist` composition.
27
+
28
+ ---
29
+
30
+ ## Quick Start
31
+
32
+ ### 1. Initialization
33
+
34
+ ```typescript
35
+ import { createApp } from 'vue'
36
+ import { createFintI18n } from '@feugene/fint-i18n/core'
37
+ import { installI18n } from '@feugene/fint-i18n/vue'
38
+ import { appLocaleLoaders } from './i18n/messages'
39
+ import { fintDsLocaleLoaders } from '@feugene/fint-ds/i18n'
40
+
41
+ const i18n = createFintI18n({
42
+ locale: 'en',
43
+ fallbackLocale: 'en',
44
+ loaders: [appLocaleLoaders, fintDsLocaleLoaders],
45
+ })
46
+
47
+ const app = createApp(App)
48
+ installI18n(app, i18n)
49
+ app.mount('#app')
50
+ ```
51
+
52
+ `loaders` accepts:
53
+
54
+ - `LocaleLoaderCollection` — a single locale/block collection;
55
+ - `LocaleLoaderCollection[]` — an array of collections from multiple packages;
56
+ - for each `block`, you can pass a single loader or an array of loaders.
57
+
58
+ Rules:
59
+
60
+ - collections in `loaders: [...]` are merged **from left to right**;
61
+ - if the same `block` is found in multiple collections, loaders are combined into an array;
62
+ - loaders for a single block are executed **sequentially**;
63
+ - in case of key conflicts in messages, the **last** loader wins;
64
+ - when `loadBlock('pages.articles')` is called, it first looks for an exact block match, then the nearest parent block (`pages`).
65
+
66
+ ### 2. Usage in components
67
+
68
+ ```vue
69
+ <script setup>
70
+ import { useFintI18n, useI18nScope } from '@feugene/fint-i18n/vue'
71
+
72
+ // Connect necessary blocks (they will load automatically)
73
+ await useI18nScope(['common', 'auth'])
74
+
75
+ const { t, locale, setLocale } = useFintI18n()
76
+
77
+ const changeLanguage = async () => {
78
+ await setLocale(locale.value === 'en' ? 'ru' : 'en')
79
+ }
80
+ </script>
81
+
82
+ <template>
83
+ <div>
84
+ <p>{{ t('common.welcome', { name: 'User' }) }}</p>
85
+ <button @click="changeLanguage">
86
+ Change Language
87
+ </button>
88
+
89
+ <!-- Directive usage -->
90
+ <span v-t="'auth.login'" />
91
+ </div>
92
+ </template>
93
+ ```
package/README.ru.md ADDED
@@ -0,0 +1,92 @@
1
+ # @feugene/fint-i18n
2
+
3
+ Библиотека локализации для Vue 3 с ленивой загрузкой блоков, компиляцией шаблонов и расширяемыми плагинами.
4
+
5
+ ## Особенности
6
+
7
+ - **Небольшой surface area**: пакет разбит на `core`, `vue` и `plugins` энтрипойнты.
8
+ - **Производительный runtime**: шаблоны компилируются в функции и кэшируются.
9
+ - **Асинхронные блоки**: Поддержка разделения переводов на блоки и их ленивая загрузка.
10
+ - **Bridge Mode**: Прозрачная интеграция с `vue-i18n`.
11
+ - **Плагины**: Система хуков для расширения функционала (персистентность, логирование и т.д.).
12
+ - **Простой runtime-контракт**: единственная peer dependency — `vue`.
13
+
14
+ ## Документация
15
+
16
+ Подробную информацию о работе с библиотекой вы найдете в соответствующих разделах:
17
+
18
+ - 📦 **[Установка и начало работы](./docs/ru/installation.md)**: Как установить пакет и настроить его в приложении Vue.
19
+ - 📂 **[Определение сообщений](./docs/ru/defining-messages.md)**: Форматы JSON, лоадеры и динамический мердж.
20
+ - 🚀 **[Использование](./docs/ru/usage.md)**: Как использовать `t()`, `$t` и директиву `v-t`.
21
+ - 📘 **[Справочник API](./docs/ru/api.md)**: Подробное описание всех функций, методов и композаблов.
22
+ - 🔌 **[Плагины](./docs/ru/plugins.md)**: Расширение функционала через систему хуков и встроенные плагины.
23
+ - 🧱 **[Блоки перевода](./docs/ru/blocks.md)**: Глубокое погружение в концепцию блоков и управление памятью.
24
+ - ⚡ **[Бенчмарки и анализ бандла](./docs/ru/bundle-analysis.md)**: Как мерить hot path и смотреть состав собранного `dist`.
25
+
26
+ ---
27
+
28
+ ## Быстрый старт
29
+
30
+ ### 1. Инициализация
31
+
32
+ ```typescript
33
+ import { createApp } from 'vue'
34
+ import { createFintI18n } from '@feugene/fint-i18n/core'
35
+ import { installI18n } from '@feugene/fint-i18n/vue'
36
+ import { appLocaleLoaders } from './i18n/messages'
37
+ import { fintDsLocaleLoaders } from '@feugene/fint-ds/i18n'
38
+
39
+ const i18n = createFintI18n({
40
+ locale: 'en',
41
+ fallbackLocale: 'en',
42
+ loaders: [appLocaleLoaders, fintDsLocaleLoaders],
43
+ })
44
+
45
+ const app = createApp(App)
46
+ installI18n(app, i18n)
47
+ app.mount('#app')
48
+ ```
49
+
50
+ `loaders` принимает:
51
+
52
+ - `LocaleLoaderCollection` — одна locale/block-коллекция;
53
+ - `LocaleLoaderCollection[]` — массив коллекций из нескольких пакетов;
54
+ - для каждого `block` можно передать один loader или массив loaders.
55
+
56
+ Правила работы:
57
+
58
+ - коллекции в `loaders: [...]` мерджатся **слева направо**;
59
+ - если один и тот же `block` встречается в нескольких коллекциях, loaders объединяются в массив;
60
+ - loaders одного блока выполняются **последовательно**;
61
+ - при конфликте ключей в сообщениях побеждает **последний** loader;
62
+ - при `loadBlock('pages.articles')` сначала ищется точный block, затем ближайший parent block (`pages`).
63
+
64
+ ### 2. Использование в компонентах
65
+
66
+ ```vue
67
+ <script setup>
68
+ import { useFintI18n, useI18nScope } from '@feugene/fint-i18n/vue'
69
+
70
+ // Подключаем необходимые блоки (автоматически загрузятся)
71
+ await useI18nScope(['common', 'auth'])
72
+
73
+ const { t, locale, setLocale } = useFintI18n()
74
+
75
+ const changeLanguage = async () => {
76
+ await setLocale(locale.value === 'en' ? 'ru' : 'en')
77
+ }
78
+ </script>
79
+
80
+ <template>
81
+ <div>
82
+ <p>{{ t('common.welcome', { name: 'User' }) }}</p>
83
+ <button @click="changeLanguage">
84
+ Change Language
85
+ </button>
86
+
87
+ <!-- Использование директивы -->
88
+ <span v-t="'auth.login'" />
89
+ </div>
90
+ </template>
91
+ ```
92
+
@@ -0,0 +1,323 @@
1
+ import { isRef as e, reactive as t, ref as n, watch as r } from "vue";
2
+ //#region src/core/compiler.ts
3
+ function i(e) {
4
+ if (!e.includes("{")) return () => e;
5
+ let t = [], n = 0, r = /\{(\w+)\}/g, i = r.exec(e);
6
+ for (; i;) i.index > n && t.push(e.slice(n, i.index)), t.push({
7
+ key: i[1],
8
+ fallback: i[0]
9
+ }), n = i.index + i[0].length, i = r.exec(e);
10
+ return n < e.length && t.push(e.slice(n)), (n) => {
11
+ if (!n) return e;
12
+ let r = "";
13
+ for (let e of t) if (typeof e == "string") r += e;
14
+ else {
15
+ let t = n[e.key];
16
+ r += t === void 0 ? e.fallback : String(t);
17
+ }
18
+ return r;
19
+ };
20
+ }
21
+ //#endregion
22
+ //#region src/core/hooks.ts
23
+ var a = class {
24
+ hooks = /* @__PURE__ */ new Map();
25
+ on(e, t) {
26
+ let n = this.hooks.get(e);
27
+ return n ? n.push(t) : this.hooks.set(e, [t]), () => this.off(e, t);
28
+ }
29
+ off(e, t) {
30
+ let n = this.hooks.get(e);
31
+ if (n) {
32
+ let r = n.indexOf(t);
33
+ r !== -1 && (n.splice(r, 1), n.length === 0 && this.hooks.delete(e));
34
+ }
35
+ }
36
+ emit(e, t) {
37
+ let n = this.hooks.get(e);
38
+ if (!n || n.length === 0) return Promise.resolve(t);
39
+ let r = t, i = n.slice();
40
+ return (async () => {
41
+ for (let e of i) {
42
+ let t = await e(r);
43
+ t !== void 0 && (r = t);
44
+ }
45
+ return r;
46
+ })();
47
+ }
48
+ emitSync(e, t) {
49
+ let n = this.hooks.get(e);
50
+ if (!n || n.length === 0) return t;
51
+ let r = t, i = n.slice();
52
+ for (let t of i) {
53
+ let n = t(r);
54
+ if (n instanceof Promise) {
55
+ console.warn(`[fint-i18n] Sync hook "${e}" received a promise from handler. This will be ignored in sync execution.`);
56
+ continue;
57
+ }
58
+ n !== void 0 && (r = n);
59
+ }
60
+ return r;
61
+ }
62
+ }, o = class {
63
+ loaders;
64
+ constructor(e) {
65
+ this.loaders = this.normalize(e);
66
+ }
67
+ resolve(e, t) {
68
+ let n = this.loaders[e];
69
+ if (!n) return null;
70
+ if (n[t]) return {
71
+ resolvedBlockName: t,
72
+ loaders: n[t]
73
+ };
74
+ if (!t.includes(".")) return null;
75
+ let r = t.split(".");
76
+ for (let e = r.length - 1; e >= 1; e--) {
77
+ let t = r.slice(0, e).join("."), i = n[t];
78
+ if (i) return {
79
+ resolvedBlockName: t,
80
+ loaders: i
81
+ };
82
+ }
83
+ return null;
84
+ }
85
+ normalize(e) {
86
+ let t = Object.create(null);
87
+ if (!e) return t;
88
+ let n = Array.isArray(e) ? e : [e];
89
+ for (let e of n) this.mergeCollection(t, e);
90
+ return t;
91
+ }
92
+ mergeCollection(e, t) {
93
+ for (let n in t) {
94
+ e[n] || (e[n] = Object.create(null));
95
+ let r = t[n];
96
+ for (let t in r) {
97
+ let i = this.normalizeEntry(r[t]);
98
+ if (!e[n][t]) {
99
+ e[n][t] = i;
100
+ continue;
101
+ }
102
+ e[n][t] = [...e[n][t], ...i];
103
+ }
104
+ }
105
+ }
106
+ normalizeEntry(e) {
107
+ return Array.isArray(e) ? e : [e];
108
+ }
109
+ };
110
+ //#endregion
111
+ //#region src/core/message-utils.ts
112
+ function s(e) {
113
+ return !!e && typeof e == "object" && !Array.isArray(e);
114
+ }
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];
117
+ }
118
+ function l(e, n) {
119
+ if (s(e) && s(n)) {
120
+ let r = t({});
121
+ return c(r, e), c(r, n), r;
122
+ }
123
+ return n;
124
+ }
125
+ function u(e, t) {
126
+ let n = e, r = 0;
127
+ for (; n && typeof n == "object";) {
128
+ let e = t.indexOf(".", r), i = e === -1 ? t.slice(r) : t.slice(r, e);
129
+ if (n = n[i], n === void 0 || e === -1) return n;
130
+ r = e + 1;
131
+ }
132
+ }
133
+ //#endregion
134
+ //#region src/core/translate-params.ts
135
+ function d(t) {
136
+ if (!t) return;
137
+ let n;
138
+ for (let r in t) {
139
+ let i = t[r];
140
+ e(i) && (n ||= { ...t }, n[r] = i.value);
141
+ }
142
+ return n || t;
143
+ }
144
+ //#endregion
145
+ //#region src/core/instance.ts
146
+ var f = class {
147
+ locale;
148
+ fallbackLocale;
149
+ globalInstall;
150
+ messages = t({});
151
+ compiledMessages = Object.create(null);
152
+ loaderRegistry;
153
+ loadingBlocks = /* @__PURE__ */ new Map();
154
+ loadedBlocks = /* @__PURE__ */ new Map();
155
+ blockUsageCounters = /* @__PURE__ */ new Map();
156
+ pendingUsedBlockLoads = /* @__PURE__ */ new Map();
157
+ skipNextUsedBlockLoadLocale = null;
158
+ hooks = new a();
159
+ 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) => {
161
+ if (e !== t) {
162
+ if (this.skipNextUsedBlockLoadLocale === e) {
163
+ this.skipNextUsedBlockLoadLocale = null;
164
+ return;
165
+ }
166
+ this.loadUsedBlocks(e);
167
+ }
168
+ }), this.hooks.emitSync("afterInit", void 0);
169
+ }
170
+ t = (e, t, n) => {
171
+ let r = this.locale.value, i = d(t), a = this.hooks.emitSync("onTranslate", {
172
+ key: e,
173
+ params: i,
174
+ result: this.resolve(r, e, i) || e
175
+ }).result;
176
+ if (a === e) {
177
+ let t = n?.fallbackLocale || this.fallbackLocale;
178
+ if (t && t !== r) {
179
+ let n = this.resolve(t, e, i);
180
+ if (n !== void 0) return n;
181
+ }
182
+ this.hooks.emit("onMissingKey", {
183
+ key: e,
184
+ locale: r
185
+ }).catch((e) => {
186
+ console.error("[fint-i18n] Error in onMissingKey hook:", e);
187
+ });
188
+ }
189
+ return a;
190
+ };
191
+ resolve = (e, t, n) => {
192
+ let r = this.compiledMessages[e]?.[t];
193
+ if (r) return r(n);
194
+ let a = this.messages[e];
195
+ if (!a) return;
196
+ let o = u(a, t);
197
+ if (typeof o == "string") {
198
+ let r = i(o);
199
+ return this.setCompiled(e, t, r), r(n);
200
+ }
201
+ if (typeof o == "function") {
202
+ let r = o;
203
+ return this.setCompiled(e, t, r), r(n);
204
+ }
205
+ if (o != null && typeof o != "object") return String(o);
206
+ };
207
+ setCompiled = (e, t, n) => {
208
+ this.compiledMessages[e] || (this.compiledMessages[e] = Object.create(null)), this.compiledMessages[e][t] = n;
209
+ };
210
+ loadBlock = async (e, t) => {
211
+ let n = t || this.locale.value, r = `${n}:${e}`;
212
+ if (this.isBlockLoaded(e, n)) return;
213
+ if (this.loadingBlocks.has(r)) return this.loadingBlocks.get(r);
214
+ await this.hooks.emit("beforeLoadBlock", e);
215
+ let i = (async () => {
216
+ try {
217
+ let t = this.loaderRegistry.resolve(n, e);
218
+ if (!t) {
219
+ console.warn(`[fint-i18n] No loader for block "${e}" in locale "${n}"`);
220
+ return;
221
+ }
222
+ let r;
223
+ for (let e of t.loaders) {
224
+ let i = await e(), a = i.default || i;
225
+ this.mergeMessages(n, t.resolvedBlockName, a), r = r === void 0 ? a : l(r, a);
226
+ }
227
+ this.markBlockLoaded(t.resolvedBlockName, n), await this.hooks.emit("afterLoadBlock", {
228
+ block: t.resolvedBlockName,
229
+ locale: n,
230
+ messages: r
231
+ });
232
+ } finally {
233
+ this.loadingBlocks.delete(r);
234
+ }
235
+ })();
236
+ return this.loadingBlocks.set(r, i), i;
237
+ };
238
+ mergeMessages = (e, n, r) => {
239
+ this.messages[e] || (this.messages[e] = t({}));
240
+ 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;
242
+ else {
243
+ (!this.messages[e][a] || typeof this.messages[e][a] != "object") && (this.messages[e][a] = t({}));
244
+ let n = this.messages[e][a];
245
+ for (let e = 1; e < i.length; e++) {
246
+ let r = i[e];
247
+ (!n[r] || typeof n[r] != "object") && (n[r] = t({})), n = n[r];
248
+ }
249
+ c(n, r);
250
+ }
251
+ this.precompileBlock(e, n, r);
252
+ };
253
+ precompileBlock = (e, t, n) => {
254
+ if (typeof n == "string") {
255
+ let r = i(n);
256
+ this.setCompiled(e, t, r);
257
+ return;
258
+ }
259
+ if (typeof n == "function") {
260
+ this.setCompiled(e, t, n);
261
+ return;
262
+ }
263
+ if (s(n)) for (let r in n) {
264
+ let a = n[r], o = `${t}.${r}`;
265
+ if (typeof a == "string") {
266
+ let t = i(a);
267
+ this.setCompiled(e, o, t);
268
+ } else typeof a == "function" ? this.setCompiled(e, o, a) : s(a) && this.precompileBlock(e, o, a);
269
+ }
270
+ };
271
+ isBlockLoaded = (e, t) => {
272
+ let n = t || this.locale.value, r = this.loadedBlocks.get(n);
273
+ if (!r) return !1;
274
+ if (r.has(e)) return !0;
275
+ let i = e.split("."), a = "";
276
+ for (let e = 0; e < i.length - 1; e++) if (a = a ? `${a}.${i[e]}` : i[e], r.has(a)) return !0;
277
+ return !1;
278
+ };
279
+ markBlockLoaded = (e, t) => {
280
+ this.loadedBlocks.has(t) || this.loadedBlocks.set(t, /* @__PURE__ */ new Set()), this.loadedBlocks.get(t).add(e);
281
+ };
282
+ loadUsedBlocks = async (e) => {
283
+ let t = this.pendingUsedBlockLoads.get(e);
284
+ if (t) {
285
+ await t;
286
+ return;
287
+ }
288
+ let n = (async () => {
289
+ let t = [];
290
+ for (let [n, r] of this.blockUsageCounters.entries()) r > 0 && t.push(this.loadBlock(n, e));
291
+ await Promise.all(t);
292
+ })();
293
+ this.pendingUsedBlockLoads.set(e, n);
294
+ try {
295
+ await n;
296
+ } finally {
297
+ this.pendingUsedBlockLoads.delete(e);
298
+ }
299
+ };
300
+ setLocale = async (e) => {
301
+ let t = this.locale.value;
302
+ t !== e && (await this.loadUsedBlocks(e), this.skipNextUsedBlockLoadLocale = e, this.locale.value = e, this.hooks.emitSync("onLocaleChange", {
303
+ locale: e,
304
+ previous: t
305
+ }));
306
+ };
307
+ registerUsage = (e) => {
308
+ let t = this.blockUsageCounters.get(e) || 0;
309
+ this.blockUsageCounters.set(e, t + 1);
310
+ };
311
+ registerBlocks = (e) => {
312
+ e.forEach((e) => this.registerUsage(e));
313
+ };
314
+ unregisterUsage = (e) => {
315
+ let t = this.blockUsageCounters.get(e) || 0;
316
+ t <= 1 ? this.blockUsageCounters.delete(e) : this.blockUsageCounters.set(e, t - 1);
317
+ };
318
+ };
319
+ function p(e) {
320
+ return new f(e);
321
+ }
322
+ //#endregion
323
+ export { i, p as n, a as r, f as t };
package/dist/core.js ADDED
@@ -0,0 +1,2 @@
1
+ import { i as e, n as t, r as n, t as r } from "./chunks/core-Dbw0feTs.js";
2
+ export { r as FintI18n, n as HookManager, e as compileTemplate, t as createFintI18n };
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import { i as e, n as t, r as n, t as r } from "./chunks/core-Dbw0feTs.js";
2
+ import { FINT_I18N_KEY as i, createVTDirective as a, installI18n as o, useFintI18n as s, useI18nScope as c } from "./vue.js";
3
+ import { BridgePlugin as l, HookLoggerPlugin as u, PersistencePlugin as d } from "./plugins.js";
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 };
@@ -0,0 +1,69 @@
1
+ import { watch as e } from "vue";
2
+ //#region src/plugins/persistence.ts
3
+ var t = class {
4
+ name = "persistence";
5
+ options;
6
+ constructor(e = {}) {
7
+ this.options = {
8
+ key: "fint-i18n-locale",
9
+ syncTabs: !0,
10
+ ...e
11
+ };
12
+ }
13
+ install(e) {
14
+ let t = this.options.key, n = this.options.storage || (typeof window < "u" ? window.localStorage : void 0);
15
+ if (!n) return;
16
+ let r = n.getItem(t);
17
+ r && (e.locale.value = r), e.hooks.on("onLocaleChange", ({ locale: e }) => {
18
+ n.setItem(t, e);
19
+ }), this.options.syncTabs && typeof window < "u" && window.addEventListener("storage", (n) => {
20
+ n.key === t && n.newValue && n.newValue !== e.locale.value && (e.locale.value = n.newValue);
21
+ });
22
+ }
23
+ }, n = class {
24
+ name = "bridge";
25
+ options;
26
+ constructor(e) {
27
+ this.options = e;
28
+ }
29
+ install(t) {
30
+ let n = this.options.i18n;
31
+ e(() => n.locale.value || n.locale, (e) => {
32
+ t.locale.value !== e && (t.locale.value = e);
33
+ }, { immediate: !0 }), t.hooks.on("onLocaleChange", ({ locale: e }) => {
34
+ typeof n.locale == "object" ? n.locale.value = e : n.locale = e;
35
+ }), t.hooks.on("onTranslate", (e) => {
36
+ let t = e.params ? n.t(e.key, e.params) : n.t(e.key);
37
+ return t === e.key ? e : {
38
+ ...e,
39
+ result: t
40
+ };
41
+ });
42
+ }
43
+ }, r = [
44
+ "beforeInit",
45
+ "afterInit",
46
+ "onLocaleChange",
47
+ "beforeLoadBlock",
48
+ "afterLoadBlock",
49
+ "onMissingKey",
50
+ "onTranslate"
51
+ ], i = class {
52
+ name = "hook-logger";
53
+ options;
54
+ constructor(e = {}) {
55
+ this.options = {
56
+ logger: console.log,
57
+ prefix: "[fint-i18n] Hook",
58
+ ...e
59
+ };
60
+ }
61
+ install(e) {
62
+ r.forEach((t) => this.registerHook(e, t));
63
+ }
64
+ registerHook(e, t) {
65
+ e.hooks.on(t, (e) => (this.options.logger(`${this.options.prefix} "${t}" called`, e), e));
66
+ }
67
+ };
68
+ //#endregion
69
+ export { n as BridgePlugin, i as HookLoggerPlugin, t as PersistencePlugin };
@@ -0,0 +1,6 @@
1
+ export type MessageFunction = (params?: Record<string, any>) => string;
2
+ /**
3
+ * JIT-компилятор шаблонов.
4
+ * Преобразует строку "Привет, {name}!" в функцию (p) => "Привет, " + p.name + "!"
5
+ */
6
+ export declare function compileTemplate(template: string): MessageFunction;
@@ -0,0 +1,34 @@
1
+ import type { Locale } from './types';
2
+ export type HookFn<T = any> = (arg: T) => void | T | Promise<void | T>;
3
+ type HookPayload<K extends keyof FintI18nHooks> = Parameters<FintI18nHooks[K]>[0];
4
+ export interface FintI18nHooks {
5
+ 'beforeInit': HookFn<any>;
6
+ 'afterInit': HookFn<void>;
7
+ 'onLocaleChange': HookFn<{
8
+ locale: Locale;
9
+ previous: Locale;
10
+ }>;
11
+ 'beforeLoadBlock': HookFn<string>;
12
+ 'afterLoadBlock': HookFn<{
13
+ block: string;
14
+ locale: Locale;
15
+ messages: any;
16
+ }>;
17
+ 'onMissingKey': HookFn<{
18
+ key: string;
19
+ locale: Locale;
20
+ }>;
21
+ 'onTranslate': HookFn<{
22
+ key: string;
23
+ params?: any;
24
+ result: string;
25
+ }>;
26
+ }
27
+ export declare class HookManager {
28
+ private hooks;
29
+ on<K extends keyof FintI18nHooks>(name: K, fn: FintI18nHooks[K]): () => void;
30
+ off<K extends keyof FintI18nHooks>(name: K, fn: FintI18nHooks[K]): void;
31
+ emit<K extends keyof FintI18nHooks>(name: K, arg: HookPayload<K>): Promise<HookPayload<K>>;
32
+ emitSync<K extends keyof FintI18nHooks>(name: K, arg: HookPayload<K>): HookPayload<K>;
33
+ }
34
+ export {};
@@ -0,0 +1,4 @@
1
+ export * from './types';
2
+ export * from './instance';
3
+ export * from './hooks';
4
+ export * from './compiler';
@@ -0,0 +1,32 @@
1
+ import { type Ref } from 'vue';
2
+ import { HookManager } from './hooks';
3
+ import type { FintI18nOptions, Locale, MessageValue, TranslateOptions } from './types';
4
+ export declare class FintI18n {
5
+ locale: Ref<Locale>;
6
+ fallbackLocale: Locale;
7
+ globalInstall: boolean;
8
+ readonly messages: Record<Locale, any>;
9
+ private compiledMessages;
10
+ private readonly loaderRegistry;
11
+ private loadingBlocks;
12
+ private loadedBlocks;
13
+ private blockUsageCounters;
14
+ private pendingUsedBlockLoads;
15
+ private skipNextUsedBlockLoadLocale;
16
+ hooks: HookManager;
17
+ constructor(options: FintI18nOptions);
18
+ t: (key: string, params?: Record<string, any>, options?: TranslateOptions) => string;
19
+ private resolve;
20
+ private setCompiled;
21
+ loadBlock: (blockName: string, locale?: Locale) => Promise<void>;
22
+ mergeMessages: (locale: Locale, blockName: string, messages: MessageValue) => void;
23
+ private precompileBlock;
24
+ isBlockLoaded: (blockName: string, locale?: Locale) => boolean;
25
+ markBlockLoaded: (blockName: string, locale: Locale) => void;
26
+ loadUsedBlocks: (locale: Locale) => Promise<void>;
27
+ setLocale: (newLocale: Locale) => Promise<void>;
28
+ registerUsage: (blockName: string) => void;
29
+ registerBlocks: (blockNames: string[]) => void;
30
+ unregisterUsage: (blockName: string) => void;
31
+ }
32
+ export declare function createFintI18n(options: FintI18nOptions): FintI18n;
@@ -0,0 +1,13 @@
1
+ import type { Locale, LocaleBlockLoader, LocaleLoaderSource } from './types';
2
+ export interface ResolvedLocaleBlockLoaders {
3
+ resolvedBlockName: string;
4
+ loaders: LocaleBlockLoader[];
5
+ }
6
+ export declare class LocaleLoaderRegistry {
7
+ private readonly loaders;
8
+ constructor(source?: LocaleLoaderSource);
9
+ resolve(locale: Locale, blockName: string): ResolvedLocaleBlockLoaders | null;
10
+ private normalize;
11
+ private mergeCollection;
12
+ private normalizeEntry;
13
+ }
@@ -0,0 +1,5 @@
1
+ import type { MessageSchema, MessageValue } from './types';
2
+ export declare function isMessageObject(value: unknown): value is MessageSchema;
3
+ export declare function deepMerge(target: any, source: any): void;
4
+ export declare function mergeMessageValues(target: MessageValue, source: MessageValue): MessageValue;
5
+ export declare function getMessageValue(messages: Record<string, any>, key: string): unknown;
@@ -0,0 +1 @@
1
+ export declare function normalizeTranslateParams(params?: Record<string, any>): Record<string, any> | undefined;
@@ -0,0 +1,26 @@
1
+ import type { MessageFunction } from './compiler';
2
+ import type { FintI18n } from './instance';
3
+ export type Locale = string;
4
+ export type LocaleBlockLoader = () => Promise<any>;
5
+ export type LocaleBlockLoaders = LocaleBlockLoader | LocaleBlockLoader[];
6
+ export type LocaleLoaderCollection = Record<Locale, Record<string, LocaleBlockLoaders>>;
7
+ export type LocaleLoaderSource = LocaleLoaderCollection | LocaleLoaderCollection[];
8
+ export interface FintI18nPlugin {
9
+ name: string;
10
+ install: (instance: FintI18n) => void;
11
+ }
12
+ export interface FintI18nOptions {
13
+ locale: Locale;
14
+ fallbackLocale?: Locale;
15
+ loaders?: LocaleLoaderSource;
16
+ plugins?: FintI18nPlugin[];
17
+ globalInstall?: boolean;
18
+ }
19
+ export type MessagePrimitive = string | number | boolean;
20
+ export interface MessageSchema {
21
+ [key: string]: MessagePrimitive | MessageFunction | MessageSchema;
22
+ }
23
+ export type MessageValue = MessagePrimitive | MessageFunction | MessageSchema;
24
+ export interface TranslateOptions {
25
+ fallbackLocale?: Locale;
26
+ }
@@ -0,0 +1,3 @@
1
+ export * from './core';
2
+ export * from './vue';
3
+ export * from './plugins';
@@ -0,0 +1,10 @@
1
+ import type { FintI18n, FintI18nPlugin } from '@/core';
2
+ export interface BridgeOptions {
3
+ i18n: any;
4
+ }
5
+ export declare class BridgePlugin implements FintI18nPlugin {
6
+ name: string;
7
+ private options;
8
+ constructor(options: BridgeOptions);
9
+ install(fintI18n: FintI18n): void;
10
+ }
@@ -0,0 +1,14 @@
1
+ import type { FintI18n, FintI18nPlugin } from '@/core';
2
+ type LoggerFn = (message?: any, ...optionalParams: any[]) => void;
3
+ export interface HookLoggerPluginOptions {
4
+ logger?: LoggerFn;
5
+ prefix?: string;
6
+ }
7
+ export declare class HookLoggerPlugin implements FintI18nPlugin {
8
+ name: string;
9
+ private options;
10
+ constructor(options?: HookLoggerPluginOptions);
11
+ install(i18n: FintI18n): void;
12
+ private registerHook;
13
+ }
14
+ export {};
@@ -0,0 +1,3 @@
1
+ export * from './persistence';
2
+ export * from './bridge';
3
+ export * from './hook-logger';
@@ -0,0 +1,12 @@
1
+ import type { FintI18n, FintI18nPlugin } from '@/core';
2
+ export interface PersistenceOptions {
3
+ key?: string;
4
+ storage?: Storage;
5
+ syncTabs?: boolean;
6
+ }
7
+ export declare class PersistencePlugin implements FintI18nPlugin {
8
+ name: string;
9
+ private options;
10
+ constructor(options?: PersistenceOptions);
11
+ install(i18n: FintI18n): void;
12
+ }
@@ -0,0 +1,18 @@
1
+ import type { Directive } from 'vue';
2
+ import type { FintI18n } from '@/core';
3
+ export type VTDirectiveValue = string | {
4
+ path: string;
5
+ params?: Record<string, any>;
6
+ };
7
+ export interface VTDirectiveModifiers {
8
+ once?: boolean;
9
+ preserve?: boolean;
10
+ }
11
+ /**
12
+ * Create v-t directive.
13
+ *
14
+ * Modifiers:
15
+ * - `.once`: render only once
16
+ * - `.preserve`: keep current text if key not found
17
+ */
18
+ export declare function createVTDirective(i18n: FintI18n): Directive<HTMLElement, VTDirectiveValue>;
@@ -0,0 +1,25 @@
1
+ export * from './scope';
2
+ export * from './inject';
3
+ export * from './directive';
4
+ export * from './plugin';
5
+ import type { FintI18n } from '@/core';
6
+ import type { Directive } from 'vue';
7
+ import type { VTDirectiveValue } from './directive';
8
+ declare module '@vue/runtime-core' {
9
+ interface GlobalDirectives {
10
+ vT: Directive<HTMLElement, VTDirectiveValue>;
11
+ }
12
+ interface ComponentCustomProperties {
13
+ $t: FintI18n['t'];
14
+ $i18n: FintI18n;
15
+ }
16
+ }
17
+ declare module 'vue' {
18
+ interface GlobalDirectives {
19
+ vT: Directive<HTMLElement, VTDirectiveValue>;
20
+ }
21
+ interface ComponentCustomProperties {
22
+ $t: FintI18n['t'];
23
+ $i18n: FintI18n;
24
+ }
25
+ }
@@ -0,0 +1,4 @@
1
+ import { type InjectionKey } from 'vue';
2
+ import type { FintI18n } from '@/core';
3
+ export declare const FINT_I18N_KEY: InjectionKey<FintI18n>;
4
+ export declare function useFintI18n(): FintI18n;
@@ -0,0 +1,5 @@
1
+ import type { App } from 'vue';
2
+ import type { FintI18n } from '@/core';
3
+ export declare function installI18n(app: App, i18n: FintI18n, options?: {
4
+ directive?: string | boolean;
5
+ }): void;
@@ -0,0 +1,5 @@
1
+ export declare function useI18nScope(blocks: string | string[]): Promise<{
2
+ t: (key: string, params?: Record<string, any>) => string;
3
+ locale: import("vue").Ref<string, string>;
4
+ setLocale: (l: string) => Promise<void>;
5
+ }>;
package/dist/vue.js ADDED
@@ -0,0 +1,51 @@
1
+ import { inject as e, onUnmounted as t } from "vue";
2
+ //#region src/vue/inject.ts
3
+ var n = Symbol("FintI18n");
4
+ function r() {
5
+ let t = e(n);
6
+ if (!t) throw Error("[fint-i18n] Instance not found. Did you call installI18n(app, i18n)?");
7
+ return t;
8
+ }
9
+ //#endregion
10
+ //#region src/vue/scope.ts
11
+ async function i(e) {
12
+ let n = r(), i = Array.isArray(e) ? e : [e];
13
+ t(() => {
14
+ i.forEach((e) => n.unregisterUsage(e));
15
+ });
16
+ let a = i.map((e) => (n.registerUsage(e), n.loadBlock(e)));
17
+ return await Promise.all(a), {
18
+ t: (e, t) => n.t(e, t),
19
+ locale: n.locale,
20
+ setLocale: (e) => n.setLocale(e)
21
+ };
22
+ }
23
+ //#endregion
24
+ //#region src/vue/directive.ts
25
+ function a(e) {
26
+ return {
27
+ mounted(t, n) {
28
+ o(t, n, e);
29
+ },
30
+ updated(t, n) {
31
+ n.modifiers.once || o(t, n, e);
32
+ }
33
+ };
34
+ }
35
+ function o(e, t, n) {
36
+ let { value: r, modifiers: i } = t, a, o;
37
+ if (typeof r == "string") a = r;
38
+ else if (r && typeof r == "object") a = r.path, o = r.params;
39
+ else return;
40
+ let s = n.t(a, o);
41
+ s === a && i.preserve || (e.textContent = s);
42
+ }
43
+ //#endregion
44
+ //#region src/vue/plugin.ts
45
+ function s(e, t, r = {}) {
46
+ e.provide(n, t), t.globalInstall && (e.config.globalProperties.$t = t.t, e.config.globalProperties.$i18n = t);
47
+ let i = r.directive === !1 ? null : typeof r.directive == "string" ? r.directive : "t";
48
+ i && e.directive(i, a(t));
49
+ }
50
+ //#endregion
51
+ export { n as FINT_I18N_KEY, a as createVTDirective, s as installI18n, r as useFintI18n, i as useI18nScope };
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@feugene/fint-i18n",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight Vue 3 i18n library with lazy-loading blocks and template caching",
5
+ "author": "feugene <feugene@example.com>",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/efureev/fint-i18n#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/efureev/fint-i18n.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/efureev/fint-i18n/issues"
14
+ },
15
+ "keywords": [
16
+ "vue",
17
+ "i18n",
18
+ "localization",
19
+ "lazy-loading",
20
+ "vue3",
21
+ "translation"
22
+ ],
23
+ "type": "module",
24
+ "publishConfig": {
25
+ "access": "public",
26
+ "registry": "https://registry.npmjs.org/"
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/types/index.d.ts",
34
+ "import": "./dist/index.js"
35
+ },
36
+ "./core": {
37
+ "types": "./dist/types/core/index.d.ts",
38
+ "import": "./dist/core.js"
39
+ },
40
+ "./vue": {
41
+ "types": "./dist/types/vue/index.d.ts",
42
+ "import": "./dist/vue.js"
43
+ },
44
+ "./plugins": {
45
+ "types": "./dist/types/plugins/index.d.ts",
46
+ "import": "./dist/plugins.js"
47
+ }
48
+ },
49
+ "sideEffects": false,
50
+ "peerDependencies": {
51
+ "vue": "^3.5.32"
52
+ },
53
+ "devDependencies": {
54
+ "@antfu/eslint-config": "^7.7.3",
55
+ "@iconify-json/lucide": "^1.2.101",
56
+ "@types/node": "^25.5.2",
57
+ "@unocss/eslint-plugin": "^66.6.7",
58
+ "@unocss/reset": "^66.6.7",
59
+ "@vitejs/plugin-vue": "^6.0.5",
60
+ "@vue/test-utils": "^2.4.6",
61
+ "eslint": "^10.2.0",
62
+ "globals": "^17.4.0",
63
+ "jsdom": "^29.0.1",
64
+ "rollup-plugin-visualizer": "^7.0.1",
65
+ "typescript": "^5.9.3",
66
+ "unocss": "^66.6.7",
67
+ "vite": "^8.0.5",
68
+ "vitest": "^4.1.2",
69
+ "vue": "^3.5.32",
70
+ "vue-tsc": "^3.2.6"
71
+ },
72
+ "scripts": {
73
+ "dev": "vite --config playground/vite.config.ts",
74
+ "playground": "vite --config playground/vite.config.ts",
75
+ "playground:build": "vite build --config playground/vite.config.ts",
76
+ "build:analyze": "ANALYZE=true vite build && vue-tsc -p tsconfig.build.json",
77
+ "build": "vite build && vue-tsc -p tsconfig.build.json",
78
+ "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",
80
+ "type-check": "vue-tsc --noEmit",
81
+ "test": "vitest",
82
+ "test:run": "vitest run",
83
+ "lint": "eslint .",
84
+ "lint:fix": "eslint . --fix"
85
+ }
86
+ }