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.
273 lines (272 loc) • 11.3 kB
JavaScript
import { useTranslationHelper, interpolate, isNoPrefixStrategy, RouteService, FormatService } from "@i18n-micro/core";
import { useRouter, useCookie, navigateTo, defineNuxtPlugin, useRuntimeConfig, createError } from "#imports";
import { unref } from "vue";
import { useState } from "#app";
import { plural } from "#build/i18n.plural.mjs";
const isDev = process.env.NODE_ENV !== "production";
export default defineNuxtPlugin(async (nuxtApp) => {
const config = useRuntimeConfig();
const i18nConfig = config.public.i18nConfig;
const apiBaseUrl = i18nConfig.apiBaseUrl ?? "_locales";
const apiBaseHost = import.meta.client ? i18nConfig.apiBaseClientHost : i18nConfig.apiBaseServerHost;
const router = useRouter();
const runtimeConfig = useRuntimeConfig();
const generalLocaleCache = useState("i18n-general-cache", () => ({}));
const routeLocaleCache = useState("i18n-route-cache", () => ({}));
const dynamicTranslationsCaches = useState("i18n-dynamic-caches", () => []);
const serverTranslationCache = useState("i18n-server-cache", () => ({}));
const translationCaches = {
generalLocaleCache,
routeLocaleCache,
dynamicTranslationsCaches,
serverTranslationCache
};
const i18nHelper = useTranslationHelper(translationCaches);
let hashLocaleDefault = null;
let noPrefixDefault = null;
let cookieLocaleDefault = null;
let cookieLocaleName = null;
if (!i18nConfig.hashMode) {
cookieLocaleName = i18nConfig.localeCookie || "user-locale";
}
if (i18nConfig.hashMode) {
hashLocaleDefault = await nuxtApp.runWithContext(() => useCookie("hash-locale").value);
}
if (isNoPrefixStrategy(i18nConfig.strategy)) {
if (cookieLocaleName) {
noPrefixDefault = await nuxtApp.runWithContext(() => useCookie(cookieLocaleName).value);
}
}
if (!i18nConfig.hashMode && !isNoPrefixStrategy(i18nConfig.strategy)) {
cookieLocaleDefault = await nuxtApp.runWithContext(() => useCookie(cookieLocaleName).value);
}
const routeService = new RouteService(
i18nConfig,
router,
hashLocaleDefault,
noPrefixDefault,
(to, options) => navigateTo(to, options),
(name, value) => {
nuxtApp.runWithContext(() => {
return useCookie(name).value = value;
});
},
cookieLocaleDefault,
cookieLocaleName
);
const translationService = new FormatService();
const i18nRouteParams = useState("i18n-route-params", () => ({}));
const previousPageInfo = useState("i18n-previous-page", () => null);
const enablePreviousPageFallback = i18nConfig.experimental?.i18nPreviousPageFallback ?? false;
const missingWarn = i18nConfig.missingWarn ?? true;
const customMissingHandler = useState("i18n-missing-handler", () => null);
nuxtApp.hook("page:finish", () => {
if (import.meta.client) {
previousPageInfo.value = null;
}
});
async function loadPageAndGlobalTranslations(to) {
let locale = routeService.getCurrentLocale(to);
if (i18nConfig.hashMode) {
locale = await nuxtApp.runWithContext(() => useCookie("hash-locale", { default: () => locale }).value);
}
if (isNoPrefixStrategy(i18nConfig.strategy) && cookieLocaleName) {
locale = await nuxtApp.runWithContext(() => useCookie(cookieLocaleName, { default: () => locale }).value);
}
const routeName = routeService.getPluginRouteName(to, locale);
if (!routeName) {
return;
}
if (i18nHelper.hasPageTranslation(locale, routeName)) {
if (isDev) {
console.log(`[DEBUG] Cache HIT for '${locale}:${routeName}'. Skipping fetch.`);
}
return;
}
let url = `/${apiBaseUrl}/${routeName}/${locale}/data.json`.replace(/\/{2,}/g, "/");
if (apiBaseHost) {
url = `${apiBaseHost}${url}`;
}
try {
const data = await $fetch(url, {
baseURL: runtimeConfig.app.baseURL,
params: { v: i18nConfig.dateBuild }
});
await i18nHelper.loadPageTranslations(locale, routeName, data ?? {});
} catch (e) {
if (isDev) {
console.error(`[i18n] Failed to load translations for ${routeName}/${locale}`, e);
}
throw createError({ statusCode: 404, statusMessage: "Page Not Found" });
}
}
router.beforeEach(async (to, from, next) => {
if (to.name !== from.name) {
i18nRouteParams.value = {};
}
if (to.path === from.path && !isNoPrefixStrategy(i18nConfig.strategy)) {
if (next) next();
return;
}
if (import.meta.client && enablePreviousPageFallback) {
const fromLocale = routeService.getCurrentLocale(from);
const fromRouteName = routeService.getPluginRouteName(from, fromLocale);
previousPageInfo.value = { locale: fromLocale, routeName: fromRouteName };
}
try {
await loadPageAndGlobalTranslations(to);
} catch (e) {
console.error("[i18n] Error loading translations:", e);
}
if (next) next();
});
await loadPageAndGlobalTranslations(router.currentRoute.value);
const provideData = {
i18n: void 0,
__micro: true,
getLocale: (route) => routeService.getCurrentLocale(route),
getLocaleName: () => routeService.getCurrentName(routeService.getCurrentRoute()),
defaultLocale: () => i18nConfig.defaultLocale,
getLocales: () => i18nConfig.locales || [],
getRouteName: (route, locale) => {
const selectedLocale = locale ?? routeService.getCurrentLocale();
const selectedRoute = route ?? routeService.getCurrentRoute();
return routeService.getRouteName(selectedRoute, selectedLocale);
},
t: (key, params, defaultValue, route) => {
if (!key) return "";
route = route ?? routeService.getCurrentRoute();
const locale = routeService.getCurrentLocale();
const routeName = routeService.getPluginRouteName(route, locale);
let value = i18nHelper.getTranslation(locale, routeName, key);
if (!value && previousPageInfo.value && enablePreviousPageFallback) {
const prev = previousPageInfo.value;
const prevValue = i18nHelper.getTranslation(prev.locale, prev.routeName, key);
if (prevValue) {
value = prevValue;
console.log(`Using fallback translation from previous route: ${prev.routeName} -> ${key}`);
}
}
if (!value) {
if (customMissingHandler.value) {
customMissingHandler.value(locale, key, routeName);
} else if (missingWarn && isDev && import.meta.client) {
console.warn(`Not found '${key}' key in '${locale}' locale messages for route '${routeName}'.`);
}
value = defaultValue === void 0 ? key : defaultValue;
}
return typeof value === "string" && params ? interpolate(value, params) : value;
},
ts: (key, params, defaultValue, route) => {
const value = provideData.t(key, params, defaultValue, route);
return value?.toString() ?? defaultValue ?? key;
},
_t: (route) => {
return (key, params, defaultValue) => {
return provideData.t(key, params, defaultValue, route);
};
},
_ts: (route) => {
return (key, params, defaultValue) => {
return provideData.ts(key, params, defaultValue, route);
};
},
tc: (key, params, defaultValue) => {
const currentLocale = routeService.getCurrentLocale();
const { count, ..._params } = typeof params === "number" ? { count: params } : params;
if (count === void 0) return defaultValue ?? key;
return plural(key, Number.parseInt(count.toString()), _params, currentLocale, provideData.t) ?? defaultValue ?? key;
},
tn: (value, options) => {
const currentLocale = routeService.getCurrentLocale();
return translationService.formatNumber(value, currentLocale, options);
},
td: (value, options) => {
const currentLocale = routeService.getCurrentLocale();
return translationService.formatDate(value, currentLocale, options);
},
tdr: (value, options) => {
const currentLocale = routeService.getCurrentLocale();
return translationService.formatRelativeTime(value, currentLocale, options);
},
has: (key, route) => {
route = route ?? routeService.getCurrentRoute();
const locale = routeService.getCurrentLocale();
const routeName = routeService.getPluginRouteName(route, locale);
return !!i18nHelper.getTranslation(locale, routeName, key);
},
mergeTranslations: (newTranslations) => {
const route = routeService.getCurrentRoute();
const locale = routeService.getCurrentLocale(route);
const routeName = routeService.getPluginRouteName(route, locale);
i18nHelper.mergeTranslation(locale, routeName, newTranslations);
},
mergeGlobalTranslations: (newTranslations) => {
const locale = routeService.getCurrentLocale();
i18nHelper.mergeGlobalTranslation(locale, newTranslations, true);
},
switchLocaleRoute: (toLocale) => {
const route = routeService.getCurrentRoute();
const fromLocale = routeService.getCurrentLocale(route);
return routeService.switchLocaleRoute(fromLocale, toLocale, route, unref(i18nRouteParams.value));
},
clearCache: () => {
i18nHelper.clearCache();
},
switchLocalePath: (toLocale) => {
const route = routeService.getCurrentRoute();
const fromLocale = routeService.getCurrentLocale(route);
const localeRoute = routeService.switchLocaleRoute(fromLocale, toLocale, route, unref(i18nRouteParams.value));
if (typeof localeRoute === "string") {
return localeRoute;
}
if ("fullPath" in localeRoute && localeRoute.fullPath) {
return localeRoute.fullPath;
}
if ("name" in localeRoute && localeRoute.name) {
if (router.hasRoute(localeRoute.name)) {
return router.resolve(localeRoute).fullPath;
}
}
return "";
},
switchLocale: (toLocale) => {
return routeService.switchLocaleLogic(toLocale, unref(i18nRouteParams.value));
},
switchRoute: (route, toLocale) => {
return routeService.switchLocaleLogic(toLocale ?? routeService.getCurrentLocale(), unref(i18nRouteParams.value), route);
},
localeRoute: (to, locale) => {
return routeService.resolveLocalizedRoute(to, locale);
},
localePath: (to, locale) => {
const localeRoute = routeService.resolveLocalizedRoute(to, locale);
if (typeof localeRoute === "string") {
return localeRoute;
}
if ("fullPath" in localeRoute) {
return localeRoute.fullPath;
}
return "";
},
setI18nRouteParams: (value) => {
i18nRouteParams.value = value;
return i18nRouteParams.value;
},
loadPageTranslations: async (locale, routeName, translations) => {
await i18nHelper.loadPageTranslations(locale, routeName, translations);
},
setMissingHandler: (handler) => {
customMissingHandler.value = handler;
},
helper: i18nHelper
// Оставляем helper, он может быть полезен для продвинутых пользователей
};
const $provideData = Object.fromEntries(
Object.entries(provideData).map(([key, value]) => [`$${key}`, value])
);
provideData.i18n = { ...provideData, ...$provideData };
return {
provide: provideData
};
});