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
JavaScript
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 };
}