@i18n-micro/core 1.0.27
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/LICENSE +21 -0
- package/README.md +176 -0
- package/dist/format-service.d.ts +5 -0
- package/dist/helpers.d.ts +18 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +366 -0
- package/dist/route-service.d.ts +40 -0
- package/dist/translation.d.ts +24 -0
- package/fix.ts +1 -0
- package/jest.config.cjs +7 -0
- package/package.json +35 -0
- package/src/format-service.ts +41 -0
- package/src/helpers.ts +53 -0
- package/src/index.ts +18 -0
- package/src/route-service.ts +473 -0
- package/src/translation.ts +216 -0
- package/tests/core.test.ts +81 -0
- package/tests/format-service.test.ts +101 -0
- package/tests/helpers.test.ts +103 -0
- package/tests/route-service.test.ts +377 -0
- package/tsconfig.json +24 -0
- package/vite.config.mts +25 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { Translations } from '@i18n-micro/types'
|
|
2
|
+
|
|
3
|
+
// Duck-typing для Ref, чтобы не тащить Vue зависимость
|
|
4
|
+
export interface RefLike<T> {
|
|
5
|
+
value: T
|
|
6
|
+
}
|
|
7
|
+
|
|
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
|
|
34
|
+
|
|
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
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof value === 'object' && value !== null) {
|
|
55
|
+
return deepClone(value) as T
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (value as T) ?? null
|
|
59
|
+
}
|
|
60
|
+
|
|
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
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
hasCache(locale: string, page: string) {
|
|
93
|
+
const cacheKey = `${locale}:${page}`
|
|
94
|
+
const cache = getValueByKey(serverTranslationCache, cacheKey)
|
|
95
|
+
return (cache ?? new Map<string, Translations | unknown>()).size > 0
|
|
96
|
+
},
|
|
97
|
+
getCache(locale: string, routeName: string) {
|
|
98
|
+
const cacheKey = `${locale}:${routeName}`
|
|
99
|
+
return getValueByKey(serverTranslationCache, cacheKey)
|
|
100
|
+
},
|
|
101
|
+
setCache(locale: string, routeName: string, cache: Map<string, Translations | unknown>) {
|
|
102
|
+
const cacheKey = `${locale}:${routeName}`
|
|
103
|
+
setValue(serverTranslationCache, cacheKey, cache)
|
|
104
|
+
},
|
|
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.`)
|
|
121
|
+
}
|
|
122
|
+
},
|
|
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
|
+
})
|
|
133
|
+
},
|
|
134
|
+
hasGeneralTranslation(locale: string) {
|
|
135
|
+
return !!getValueByKey(generalLocaleCache, locale)
|
|
136
|
+
},
|
|
137
|
+
hasPageTranslation(locale: string, routeName: string) {
|
|
138
|
+
const cacheKey = `${locale}:${routeName}`
|
|
139
|
+
|
|
140
|
+
return !!getValueByKey(routeLocaleCache, cacheKey)
|
|
141
|
+
},
|
|
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
|
|
152
|
+
},
|
|
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)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (result) {
|
|
177
|
+
const currentServerCache = serverCache ?? new Map<string, Translations | unknown>()
|
|
178
|
+
currentServerCache.set(key, result)
|
|
179
|
+
setValue(serverTranslationCache, cacheKey, currentServerCache)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return result
|
|
183
|
+
},
|
|
184
|
+
async loadPageTranslations(locale: string, routeName: string, translations: Translations): Promise<void> {
|
|
185
|
+
const cacheKey = `${locale}:${routeName}`
|
|
186
|
+
setValue(routeLocaleCache, cacheKey, { ...translations })
|
|
187
|
+
},
|
|
188
|
+
async loadTranslations(locale: string, translations: Translations): Promise<void> {
|
|
189
|
+
setValue(generalLocaleCache, locale, { ...translations })
|
|
190
|
+
},
|
|
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
|
+
})
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useTranslationHelper, interpolate } from '../src'
|
|
2
|
+
|
|
3
|
+
describe('Translation Helper', () => {
|
|
4
|
+
const translations = {
|
|
5
|
+
en: {
|
|
6
|
+
greeting: 'Hello, {name}!',
|
|
7
|
+
nested: {
|
|
8
|
+
message: 'This is a nested message.',
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
fr: {
|
|
12
|
+
greeting: 'Bonjour, {name}!',
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('interpolate function replaces placeholders correctly', () => {
|
|
17
|
+
const result = interpolate('Hello, {name}!', { name: 'John' })
|
|
18
|
+
expect(result).toBe('Hello, John!')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('interpolate function handles missing placeholders gracefully', () => {
|
|
22
|
+
const result = interpolate('Hello, {name}!', { age: 30 })
|
|
23
|
+
expect(result).toBe('Hello, {name}!')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('getTranslation fetches correct translation', () => {
|
|
27
|
+
const helper = useTranslationHelper()
|
|
28
|
+
helper.loadTranslations('en', translations.en)
|
|
29
|
+
|
|
30
|
+
const translation = helper.getTranslation('en', 'index', 'greeting')
|
|
31
|
+
expect(translation).toBe('Hello, {name}!')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('getTranslation supports nested keys', () => {
|
|
35
|
+
const helper = useTranslationHelper()
|
|
36
|
+
helper.loadTranslations('en', translations.en)
|
|
37
|
+
|
|
38
|
+
const translation = helper.getTranslation('en', 'index', 'nested.message')
|
|
39
|
+
expect(translation).toBe('This is a nested message.')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('getTranslation falls back when translation is missing', () => {
|
|
43
|
+
const helper = useTranslationHelper()
|
|
44
|
+
helper.loadTranslations('en', translations.en)
|
|
45
|
+
|
|
46
|
+
const translation = helper.getTranslation('en', 'index', 'nonexistent.key')
|
|
47
|
+
expect(translation).toBeNull()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('loadPageTranslations correctly caches translations', async () => {
|
|
51
|
+
const helper = useTranslationHelper()
|
|
52
|
+
await helper.loadPageTranslations('fr', 'home', translations.fr)
|
|
53
|
+
|
|
54
|
+
expect(helper.hasPageTranslation('fr', 'home')).toBe(true)
|
|
55
|
+
expect(helper.getTranslation('fr', 'home', 'greeting')).toBe('Bonjour, {name}!')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('mergeTranslation updates route translations', () => {
|
|
59
|
+
const helper = useTranslationHelper()
|
|
60
|
+
helper.loadPageTranslations('en', 'home', translations.en)
|
|
61
|
+
|
|
62
|
+
helper.mergeTranslation('en', 'home', { newKey: 'New value' })
|
|
63
|
+
expect(helper.getTranslation('en', 'home', 'newKey')).toBe('New value')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('mergeGlobalTranslation updates general translations', () => {
|
|
67
|
+
const helper = useTranslationHelper()
|
|
68
|
+
helper.loadTranslations('en', translations.en)
|
|
69
|
+
|
|
70
|
+
helper.mergeGlobalTranslation('en', { newGlobalKey: 'Global value' })
|
|
71
|
+
expect(helper.getTranslation('en', 'index', 'newGlobalKey')).toBe('Global value')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('deepClone creates a deep copy of objects', () => {
|
|
75
|
+
const original = { nested: { key: 'value' } }
|
|
76
|
+
const cloned = JSON.parse(JSON.stringify(original))
|
|
77
|
+
|
|
78
|
+
expect(cloned).toEqual(original)
|
|
79
|
+
expect(cloned).not.toBe(original)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { FormatService } from '../src'
|
|
2
|
+
|
|
3
|
+
describe('FormatService', () => {
|
|
4
|
+
let formatService: FormatService
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
formatService = new FormatService()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
describe('formatNumber', () => {
|
|
11
|
+
test('should format number with default options', () => {
|
|
12
|
+
const result = formatService.formatNumber(123456.789, 'en-US')
|
|
13
|
+
expect(result).toBe('123,456.789')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('should format number with custom options', () => {
|
|
17
|
+
const result = formatService.formatNumber(123456.789, 'de-DE', {
|
|
18
|
+
style: 'currency',
|
|
19
|
+
currency: 'EUR',
|
|
20
|
+
})
|
|
21
|
+
expect(result).toBe('123.456,79 €')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('should handle invalid locale by falling back to default formatting', () => {
|
|
25
|
+
const result = formatService.formatNumber(123456.789, 'invalid-locale')
|
|
26
|
+
expect(result).toBe('123,456.789') // Fallback to default formatting
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('formatDate', () => {
|
|
31
|
+
test('should format date with default options', () => {
|
|
32
|
+
const date = new Date('2023-10-05T12:34:56Z')
|
|
33
|
+
const result = formatService.formatDate(date, 'en-US')
|
|
34
|
+
expect(result).toBe('10/5/2023')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('should format date with custom options', () => {
|
|
38
|
+
const date = new Date('2023-10-05T12:34:56Z')
|
|
39
|
+
const result = formatService.formatDate(date, 'de-DE', {
|
|
40
|
+
year: 'numeric',
|
|
41
|
+
month: 'long',
|
|
42
|
+
day: 'numeric',
|
|
43
|
+
})
|
|
44
|
+
expect(result).toBe('5. Oktober 2023')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('should handle invalid date by returning "Invalid Date"', () => {
|
|
48
|
+
const result = formatService.formatDate('invalid-date', 'en-US')
|
|
49
|
+
expect(result).toBe('Invalid Date')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('formatRelativeTime', () => {
|
|
54
|
+
test('should format relative time for seconds', () => {
|
|
55
|
+
const now = new Date()
|
|
56
|
+
const past = new Date(now.getTime() - 30 * 1000) // 30 seconds ago
|
|
57
|
+
const result = formatService.formatRelativeTime(past, 'en-US')
|
|
58
|
+
expect(result).toBe('30 seconds ago')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('should format relative time for minutes', () => {
|
|
62
|
+
const now = new Date()
|
|
63
|
+
const past = new Date(now.getTime() - 5 * 60 * 1000) // 5 minutes ago
|
|
64
|
+
const result = formatService.formatRelativeTime(past, 'en-US')
|
|
65
|
+
expect(result).toBe('5 minutes ago')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('should format relative time for hours', () => {
|
|
69
|
+
const now = new Date()
|
|
70
|
+
const past = new Date(now.getTime() - 2 * 60 * 60 * 1000) // 2 hours ago
|
|
71
|
+
const result = formatService.formatRelativeTime(past, 'en-US')
|
|
72
|
+
expect(result).toBe('2 hours ago')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('should format relative time for days', () => {
|
|
76
|
+
const now = new Date()
|
|
77
|
+
const past = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000) // 3 days ago
|
|
78
|
+
const result = formatService.formatRelativeTime(past, 'en-US')
|
|
79
|
+
expect(result).toBe('3 days ago')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('should format relative time for months', () => {
|
|
83
|
+
const now = new Date()
|
|
84
|
+
const past = new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000) // ~2 months ago
|
|
85
|
+
const result = formatService.formatRelativeTime(past, 'en-US')
|
|
86
|
+
expect(result).toBe('2 months ago')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('should format relative time for years', () => {
|
|
90
|
+
const now = new Date()
|
|
91
|
+
const past = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000) // 1 year ago
|
|
92
|
+
const result = formatService.formatRelativeTime(past, 'en-US')
|
|
93
|
+
expect(result).toBe('1 year ago')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('should handle invalid date by returning "0 seconds ago"', () => {
|
|
97
|
+
const result = formatService.formatRelativeTime('invalid-date', 'en-US')
|
|
98
|
+
expect(result).toBe('in 0 seconds')
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
interpolate,
|
|
3
|
+
withPrefixStrategy,
|
|
4
|
+
isNoPrefixStrategy,
|
|
5
|
+
isPrefixStrategy,
|
|
6
|
+
isPrefixExceptDefaultStrategy,
|
|
7
|
+
isPrefixAndDefaultStrategy,
|
|
8
|
+
} from '../src/helpers'
|
|
9
|
+
|
|
10
|
+
describe('Helpers', () => {
|
|
11
|
+
describe('interpolate', () => {
|
|
12
|
+
test('should replace placeholders with params', () => {
|
|
13
|
+
const template = 'Hello, {name}! Your age is {age}.'
|
|
14
|
+
const params = { name: 'John', age: 30 }
|
|
15
|
+
const result = interpolate(template, params)
|
|
16
|
+
expect(result).toBe('Hello, John! Your age is 30.')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('should handle missing params by leaving placeholders', () => {
|
|
20
|
+
const template = 'Hello, {name}! Your age is {age}.'
|
|
21
|
+
const params = { name: 'John' } // age is missing
|
|
22
|
+
const result = interpolate(template, params)
|
|
23
|
+
expect(result).toBe('Hello, John! Your age is {age}.')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('should handle empty params', () => {
|
|
27
|
+
const template = 'Hello, {name}!'
|
|
28
|
+
const params = {}
|
|
29
|
+
const result = interpolate(template, params)
|
|
30
|
+
expect(result).toBe('Hello, {name}!')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('should handle empty template', () => {
|
|
34
|
+
const template = ''
|
|
35
|
+
const params = { name: 'John' }
|
|
36
|
+
const result = interpolate(template, params)
|
|
37
|
+
expect(result).toBe('')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('withPrefixStrategy', () => {
|
|
42
|
+
test('should return true for "prefix" strategy', () => {
|
|
43
|
+
expect(withPrefixStrategy('prefix')).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('should return true for "prefix_and_default" strategy', () => {
|
|
47
|
+
expect(withPrefixStrategy('prefix_and_default')).toBe(true)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('should return false for other strategies', () => {
|
|
51
|
+
expect(withPrefixStrategy('no_prefix')).toBe(false)
|
|
52
|
+
expect(withPrefixStrategy('prefix_except_default')).toBe(false)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('isNoPrefixStrategy', () => {
|
|
57
|
+
test('should return true for "no_prefix" strategy', () => {
|
|
58
|
+
expect(isNoPrefixStrategy('no_prefix')).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('should return false for other strategies', () => {
|
|
62
|
+
expect(isNoPrefixStrategy('prefix')).toBe(false)
|
|
63
|
+
expect(isNoPrefixStrategy('prefix_and_default')).toBe(false)
|
|
64
|
+
expect(isNoPrefixStrategy('prefix_except_default')).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('isPrefixStrategy', () => {
|
|
69
|
+
test('should return true for "prefix" strategy', () => {
|
|
70
|
+
expect(isPrefixStrategy('prefix')).toBe(true)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('should return false for other strategies', () => {
|
|
74
|
+
expect(isPrefixStrategy('no_prefix')).toBe(false)
|
|
75
|
+
expect(isPrefixStrategy('prefix_and_default')).toBe(false)
|
|
76
|
+
expect(isPrefixStrategy('prefix_except_default')).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('isPrefixExceptDefaultStrategy', () => {
|
|
81
|
+
test('should return true for "prefix_except_default" strategy', () => {
|
|
82
|
+
expect(isPrefixExceptDefaultStrategy('prefix_except_default')).toBe(true)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('should return false for other strategies', () => {
|
|
86
|
+
expect(isPrefixExceptDefaultStrategy('no_prefix')).toBe(false)
|
|
87
|
+
expect(isPrefixExceptDefaultStrategy('prefix')).toBe(false)
|
|
88
|
+
expect(isPrefixExceptDefaultStrategy('prefix_and_default')).toBe(false)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('isPrefixAndDefaultStrategy', () => {
|
|
93
|
+
test('should return true for "prefix_and_default" strategy', () => {
|
|
94
|
+
expect(isPrefixAndDefaultStrategy('prefix_and_default')).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('should return false for other strategies', () => {
|
|
98
|
+
expect(isPrefixAndDefaultStrategy('no_prefix')).toBe(false)
|
|
99
|
+
expect(isPrefixAndDefaultStrategy('prefix')).toBe(false)
|
|
100
|
+
expect(isPrefixAndDefaultStrategy('prefix_except_default')).toBe(false)
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
})
|