@i18n-micro/core 1.0.28 → 1.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.
@@ -5,16 +5,18 @@ interface NavigateToInterface {
5
5
  redirectCode?: number;
6
6
  external?: boolean;
7
7
  }
8
+ export type GetDefaultLocale = () => string | null | undefined;
8
9
  export declare class RouteService {
9
10
  private i18nConfig;
10
11
  private router;
11
12
  private hashLocaleDefault;
12
13
  private noPrefixDefault;
13
14
  private navigateTo;
14
- private setCookie;
15
- private cookieLocaleDefault;
16
- private cookieLocaleName;
17
- constructor(i18nConfig: ModuleOptionsExtend, router: Router, hashLocaleDefault: string | null | undefined, noPrefixDefault: string | null | undefined, navigateTo: (to: RouteLocationRaw | undefined | null, options?: NavigateToInterface) => Promise<void | NavigationFailure | false> | false | void | RouteLocationRaw, setCookie: (name: string, value: string) => void, cookieLocaleDefault?: string | null | undefined, cookieLocaleName?: string | null | undefined);
15
+ /** Optional getter for current locale (used by hash/noPrefix when state changes). Plugin provides () => localeState.value */
16
+ private getDefaultLocale?;
17
+ constructor(i18nConfig: ModuleOptionsExtend, router: Router, hashLocaleDefault: string | null | undefined, noPrefixDefault: string | null | undefined, navigateTo: (to: RouteLocationRaw | undefined | null, options?: NavigateToInterface) => Promise<void | NavigationFailure | false> | false | void | RouteLocationRaw,
18
+ /** Optional getter for current locale (used by hash/noPrefix when state changes). Plugin provides () => localeState.value */
19
+ getDefaultLocale?: GetDefaultLocale | undefined);
18
20
  /**
19
21
  * Extracts locale from URL path by checking the first path segment
20
22
  * @param path - URL path (e.g., '/ru/sdfsdf' or '/en/about')
@@ -31,7 +33,6 @@ export declare class RouteService {
31
33
  private handlePrefixStrategy;
32
34
  private createLocalizedRoute;
33
35
  getLocalizedRoute(to: RouteLocationResolvedGeneric | RouteLocationAsPathGeneric | RouteLocationNamedRaw | string, route: RouteLocationNormalizedLoaded, locale?: string): RouteLocationResolved;
34
- updateCookies(toLocale: string): void;
35
36
  getCurrentRoute(): RouteLocationNormalizedLoaded;
36
37
  private resolveRouteWithStrategy;
37
38
  switchLocaleLogic(toLocale: string, i18nRouteParams: I18nRouteParams, to?: RouteLocationNamedRaw | RouteLocationResolvedGeneric | string): string | false | void | import('vue-router').RouteLocationAsRelativeGeneric | RouteLocationAsPathGeneric | Promise<false | void | NavigationFailure>;
@@ -1,24 +1,23 @@
1
1
  import { Translations } from '@i18n-micro/types';
2
- export interface RefLike<T> {
3
- value: T;
2
+ /**
3
+ * Bare Metal: Простое хранилище переводов без Ref, useState, devalue.
4
+ * Ключ Map: locale (general) или locale:routeName (page-specific).
5
+ */
6
+ export interface TranslationStorage {
7
+ translations: Map<string, Translations>;
4
8
  }
5
- export interface TranslationCache {
6
- generalLocaleCache: RefLike<Record<string, Translations>> | Record<string, Translations>;
7
- routeLocaleCache: RefLike<Record<string, Translations>> | Record<string, Translations>;
8
- dynamicTranslationsCaches: RefLike<Record<string, Translations>[]> | Record<string, Translations>[];
9
- serverTranslationCache: RefLike<Record<string, Map<string, Translations | unknown>>> | Record<string, Map<string, Translations | unknown>>;
10
- }
11
- export declare function useTranslationHelper(caches?: TranslationCache): {
9
+ export declare function useTranslationHelper(storage?: TranslationStorage): {
12
10
  hasCache(locale: string, page: string): boolean;
13
- getCache(locale: string, routeName: string): Map<string, unknown> | undefined;
14
- setCache(locale: string, routeName: string, cache: Map<string, Translations | unknown>): void;
15
- mergeTranslation(locale: string, routeName: string, newTranslations: Translations, force?: boolean): void;
16
- mergeGlobalTranslation(locale: string, newTranslations: Translations, force?: boolean): void;
11
+ getCache(locale: string, routeName: string): Translations | undefined;
12
+ setCache(_locale: string, _routeName: string, _cache: Map<string, unknown>): void;
13
+ hasTranslation(locale: string, key: string): boolean;
17
14
  hasGeneralTranslation(locale: string): boolean;
18
15
  hasPageTranslation(locale: string, routeName: string): boolean;
19
- hasTranslation: (locale: string, key: import('@i18n-micro/types').TranslationKey) => boolean;
20
- getTranslation: <T = unknown>(locale: string, routeName: string, key: import('@i18n-micro/types').TranslationKey) => T | null;
21
- loadPageTranslations(locale: string, routeName: string, translations: Translations): Promise<void>;
22
- loadTranslations(locale: string, translations: Translations): Promise<void>;
16
+ getTranslation<T = unknown>(locale: string, routeName: string, key: string): T | null;
17
+ loadTranslations(locale: string, data: Translations): void;
18
+ setTranslations(locale: string, data: Translations): void;
19
+ loadPageTranslations(locale: string, routeName: string, data: Translations): void;
20
+ mergeTranslation(locale: string, routeName: string, newTranslations: Translations, _force?: boolean): void;
21
+ mergeGlobalTranslation(locale: string, newTranslations: Translations, _force?: boolean): void;
23
22
  clearCache(): void;
24
23
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@i18n-micro/core",
3
- "version": "1.0.28",
3
+ "version": "1.1.0",
4
4
  "description": "",
5
5
  "repository": "s00d/nuxt-i18n-micro",
6
6
  "license": "MIT",
@@ -22,10 +22,10 @@
22
22
  },
23
23
  "keywords": [],
24
24
  "dependencies": {
25
- "@i18n-micro/types": "1.0.16"
25
+ "@i18n-micro/types": "1.1.0"
26
26
  },
27
27
  "devDependencies": {
28
- "vite": "^7.2.7",
28
+ "vite": "^7.3.1",
29
29
  "vite-plugin-dts": "^4.5.4"
30
30
  },
31
31
  "scripts": {
package/src/base.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { useTranslationHelper, type TranslationCache } from './translation'
1
+ import { useTranslationHelper, type TranslationStorage } from './translation'
2
2
  import { FormatService } from './format-service'
3
3
  import { interpolate, defaultPlural } from './helpers'
4
4
  import type {
@@ -12,7 +12,7 @@ import type {
12
12
  } from '@i18n-micro/types'
13
13
 
14
14
  export interface BaseI18nOptions {
15
- cache?: TranslationCache
15
+ storage?: TranslationStorage
16
16
  plural?: PluralFunc
17
17
  missingWarn?: boolean
18
18
  missingHandler?: (locale: string, key: string, routeName: string) => void
@@ -41,7 +41,7 @@ export abstract class BaseI18n {
41
41
  public enablePreviousPageFallback: boolean
42
42
 
43
43
  constructor(options: BaseI18nOptions = {}) {
44
- this.helper = useTranslationHelper(options.cache)
44
+ this.helper = useTranslationHelper(options.storage)
45
45
  this.formatter = new FormatService()
46
46
  this.pluralFunc = options.plural || defaultPlural
47
47
  this.missingWarn = options.missingWarn ?? true
@@ -226,7 +226,7 @@ export abstract class BaseI18n {
226
226
  this.helper.mergeGlobalTranslation(locale, translations, true)
227
227
  }
228
228
  else {
229
- this.helper.loadTranslations(locale, translations)
229
+ this.helper.setTranslations(locale, translations)
230
230
  }
231
231
  }
232
232
 
package/src/helpers.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import type { Params, Strategies, PluralFunc, Getter, TranslationKey } from '@i18n-micro/types'
2
2
 
3
- export function interpolate(template: string, params: Params): string {
4
- let result = template
3
+ const RE_TOKEN = /\{(\w+)\}/g
5
4
 
6
- for (const key in params) {
7
- result = result.split(`{${key}}`).join(String(params[key]))
8
- }
5
+ export function interpolate(template: string, params: Params): string {
6
+ if (!params) return template
9
7
 
10
- return result
8
+ return template.replace(RE_TOKEN, (_, key) => {
9
+ const value = params[key]
10
+ return value !== undefined ? String(value) : `{${key}}`
11
+ })
11
12
  }
12
13
 
13
14
  export function withPrefixStrategy(strategy: Strategies) {
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { useTranslationHelper, type TranslationCache } from './translation'
1
+ import { useTranslationHelper, type TranslationStorage } from './translation'
2
2
  import { RouteService } from './route-service'
3
3
  import { FormatService } from './format-service'
4
4
  import { interpolate, withPrefixStrategy, isNoPrefixStrategy, isPrefixStrategy, isPrefixExceptDefaultStrategy, isPrefixAndDefaultStrategy, defaultPlural } from './helpers'
@@ -16,6 +16,6 @@ export {
16
16
  RouteService,
17
17
  FormatService,
18
18
  BaseI18n,
19
- type TranslationCache,
19
+ type TranslationStorage,
20
20
  type BaseI18nOptions,
21
21
  }
@@ -13,7 +13,7 @@ import type {
13
13
  Router,
14
14
  } from 'vue-router'
15
15
  import type { I18nRouteParams, Locale, ModuleOptionsExtend } from '@i18n-micro/types'
16
- import { isNoPrefixStrategy, withPrefixStrategy } from './helpers'
16
+ import { isNoPrefixStrategy, withPrefixStrategy, isPrefixAndDefaultStrategy, isPrefixExceptDefaultStrategy } from './helpers'
17
17
 
18
18
  interface NavigateToInterface {
19
19
  replace?: boolean
@@ -21,6 +21,8 @@ interface NavigateToInterface {
21
21
  external?: boolean
22
22
  }
23
23
 
24
+ export type GetDefaultLocale = () => string | null | undefined
25
+
24
26
  export class RouteService {
25
27
  constructor(
26
28
  private i18nConfig: ModuleOptionsExtend,
@@ -28,9 +30,8 @@ export class RouteService {
28
30
  private hashLocaleDefault: string | null | undefined,
29
31
  private noPrefixDefault: string | null | undefined,
30
32
  private navigateTo: (to: RouteLocationRaw | undefined | null, options?: NavigateToInterface) => Promise<void | NavigationFailure | false> | false | void | RouteLocationRaw,
31
- private setCookie: (name: string, value: string) => void,
32
- private cookieLocaleDefault: string | null | undefined = null,
33
- private cookieLocaleName: string | null | undefined = null,
33
+ /** Optional getter for current locale (used by hash/noPrefix when state changes). Plugin provides () => localeState.value */
34
+ private getDefaultLocale?: GetDefaultLocale,
34
35
  ) {}
35
36
 
36
37
  /**
@@ -75,14 +76,25 @@ export class RouteService {
75
76
  getCurrentLocale(route?: RouteLocationNormalizedLoaded | RouteLocationResolvedGeneric): string {
76
77
  route = route ?? this.router.currentRoute.value
77
78
 
78
- // 1. Check hashMode
79
- if (this.i18nConfig.hashMode && this.hashLocaleDefault) {
80
- return this.hashLocaleDefault
79
+ // 1. Check hashMode (getter overrides initial value when state changes)
80
+ if (this.i18nConfig.hashMode) {
81
+ const fromGetter = this.getDefaultLocale?.()
82
+ if (fromGetter) return fromGetter
83
+ if (this.hashLocaleDefault) return this.hashLocaleDefault
81
84
  }
82
85
 
83
86
  // 2. Check noPrefix strategy
84
- if (isNoPrefixStrategy(this.i18nConfig.strategy!) && this.noPrefixDefault) {
85
- return this.noPrefixDefault
87
+ if (isNoPrefixStrategy(this.i18nConfig.strategy!)) {
88
+ const fromGetter = this.getDefaultLocale?.()
89
+ if (fromGetter) return fromGetter
90
+ if (this.noPrefixDefault) return this.noPrefixDefault
91
+ }
92
+
93
+ const path = route.path || route.fullPath || ''
94
+ // 2b. prefix_and_default at /: useState/cookie can override — / is valid for any locale
95
+ if (isPrefixAndDefaultStrategy(this.i18nConfig.strategy!) && (path === '/' || path === '')) {
96
+ const fromGetter = this.getDefaultLocale?.()
97
+ if (fromGetter) return fromGetter
86
98
  }
87
99
 
88
100
  // 3. Check route.params.locale (for existing routes)
@@ -91,18 +103,21 @@ export class RouteService {
91
103
  }
92
104
 
93
105
  // 4. Extract locale from URL path (for non-existent routes)
94
- const path = route.path || route.fullPath || ''
95
106
  const localeFromPath = this.extractLocaleFromPath(path)
96
107
  if (localeFromPath) {
97
108
  return localeFromPath
98
109
  }
99
110
 
100
- // 5. Check cookie (if provided)
101
- if (this.cookieLocaleDefault) {
102
- return this.cookieLocaleDefault
111
+ // 5. For prefix_except_default: URL without locale prefix = defaultLocale
112
+ if (isPrefixExceptDefaultStrategy(this.i18nConfig.strategy!)) {
113
+ return (this.i18nConfig.defaultLocale || 'en').toString()
103
114
  }
104
115
 
105
- // 6. Return defaultLocale as fallback
116
+ // 6. Check getter (for no_prefix and other strategies)
117
+ const fromGetter = this.getDefaultLocale?.()
118
+ if (fromGetter) return fromGetter
119
+
120
+ // 7. Return defaultLocale as fallback
106
121
  return (this.i18nConfig.defaultLocale || 'en').toString()
107
122
  }
108
123
 
@@ -373,25 +388,6 @@ export class RouteService {
373
388
  return this.createLocalizedRoute(processedTo, route, currentLocale)
374
389
  }
375
390
 
376
- updateCookies(toLocale: string): void {
377
- const cookieLocaleName = this.cookieLocaleName || this.i18nConfig.localeCookie || 'user-locale'
378
- if (this.i18nConfig.hashMode) {
379
- this.setCookie('hash-locale', toLocale)
380
- // useCookie('hash-locale').value = toLocale
381
- this.hashLocaleDefault = toLocale
382
- }
383
- if (isNoPrefixStrategy(this.i18nConfig.strategy!)) {
384
- this.setCookie(cookieLocaleName, toLocale)
385
- // useCookie(cookieLocaleName).value = toLocale
386
- this.noPrefixDefault = toLocale
387
- }
388
- // Update cookie for regular strategy (prefix or prefix_except_default)
389
- if (!this.i18nConfig.hashMode && !isNoPrefixStrategy(this.i18nConfig.strategy!) && cookieLocaleName) {
390
- this.setCookie(cookieLocaleName, toLocale)
391
- this.cookieLocaleDefault = toLocale
392
- }
393
- }
394
-
395
391
  getCurrentRoute(): RouteLocationNormalizedLoaded {
396
392
  return this.router.currentRoute.value
397
393
  }
@@ -423,7 +419,6 @@ export class RouteService {
423
419
  current = to ?? this.getCurrentRoute() as RouteLocationResolved
424
420
  }
425
421
 
426
- this.updateCookies(toLocale)
427
422
  const switchedRoute = this.switchLocaleRoute(fromLocale, toLocale, current, i18nRouteParams)
428
423
 
429
424
  if (typeof switchedRoute === 'string' && switchedRoute.startsWith('http')) {
@@ -1,216 +1,106 @@
1
1
  import type { Translations } from '@i18n-micro/types'
2
2
 
3
- // Duck-typing для Ref, чтобы не тащить Vue зависимость
4
- export interface RefLike<T> {
5
- value: T
3
+ /**
4
+ * Bare Metal: Простое хранилище переводов без Ref, useState, devalue.
5
+ * Ключ Map: locale (general) или locale:routeName (page-specific).
6
+ */
7
+ export interface TranslationStorage {
8
+ translations: Map<string, Translations>
6
9
  }
7
10
 
8
- export interface TranslationCache {
9
- generalLocaleCache: RefLike<Record<string, Translations>> | Record<string, Translations>
10
- routeLocaleCache: RefLike<Record<string, Translations>> | Record<string, Translations>
11
- dynamicTranslationsCaches: RefLike<Record<string, Translations>[]> | Record<string, Translations>[]
12
- serverTranslationCache: RefLike<Record<string, Map<string, Translations | unknown>>> | Record<string, Map<string, Translations | unknown>>
13
- }
14
-
15
- // Глобальные кэши для fallback (только для unit-тестов и обратной совместимости)
16
- // НЕ используются в SSR продакшене
17
- const globalGeneralLocaleCache: Record<string, Translations> = {}
18
- const globalRouteLocaleCache: Record<string, Translations> = {}
19
- const globalDynamicTranslationsCaches: Record<string, Translations>[] = []
20
- const globalServerTranslationCache: Record<string, Map<string, Translations | unknown>> = {}
21
-
22
- function deepClone<T>(value: T): T {
23
- if (Array.isArray(value)) {
24
- return JSON.parse(JSON.stringify(value)) as T
25
- }
26
- else if (typeof value === 'object' && value !== null) {
27
- return JSON.parse(JSON.stringify(value)) as T
28
- }
29
- return value
30
- }
31
-
32
- function findTranslation<T = unknown>(translations: Translations | null, key: string): T | null {
33
- let value: string | number | boolean | Translations | unknown | null = translations
11
+ function findValue<T = unknown>(data: Translations | null | undefined, key: string): T | null {
12
+ if (!data || typeof key !== 'string') return null
34
13
 
35
- if (translations === null || typeof key !== 'string') {
36
- return null
37
- }
38
-
39
- if (translations[key]) {
40
- value = translations[key]
41
- }
42
- else {
43
- const parts = key.toString().split('.')
44
- for (const part of parts) {
45
- if (value && typeof value === 'object' && part in value) {
46
- value = (value as Translations)[part]
47
- }
48
- else {
49
- return null
50
- }
14
+ if (key in data) {
15
+ const value = data[key]
16
+ if (typeof value === 'object' && value !== null) {
17
+ return value as T
51
18
  }
19
+ return value as T
52
20
  }
53
21
 
54
- if (typeof value === 'object' && value !== null) {
55
- return deepClone(value) as T
22
+ const parts = key.split('.')
23
+ let value: unknown = data
24
+ for (const part of parts) {
25
+ if (value && typeof value === 'object' && part in (value as object)) {
26
+ value = (value as Translations)[part]
27
+ }
28
+ else {
29
+ return null
30
+ }
56
31
  }
57
-
58
32
  return (value as T) ?? null
59
33
  }
60
34
 
61
- // Вспомогательная функция для получения значения из Ref или обычного объекта
62
- function getValue<T>(refOrValue: RefLike<T> | T): T {
63
- return typeof refOrValue === 'object' && refOrValue !== null && 'value' in refOrValue
64
- ? (refOrValue as RefLike<T>).value
65
- : refOrValue as T
66
- }
67
-
68
- // Вспомогательная функция для установки значения в Ref или обычный объект
69
- function setValue<T extends Record<string, unknown>>(
70
- refOrValue: RefLike<T> | T,
71
- key: string,
72
- value: T[keyof T],
73
- ): void {
74
- const target = getValue(refOrValue)
75
- ;(target as Record<string, unknown>)[key] = value
76
- }
77
-
78
- // Вспомогательная функция для получения значения из Ref или обычного объекта по ключу
79
- function getValueByKey<T extends Record<string, unknown>>(refOrValue: RefLike<T> | T, key: string): T[keyof T] | undefined {
80
- const target = getValue(refOrValue)
81
- return (target as Record<string, unknown>)[key] as T[keyof T] | undefined
82
- }
83
-
84
- export function useTranslationHelper(caches?: TranslationCache) {
85
- // Используем переданные кэши или глобальные (fallback для тестов)
86
- const generalLocaleCache = caches?.generalLocaleCache ?? globalGeneralLocaleCache
87
- const routeLocaleCache = caches?.routeLocaleCache ?? globalRouteLocaleCache
88
- const dynamicTranslationsCaches = caches?.dynamicTranslationsCaches ?? globalDynamicTranslationsCaches
89
- const serverTranslationCache = caches?.serverTranslationCache ?? globalServerTranslationCache
35
+ export function useTranslationHelper(storage?: TranslationStorage) {
36
+ const translations = storage?.translations ?? new Map<string, Translations>()
90
37
 
91
38
  return {
92
39
  hasCache(locale: string, page: string) {
93
40
  const cacheKey = `${locale}:${page}`
94
- const cache = getValueByKey(serverTranslationCache, cacheKey)
95
- return (cache ?? new Map<string, Translations | unknown>()).size > 0
41
+ return translations.has(cacheKey) || translations.has(locale)
96
42
  },
97
43
  getCache(locale: string, routeName: string) {
98
44
  const cacheKey = `${locale}:${routeName}`
99
- return getValueByKey(serverTranslationCache, cacheKey)
45
+ return translations.get(cacheKey)
100
46
  },
101
- setCache(locale: string, routeName: string, cache: Map<string, Translations | unknown>) {
102
- const cacheKey = `${locale}:${routeName}`
103
- setValue(serverTranslationCache, cacheKey, cache)
47
+ setCache(_locale: string, _routeName: string, _cache: Map<string, unknown>) {
48
+ // No-op for bare metal
104
49
  },
105
- mergeTranslation(locale: string, routeName: string, newTranslations: Translations, force = false) {
106
- const cacheKey = `${locale}:${routeName}`
107
- const currentCache = getValueByKey(routeLocaleCache, cacheKey)
108
-
109
- if (currentCache || force) {
110
- const existing = currentCache ?? {}
111
- setValue(routeLocaleCache, cacheKey, {
112
- ...existing,
113
- ...newTranslations,
114
- })
115
- }
116
-
117
- const isDev = process.env.NODE_ENV !== 'production'
118
- if (!currentCache && isDev) {
119
- // Если кэша нет, выводим предупреждение в dev-режиме и ничего не делаем.
120
- console.warn(`[i18n] mergeTranslation called for '${cacheKey}' which was not pre-loaded. Skipping merge. Use force: true if this is intentional.`)
50
+ hasTranslation(locale: string, key: string): boolean {
51
+ for (const [k, v] of translations) {
52
+ if ((k === locale || k.startsWith(`${locale}:`)) && findValue(v, key) !== null) {
53
+ return true
54
+ }
121
55
  }
56
+ return false
122
57
  },
123
- mergeGlobalTranslation(locale: string, newTranslations: Translations, force = false) {
124
- const currentCache = getValueByKey(generalLocaleCache, locale)
125
- if (!force && !currentCache) {
126
- console.error(`marge: route ${locale} not loaded`)
127
- }
128
- const existing = currentCache ?? {}
129
- setValue(generalLocaleCache, locale, {
130
- ...existing,
131
- ...newTranslations,
132
- })
58
+ hasGeneralTranslation(locale: string): boolean {
59
+ return translations.has(locale)
133
60
  },
134
- hasGeneralTranslation(locale: string) {
135
- return !!getValueByKey(generalLocaleCache, locale)
61
+ hasPageTranslation(locale: string, routeName: string): boolean {
62
+ return translations.has(`${locale}:${routeName}`)
136
63
  },
137
- hasPageTranslation(locale: string, routeName: string) {
138
- const cacheKey = `${locale}:${routeName}`
139
-
140
- return !!getValueByKey(routeLocaleCache, cacheKey)
64
+ getTranslation<T = unknown>(locale: string, routeName: string, key: string): T | null {
65
+ const routeKey = `${locale}:${routeName}`
66
+ const routeData = translations.get(routeKey)
67
+ const val = findValue<T>(routeData, key)
68
+ if (val !== null) return val
69
+
70
+ const generalData = translations.get(locale)
71
+ return findValue<T>(generalData, key)
141
72
  },
142
- hasTranslation: (locale: string, key: import('@i18n-micro/types').TranslationKey): boolean => {
143
- const dynamicCaches = getValue(dynamicTranslationsCaches)
144
- for (const dynamicCache of dynamicCaches) {
145
- if (findTranslation(dynamicCache[locale] || null, key) !== null) {
146
- return true
147
- }
148
- }
149
-
150
- const generalCache = getValueByKey(generalLocaleCache, locale)
151
- return findTranslation(generalCache || null, key) !== null
73
+ loadTranslations(locale: string, data: Translations): void {
74
+ // Merge with existing, replacing duplicate keys
75
+ const existing = translations.get(locale) ?? {}
76
+ translations.set(locale, { ...existing, ...data })
152
77
  },
153
- getTranslation: <T = unknown>(locale: string, routeName: string, key: import('@i18n-micro/types').TranslationKey): T | null => {
154
- const cacheKey = `${locale}:${routeName}`
155
- const serverCache = getValueByKey(serverTranslationCache, cacheKey)
156
- const cached = serverCache?.get(key)
157
- if (cached) {
158
- return cached as T
159
- }
160
-
161
- let result: T | null = null
162
-
163
- const dynamicCaches = getValue(dynamicTranslationsCaches)
164
- for (const dynamicCache of dynamicCaches) {
165
- result = findTranslation<T>(dynamicCache[locale] || null, key)
166
- if (result !== null) break
167
- }
168
-
169
- if (!result) {
170
- const routeCache = getValueByKey(routeLocaleCache, cacheKey)
171
- const generalCache = getValueByKey(generalLocaleCache, locale)
172
- result = findTranslation<T>(routeCache || null, key)
173
- ?? findTranslation<T>(generalCache || null, key)
78
+ setTranslations(locale: string, data: Translations): void {
79
+ // Replace all translations for locale (no merge)
80
+ translations.set(locale, data)
81
+ },
82
+ loadPageTranslations(locale: string, routeName: string, data: Translations): void {
83
+ const key = `${locale}:${routeName}`
84
+ const existing = translations.get(key)
85
+ // Perf: при пустом existing — сохраняем ссылку, избегаем O(n) копирования больших объектов
86
+ if (!existing || Object.keys(existing).length === 0) {
87
+ translations.set(key, data)
174
88
  }
175
-
176
- if (result) {
177
- const currentServerCache = serverCache ?? new Map<string, Translations | unknown>()
178
- currentServerCache.set(key, result)
179
- setValue(serverTranslationCache, cacheKey, currentServerCache)
89
+ else {
90
+ translations.set(key, { ...existing, ...data })
180
91
  }
181
-
182
- return result
183
92
  },
184
- async loadPageTranslations(locale: string, routeName: string, translations: Translations): Promise<void> {
185
- const cacheKey = `${locale}:${routeName}`
186
- setValue(routeLocaleCache, cacheKey, { ...translations })
93
+ mergeTranslation(locale: string, routeName: string, newTranslations: Translations, _force = false): void {
94
+ const key = `${locale}:${routeName}`
95
+ const existing = translations.get(key) ?? {}
96
+ translations.set(key, { ...existing, ...newTranslations })
187
97
  },
188
- async loadTranslations(locale: string, translations: Translations): Promise<void> {
189
- setValue(generalLocaleCache, locale, { ...translations })
98
+ mergeGlobalTranslation(locale: string, newTranslations: Translations, _force = false): void {
99
+ const existing = translations.get(locale) ?? {}
100
+ translations.set(locale, { ...existing, ...newTranslations })
190
101
  },
191
- clearCache() {
192
- // Clear general cache
193
- const generalCache = getValue(generalLocaleCache)
194
- Object.keys(generalCache).forEach((key) => {
195
- setValue(generalLocaleCache, key, {})
196
- })
197
-
198
- // Clear route-specific cache
199
- const routeCache = getValue(routeLocaleCache)
200
- Object.keys(routeCache).forEach((key) => {
201
- setValue(routeLocaleCache, key, {})
202
- })
203
-
204
- // Clear dynamic caches
205
- const dynamicCaches = getValue(dynamicTranslationsCaches)
206
- dynamicCaches.length = 0
207
-
208
- // Clear server translation cache
209
- const serverCache = getValue(serverTranslationCache)
210
- Object.keys(serverCache).forEach((key) => {
211
- const cacheMap = getValueByKey(serverTranslationCache, key)
212
- cacheMap?.clear()
213
- })
102
+ clearCache(): void {
103
+ translations.clear()
214
104
  },
215
105
  }
216
106
  }
@@ -49,14 +49,9 @@ describe('BaseI18n', () => {
49
49
  expect(i18n.getRoute()).toBe('general')
50
50
  })
51
51
 
52
- test('should initialize with custom cache', () => {
53
- const cache = {
54
- generalLocaleCache: {},
55
- routeLocaleCache: {},
56
- dynamicTranslationsCaches: [],
57
- serverTranslationCache: {},
58
- }
59
- const i18n = new TestI18n('en', 'en', 'general', { cache })
52
+ test('should initialize with custom storage', () => {
53
+ const storage = { translations: new Map<string, Translations>() }
54
+ const i18n = new TestI18n('en', 'en', 'general', { storage })
60
55
  expect(i18n).toBeDefined()
61
56
  })
62
57
 
@@ -93,13 +88,8 @@ describe('BaseI18n', () => {
93
88
  })
94
89
 
95
90
  test('should interpolate params in translation', async () => {
96
- const cache = {
97
- generalLocaleCache: {},
98
- routeLocaleCache: {},
99
- dynamicTranslationsCaches: [],
100
- serverTranslationCache: {},
101
- }
102
- const i18n = new TestI18n('en', 'en', 'general', { cache })
91
+ const storage = { translations: new Map<string, Translations>() }
92
+ const i18n = new TestI18n('en', 'en', 'general', { storage })
103
93
  const translations: Translations = { greeting: 'Hello, {name}!' }
104
94
  await i18n['helper'].loadTranslations('en', translations)
105
95
 
@@ -117,13 +107,8 @@ describe('BaseI18n', () => {
117
107
  })
118
108
 
119
109
  test('should fallback to fallbackLocale when translation is missing', async () => {
120
- const cache = {
121
- generalLocaleCache: {},
122
- routeLocaleCache: {},
123
- dynamicTranslationsCaches: [],
124
- serverTranslationCache: {},
125
- }
126
- const i18n = new TestI18n('en', 'fr', 'general', { cache })
110
+ const storage = { translations: new Map<string, Translations>() }
111
+ const i18n = new TestI18n('en', 'fr', 'general', { storage })
127
112
  const translations: Translations = { greeting: 'Bonjour' }
128
113
  await i18n['helper'].loadTranslations('fr', translations)
129
114
 
@@ -139,15 +124,10 @@ describe('BaseI18n', () => {
139
124
  })
140
125
 
141
126
  test('should use previousPageInfo fallback when enabled', async () => {
142
- const cache = {
143
- generalLocaleCache: {},
144
- routeLocaleCache: {},
145
- dynamicTranslationsCaches: [],
146
- serverTranslationCache: {},
147
- }
127
+ const storage = { translations: new Map<string, Translations>() }
148
128
  const prevInfo = { locale: 'fr', routeName: 'previous' }
149
129
  const i18n = new TestI18n('en', 'en', 'general', {
150
- cache,
130
+ storage,
151
131
  getPreviousPageInfo: () => prevInfo,
152
132
  enablePreviousPageFallback: true,
153
133
  })
@@ -479,13 +459,8 @@ describe('BaseI18n', () => {
479
459
  })
480
460
 
481
461
  test('should handle route change', async () => {
482
- const cache = {
483
- generalLocaleCache: {},
484
- routeLocaleCache: {},
485
- dynamicTranslationsCaches: [],
486
- serverTranslationCache: {},
487
- }
488
- const i18n = new TestI18n('en', 'en', 'general', { cache })
462
+ const storage = { translations: new Map<string, Translations>() }
463
+ const i18n = new TestI18n('en', 'en', 'general', { storage })
489
464
  const generalTranslations: Translations = { greeting: 'Hello' }
490
465
  const routeTranslations: Translations = { title: 'About Page' }
491
466