import type { Translations } from 'nuxt-i18n-micro-types' import type { Ref } from 'vue' export interface TranslationCache { generalLocaleCache: Ref> | Record routeLocaleCache: Ref> | Record dynamicTranslationsCaches: Ref[]> | Record[] serverTranslationCache: Ref>> | Record> } // Глобальные кэши для fallback (только для unit-тестов и обратной совместимости) // НЕ используются в SSR продакшене const globalGeneralLocaleCache: Record = {} const globalRouteLocaleCache: Record = {} const globalDynamicTranslationsCaches: Record[] = [] const globalServerTranslationCache: Record> = {} function deepClone(value: T): T { if (Array.isArray(value)) { return JSON.parse(JSON.stringify(value)) as T } else if (typeof value === 'object' && value !== null) { return JSON.parse(JSON.stringify(value)) as T } return value } function findTranslation(translations: Translations | null, key: string): T | null { let value: string | number | boolean | Translations | unknown | null = translations if (translations === null || typeof key !== 'string') { return null } if (translations[key]) { value = translations[key] } else { const parts = key.toString().split('.') for (const part of parts) { if (value && typeof value === 'object' && part in value) { value = (value as Translations)[part] } else { return null } } } if (typeof value === 'object' && value !== null) { return deepClone(value) as T } return (value as T) ?? null } // Вспомогательная функция для получения значения из Ref или обычного объекта function getValue(refOrValue: Ref | T): T { return typeof refOrValue === 'object' && refOrValue !== null && 'value' in refOrValue ? (refOrValue as Ref).value : refOrValue as T } // Вспомогательная функция для установки значения в Ref или обычный объект function setValue>( refOrValue: Ref | T, key: string, value: T[keyof T], ): void { const target = getValue(refOrValue) ;(target as Record)[key] = value } // Вспомогательная функция для получения значения из Ref или обычного объекта по ключу function getValueByKey>(refOrValue: Ref | T, key: string): T[keyof T] | undefined { const target = getValue(refOrValue) return (target as Record)[key] as T[keyof T] | undefined } export function useTranslationHelper(caches?: TranslationCache) { // Используем переданные кэши или глобальные (fallback для тестов) const generalLocaleCache = caches?.generalLocaleCache ?? globalGeneralLocaleCache const routeLocaleCache = caches?.routeLocaleCache ?? globalRouteLocaleCache const dynamicTranslationsCaches = caches?.dynamicTranslationsCaches ?? globalDynamicTranslationsCaches const serverTranslationCache = caches?.serverTranslationCache ?? globalServerTranslationCache return { hasCache(locale: string, page: string) { const cacheKey = `${locale}:${page}` const cache = getValueByKey(serverTranslationCache, cacheKey) return (cache ?? new Map()).size > 0 }, getCache(locale: string, routeName: string) { const cacheKey = `${locale}:${routeName}` return getValueByKey(serverTranslationCache, cacheKey) }, setCache(locale: string, routeName: string, cache: Map) { const cacheKey = `${locale}:${routeName}` setValue(serverTranslationCache, cacheKey, cache) }, mergeTranslation(locale: string, routeName: string, newTranslations: Translations, force = false) { const cacheKey = `${locale}:${routeName}` const currentCache = getValueByKey(routeLocaleCache, cacheKey) if (currentCache || force) { const existing = currentCache ?? {} setValue(routeLocaleCache, cacheKey, { ...existing, ...newTranslations, }) } const isDev = process.env.NODE_ENV !== 'production' if (!currentCache && isDev) { // Если кэша нет, выводим предупреждение в dev-режиме и ничего не делаем. console.warn(`[i18n] mergeTranslation called for '${cacheKey}' which was not pre-loaded. Skipping merge. Use force: true if this is intentional.`) } }, mergeGlobalTranslation(locale: string, newTranslations: Translations, force = false) { const currentCache = getValueByKey(generalLocaleCache, locale) if (!force && !currentCache) { console.error(`marge: route ${locale} not loaded`) } const existing = currentCache ?? {} setValue(generalLocaleCache, locale, { ...existing, ...newTranslations, }) }, hasGeneralTranslation(locale: string) { return !!getValueByKey(generalLocaleCache, locale) }, hasPageTranslation(locale: string, routeName: string) { const cacheKey = `${locale}:${routeName}` return !!getValueByKey(routeLocaleCache, cacheKey) }, hasTranslation: (locale: string, key: string): boolean => { const dynamicCaches = getValue(dynamicTranslationsCaches) for (const dynamicCache of dynamicCaches) { if (findTranslation(dynamicCache[locale] || null, key) !== null) { return true } } const generalCache = getValueByKey(generalLocaleCache, locale) return findTranslation(generalCache || null, key) !== null }, getTranslation: (locale: string, routeName: string, key: string): T | null => { const cacheKey = `${locale}:${routeName}` const serverCache = getValueByKey(serverTranslationCache, cacheKey) const cached = serverCache?.get(key) if (cached) { return cached as T } let result: T | null = null const dynamicCaches = getValue(dynamicTranslationsCaches) for (const dynamicCache of dynamicCaches) { result = findTranslation(dynamicCache[locale] || null, key) if (result !== null) break } if (!result) { const routeCache = getValueByKey(routeLocaleCache, cacheKey) const generalCache = getValueByKey(generalLocaleCache, locale) result = findTranslation(routeCache || null, key) ?? findTranslation(generalCache || null, key) } if (result) { const currentServerCache = serverCache ?? new Map() currentServerCache.set(key, result) setValue(serverTranslationCache, cacheKey, currentServerCache) } return result }, async loadPageTranslations(locale: string, routeName: string, translations: Translations): Promise { const cacheKey = `${locale}:${routeName}` setValue(routeLocaleCache, cacheKey, { ...translations }) }, async loadTranslations(locale: string, translations: Translations): Promise { setValue(generalLocaleCache, locale, { ...translations }) }, clearCache() { // Clear general cache const generalCache = getValue(generalLocaleCache) Object.keys(generalCache).forEach((key) => { setValue(generalLocaleCache, key, {}) }) // Clear route-specific cache const routeCache = getValue(routeLocaleCache) Object.keys(routeCache).forEach((key) => { setValue(routeLocaleCache, key, {}) }) // Clear dynamic caches const dynamicCaches = getValue(dynamicTranslationsCaches) dynamicCaches.length = 0 // Clear server translation cache const serverCache = getValue(serverTranslationCache) Object.keys(serverCache).forEach((key) => { const cacheMap = getValueByKey(serverTranslationCache, key) cacheMap?.clear() }) }, } }