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.

273 lines (272 loc) 11.3 kB
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 }; });