UNPKG

nuxt-i18n-micro

Version:

Nuxt I18n Micro is a lightweight, high-performance internationalization module for Nuxt, designed to handle multi-language support with minimal overhead, fast build times, and efficient runtime performance.

336 lines (335 loc) 12.5 kB
import { BaseI18n, isNoPrefixStrategy, mergeTranslationChunk, resolveTranslation, translationCacheKey } from "@i18n-micro/core"; import { deepMergeTranslations } from "@i18n-micro/utils/deep-merge"; import { shallowRef, triggerRef, unref } from "vue"; import { translationStorage } from "./storage.js"; function toRouteLocationResolved(result) { const fullPath = result.fullPath ?? result.path ?? ""; const path = result.path ?? fullPath.split("?")[0]?.split("#")[0] ?? fullPath; const out = { path, fullPath, href: fullPath }; if (result.query && Object.keys(result.query).length) out.query = result.query; if (result.hash) out.hash = result.hash; return out; } function extractSwitchLocalePath(localeRoute, resolveRouter) { if (!localeRoute || typeof localeRoute !== "object") return String(localeRoute ?? ""); if ("fullPath" in localeRoute && localeRoute.fullPath) return localeRoute.fullPath; if ("path" in localeRoute && localeRoute.path) return localeRoute.path; if ("name" in localeRoute && localeRoute.name && resolveRouter.hasRoute(String(localeRoute.name))) { return resolveRouter.resolve(localeRoute).fullPath; } return ""; } export class NuxtI18n extends BaseI18n { storage; contextSignal; cachedTranslations = {}; pendingCleanState = null; currentLocale = ""; currentRouteName = ""; resolveRouteContext; constructor(options = {}) { const storage = { translations: /* @__PURE__ */ new Map() }; super({ ...options, storage }); this.storage = storage; this.contextSignal = shallowRef(0); } setRouteContextResolver(resolver) { this.resolveRouteContext = resolver; } getCacheKey(locale, routeName) { return translationCacheKey(locale, routeName); } getChunk(locale, routeName) { return this.storage.translations.get(this.getCacheKey(locale, routeName)) ?? {}; } setChunk(locale, routeName, data) { this.storage.translations.set(this.getCacheKey(locale, routeName), data); } hasChunk(locale, routeName) { return this.storage.translations.has(this.getCacheKey(locale, routeName)); } mergeChunk(locale, routeName, data) { const cacheKey = this.getCacheKey(locale, routeName); const existing = this.storage.translations.get(cacheKey) ?? {}; const merged = mergeTranslationChunk(existing, data); this.storage.translations.set(cacheKey, merged); return merged; } getLocale() { return this.currentLocale; } getFallbackLocale() { return this.currentLocale; } getRoute() { return this.currentRouteName || "index"; } getCurrentLocale() { return this.currentLocale; } getCurrentRouteName() { return this.currentRouteName; } applySwitchContext(locale, routeName, data) { this.setChunk(locale, routeName, data); const sameLocale = this.currentLocale === locale; if (sameLocale) { this.cachedTranslations = deepMergeTranslations(this.cachedTranslations, data); this.pendingCleanState = data; } else { this.cachedTranslations = data; this.pendingCleanState = null; } this.currentLocale = locale; this.currentRouteName = routeName || ""; triggerRef(this.contextSignal); } finishTransition() { if (this.pendingCleanState) { this.cachedTranslations = this.pendingCleanState; this.pendingCleanState = null; } } mergeTranslations(newTranslations) { const merged = this.mergeChunk(this.currentLocale, this.currentRouteName, newTranslations); this.cachedTranslations = deepMergeTranslations(this.cachedTranslations, merged); if (this.pendingCleanState) this.pendingCleanState = merged; triggerRef(this.contextSignal); } async loadPageTranslations(locale, routeName, translations) { const mergedChunk = this.mergeChunk(locale, routeName, translations); if (locale === this.currentLocale && routeName === this.currentRouteName) { this.cachedTranslations = deepMergeTranslations(this.cachedTranslations, mergedChunk); if (this.pendingCleanState) this.pendingCleanState = mergedChunk; triggerRef(this.contextSignal); } } async mergeTranslationAsync(locale, routeName, newTranslations, loadIfMissing) { if (!this.hasChunk(locale, routeName)) { const loaded = await loadIfMissing(locale, routeName || void 0); this.setChunk(locale, routeName, loaded); } const mergedChunk = this.mergeChunk(locale, routeName, newTranslations); if (locale === this.currentLocale && routeName === this.currentRouteName) { this.cachedTranslations = deepMergeTranslations(this.cachedTranslations, mergedChunk); if (this.pendingCleanState) this.pendingCleanState = mergedChunk; triggerRef(this.contextSignal); } } tForRoute(route) { return (key, params, defaultValue) => this.t(key, params, defaultValue, route); } tsForRoute(route) { return (key, params, defaultValue) => this.ts(key, params, defaultValue, route); } touch() { this.contextSignal.value; } resolveLookup(key, routeContext) { const translations = routeContext && this.resolveRouteContext ? (() => { const { locale, routeName } = this.resolveRouteContext(routeContext); return this.getChunk(locale, routeName); })() : this.cachedTranslations; return resolveTranslation(translations, String(key)); } resolveHas(key) { return resolveTranslation(this.cachedTranslations, String(key)) !== null; } getMissingContext(_routeContext) { return { locale: this.currentLocale, routeName: this.currentRouteName }; } warnMissing(key) { const customHandler = this.getCustomMissingHandler?.(); if (customHandler) { customHandler(this.currentLocale, String(key), this.currentRouteName); return; } if (this.missingWarn && process.env.NODE_ENV !== "production" && import.meta.client) { console.warn(`[i18n] Missing key '${key}' in '${this.currentLocale}' for route '${this.currentRouteName}'`); } } clearCache() { super.clearCache(); this.cachedTranslations = {}; this.pendingCleanState = null; triggerRef(this.contextSignal); } } export class NuxtTranslationLoader { constructor(options) { this.options = options; } pendingLoads = /* @__PURE__ */ new Map(); loadFromCacheSync(locale, routeName) { const { i18n } = this.options; if (i18n.hasChunk(locale, routeName)) { return i18n.getChunk(locale, routeName); } const cached = translationStorage.getFromCache(locale, routeName); if (cached) { i18n.setChunk(locale, routeName, cached.data); return cached.data; } return null; } loadAsync(locale, routeName) { const { i18n, loadOptions, setSsrChunk, isDev } = this.options; const cacheKey = i18n.getCacheKey(locale, routeName); const pending = this.pendingLoads.get(cacheKey); if (pending) return pending; const promise = (async () => { try { const result = await translationStorage.load(locale, routeName, loadOptions); const existing = i18n.getChunk(locale, routeName); const mergedChunk = mergeTranslationChunk(existing, result.data, { preserveExisting: true }); i18n.setChunk(locale, routeName, mergedChunk); if (import.meta.server) { setSsrChunk(cacheKey, mergedChunk); } return mergedChunk; } catch (e) { if (isDev) console.error("[i18n] Load error:", e); return {}; } finally { this.pendingLoads.delete(cacheKey); } })(); this.pendingLoads.set(cacheKey, promise); return promise; } async switchContext(locale, routeName) { let data = this.loadFromCacheSync(locale, routeName); if (data === null) { data = await this.loadAsync(locale, routeName); } this.options.i18n.applySwitchContext(locale, routeName, data); } } export function createNuxtI18nPluginApi(deps) { const { i18n, loader, i18nStrategy, i18nConfig, router, getCurrentLocale, getEffectiveLocale, getPluginRouteName, getRouteName, i18nRouteParams, setLocale, isValidLocale, navigateTo, setMissingHandler } = deps; const isDev = process.env.NODE_ENV !== "production"; const isNoPrefix = isNoPrefixStrategy(i18nConfig.strategy); const switchLocaleLogic = (toLocale, i18nParams, to) => { const fromLocale = getCurrentLocale(); const currentRoute = router.currentRoute.value; let resolvedTarget; if (typeof to === "string") { resolvedTarget = router.resolve(i18nStrategy.formatPathForResolve(to, fromLocale, toLocale)); } else { resolvedTarget = to ?? currentRoute; } const switchedRoute = i18nStrategy.switchLocaleRoute(fromLocale, toLocale, resolvedTarget, { i18nRouteParams: i18nParams }); if (typeof switchedRoute === "string" && (switchedRoute.startsWith("http://") || switchedRoute.startsWith("https://"))) { return navigateTo(switchedRoute, { redirectCode: 200, external: true }); } if (isNoPrefix) { ; switchedRoute.force = true; } return router.push(switchedRoute); }; const helper = { async mergeTranslation(locale, routeName, newTranslations, _force = false) { await i18n.mergeTranslationAsync(locale, routeName, newTranslations, (l, r) => loader.loadAsync(l, r)); } }; const switchContext = (locale, routeName) => loader.switchContext(locale, routeName); const provide = { i18nStrategy, getLocale: (route) => getEffectiveLocale(route, (r) => getCurrentLocale(r)), getLocaleName: () => i18nStrategy.getCurrentLocaleName(router.currentRoute.value, getCurrentLocale() ?? null), defaultLocale: () => i18nConfig.defaultLocale, getLocales: () => i18nConfig.locales || [], getRouteName, t: i18n.t.bind(i18n), ts: (key, params, defaultValue, route) => { const value = route ? i18n.t(key, params, defaultValue, route) : i18n.ts(key, params, defaultValue); return value?.toString() ?? defaultValue ?? key; }, _t: (route) => i18n.tForRoute(route), _ts: (route) => i18n.tsForRoute(route), tc: i18n.tc.bind(i18n), tn: i18n.tn.bind(i18n), td: i18n.td.bind(i18n), tdr: i18n.tdr.bind(i18n), has: i18n.has.bind(i18n), mergeTranslations: i18n.mergeTranslations.bind(i18n), switchLocaleRoute: (toLocale) => { const route = router.currentRoute.value; const fromLocale = getCurrentLocale(route); return i18nStrategy.switchLocaleRoute(fromLocale, toLocale, route, { i18nRouteParams: unref(i18nRouteParams.value) }); }, clearCache: () => { translationStorage.clear(); i18n.clearCache(); }, switchLocalePath: (toLocale) => { const route = router.currentRoute.value; const fromLocale = getCurrentLocale(route); const switched = i18nStrategy.switchLocaleRoute(fromLocale, toLocale, route, { i18nRouteParams: unref(i18nRouteParams.value) }); return extractSwitchLocalePath(switched, router); }, switchLocale: async (toLocale) => { if (!isValidLocale(toLocale)) { if (isDev) console.warn(`[i18n] Invalid locale '${toLocale}'`); return; } setLocale(toLocale); if (isNoPrefixStrategy(i18nConfig.strategy) || i18nConfig.hashMode) { const route = router.currentRoute.value; const routeName = getPluginRouteName(route, toLocale); await loader.switchContext(toLocale, routeName); } return switchLocaleLogic(toLocale, unref(i18nRouteParams.value)); }, switchRoute: (route, toLocale) => { return switchLocaleLogic(toLocale ?? getCurrentLocale(), unref(i18nRouteParams.value), route); }, localeRoute(to, locale) { const targetLocale = locale !== void 0 && locale !== "" ? String(locale) : getCurrentLocale(); const result = i18nStrategy.localeRoute(targetLocale, to, router.currentRoute.value); return toRouteLocationResolved(result); }, localePath(to, locale) { const targetLocale = locale !== void 0 && locale !== "" ? String(locale) : getCurrentLocale(); const result = i18nStrategy.localeRoute(targetLocale, to, router.currentRoute.value); return result.fullPath ?? result.path ?? ""; }, setI18nRouteParams: (value) => { i18nRouteParams.value = value; return i18nRouteParams.value; }, loadPageTranslations: i18n.loadPageTranslations.bind(i18n), setMissingHandler }; return { helper, switchContext, provide }; }