@i18n-micro/core 1.0.27 → 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.
@@ -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
  }