nuxt-i18n-micro 3.30.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -2
- package/dist/client/200.html +1 -1
- package/dist/client/404.html +1 -1
- package/dist/client/_nuxt/BVna_KKx.js +3 -0
- package/dist/client/_nuxt/Bw81RhAv.js +1 -0
- package/dist/client/_nuxt/CPz_jhsD.js +1 -0
- package/dist/client/_nuxt/CmvObVGq.js +1 -0
- package/dist/client/_nuxt/DKUOBR6F.js +5 -0
- package/dist/client/_nuxt/builds/latest.json +1 -1
- package/dist/client/_nuxt/builds/meta/f0c8d4bf-40d2-4915-8b4a-ebe46bde61c8.json +1 -0
- package/dist/client/_nuxt/{entry.BM-mexAv.css → entry.PSWtHhay.css} +1 -1
- package/dist/client/_nuxt/error-404.xs3fxhVJ.css +1 -0
- package/dist/client/_nuxt/error-500.C2S-4iLg.css +1 -0
- package/dist/client/index.html +1 -1
- package/dist/module.d.mts +14 -5
- package/dist/module.json +4 -1
- package/dist/module.mjs +850 -475
- package/dist/runtime/components/i18n-group.d.vue.ts +17 -9
- package/dist/runtime/components/i18n-group.vue +2 -4
- package/dist/runtime/components/i18n-group.vue.d.ts +17 -9
- package/dist/runtime/components/i18n-link.d.vue.ts +9 -6
- package/dist/runtime/components/i18n-link.vue +7 -28
- package/dist/runtime/components/i18n-link.vue.d.ts +9 -6
- package/dist/runtime/components/i18n-switcher.d.vue.ts +128 -54
- package/dist/runtime/components/i18n-switcher.vue +56 -156
- package/dist/runtime/components/i18n-switcher.vue.d.ts +128 -54
- package/dist/runtime/components/i18n-t.d.vue.ts +59 -116
- package/dist/runtime/components/i18n-t.vue +24 -71
- package/dist/runtime/components/i18n-t.vue.d.ts +59 -116
- package/dist/runtime/composables/useI18n.js +2 -38
- package/dist/runtime/composables/useI18nHead.js +21 -6
- package/dist/runtime/composables/useI18nLocale.d.ts +4 -1
- package/dist/runtime/composables/useI18nLocale.js +20 -22
- package/dist/runtime/composables/useLocaleHead.d.ts +26 -31
- package/dist/runtime/composables/useLocaleHead.js +47 -167
- package/dist/runtime/devtools/bridge.d.ts +2 -2
- package/dist/runtime/devtools/vue-devtools.d.ts +2 -2
- package/dist/runtime/devtools/vue-devtools.js +5 -5
- package/dist/runtime/middleware/i18n-redirect.global.d.ts +3 -2
- package/dist/runtime/middleware/i18n-redirect.global.js +30 -41
- package/dist/runtime/plugins/01.plugin.d.ts +156 -34
- package/dist/runtime/plugins/01.plugin.js +184 -138
- package/dist/runtime/plugins/02.meta.js +36 -43
- package/dist/runtime/plugins/04.vue-devtools.js +1 -1
- package/dist/runtime/plugins/06.redirect.d.ts +3 -3
- package/dist/runtime/plugins/06.redirect.js +65 -91
- package/dist/runtime/server/{utils → internal}/dev-live-translations.js +4 -3
- package/dist/runtime/server/internal/server-translation-cache.d.ts +9 -0
- package/dist/runtime/server/internal/server-translation-cache.js +10 -0
- package/dist/runtime/server/internal/shared-cache.d.ts +24 -0
- package/dist/runtime/server/internal/shared-cache.js +71 -0
- package/dist/runtime/server/internal/translation-watcher.d.ts +79 -0
- package/dist/runtime/server/internal/translation-watcher.js +105 -0
- package/dist/runtime/server/middleware/i18n.global.js +24 -37
- package/dist/runtime/server/payload-source.assets.d.ts +1 -0
- package/dist/runtime/server/payload-source.assets.js +3 -8
- package/dist/runtime/server/payload-source.fs.d.ts +1 -0
- package/dist/runtime/server/payload-source.fs.js +18 -7
- package/dist/runtime/server/plugins/failed-read.d.ts +7 -0
- package/dist/runtime/server/plugins/failed-read.js +23 -0
- package/dist/runtime/server/plugins/payload-cache.d.ts +6 -0
- package/dist/runtime/server/plugins/payload-cache.js +16 -0
- package/dist/runtime/server/plugins/watcher.dev.js +15 -79
- package/dist/runtime/server/routes/i18n.js +13 -14
- package/dist/runtime/server/utils/locale-detector.d.ts +32 -2
- package/dist/runtime/server/utils/locale-detector.js +48 -27
- package/dist/runtime/server/utils/locale-server-middleware.js +14 -33
- package/dist/runtime/server/utils/payload-cache.d.ts +4 -0
- package/dist/runtime/server/utils/payload-cache.js +13 -0
- package/dist/runtime/server/utils/server-loader.d.ts +17 -6
- package/dist/runtime/server/utils/server-loader.js +105 -51
- package/dist/runtime/server/utils/translation-server-middleware.js +18 -35
- package/dist/runtime/utils/cache-keys.d.ts +6 -0
- package/dist/runtime/utils/cache-keys.js +2 -0
- package/dist/runtime/utils/i18n-api.d.ts +185 -0
- package/dist/runtime/utils/i18n-api.js +196 -0
- package/dist/runtime/utils/i18n-config.d.ts +18 -0
- package/dist/runtime/utils/i18n-config.js +47 -0
- package/dist/runtime/utils/i18n-navigation.d.ts +26 -0
- package/dist/runtime/utils/i18n-navigation.js +96 -0
- package/dist/runtime/utils/nuxt-i18n-server.d.ts +26 -0
- package/dist/runtime/utils/nuxt-i18n-server.js +83 -0
- package/dist/runtime/utils/nuxt-i18n.d.ts +81 -96
- package/dist/runtime/utils/nuxt-i18n.js +180 -252
- package/dist/runtime/utils/page-head.d.ts +12 -0
- package/dist/runtime/utils/page-head.js +6 -0
- package/dist/runtime/utils/request-url.d.ts +6 -0
- package/dist/runtime/utils/request-url.js +11 -0
- package/dist/runtime/utils/storage.d.ts +31 -19
- package/dist/runtime/utils/storage.js +100 -49
- package/dist/runtime/utils/translation-entry.d.ts +6 -0
- package/dist/runtime/utils/translation-entry.js +16 -0
- package/dist/types.d.mts +4 -6
- package/internals.d.mts +9 -2
- package/package.json +78 -79
- package/dist/client/_nuxt/BOxWElQP.js +0 -21
- package/dist/client/_nuxt/CUWB4CwN.js +0 -3
- package/dist/client/_nuxt/CmUbe-uv.js +0 -1
- package/dist/client/_nuxt/DAzShkZF.js +0 -1
- package/dist/client/_nuxt/MokPP5zi.js +0 -1
- package/dist/client/_nuxt/builds/meta/60974e48-d00e-4f9b-9467-9d4b322519da.json +0 -1
- package/dist/client/_nuxt/error-404.D9Svp6Hk.css +0 -1
- package/dist/client/_nuxt/error-500.B-mjI9_9.css +0 -1
- package/dist/runtime/plugins/03.define.d.ts +0 -7
- package/dist/runtime/plugins/03.define.js +0 -42
- package/dist/runtime/plugins/05.hooks.d.ts +0 -2
- package/dist/runtime/plugins/05.hooks.js +0 -47
- package/dist/runtime/server/utils/server-translation-cache.d.ts +0 -11
- package/dist/runtime/server/utils/server-translation-cache.js +0 -20
- /package/dist/runtime/server/{utils → internal}/dev-live-translations.d.ts +0 -0
|
@@ -1,10 +1,8 @@
|
|
|
1
|
+
import { trimTrailingSlashes } from "@i18n-micro/utils/app-path";
|
|
1
2
|
import { mergeI18nHead } from "@i18n-micro/utils/merge-i18n-head";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { getI18nConfig } from "#build/i18n.strategy.mjs";
|
|
6
|
-
import { defineNuxtPlugin, useHead, useRequestURL, useRoute, useState } from "#imports";
|
|
7
|
-
import { useI18nHead } from "../composables/useI18nHead.js";
|
|
3
|
+
import { joinURL } from "ufo";
|
|
4
|
+
import { defineNuxtPlugin, useHead, useRequestURL, useRoute, useRuntimeConfig } from "#imports";
|
|
5
|
+
import { usePageHead } from "../utils/page-head.js";
|
|
8
6
|
import { useLocaleHead } from "../composables/useLocaleHead.js";
|
|
9
7
|
function readSiteConfigUrl(nuxtApp) {
|
|
10
8
|
const stack = nuxtApp.$nuxtSiteConfig;
|
|
@@ -16,57 +14,52 @@ function readSiteConfigUrl(nuxtApp) {
|
|
|
16
14
|
return void 0;
|
|
17
15
|
}
|
|
18
16
|
}
|
|
17
|
+
function requestOrigin(i18nConfig) {
|
|
18
|
+
const xForwardedProto = i18nConfig.metaTrustForwardedProto !== false;
|
|
19
|
+
try {
|
|
20
|
+
return useRequestURL({ xForwardedHost: i18nConfig.metaTrustForwardedHost !== false, xForwardedProto }).origin;
|
|
21
|
+
} catch {
|
|
22
|
+
try {
|
|
23
|
+
return useRequestURL({ xForwardedProto }).origin;
|
|
24
|
+
} catch {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
19
29
|
function resolveSeoBaseUrl(i18nConfig, siteUrl) {
|
|
20
30
|
const configured = i18nConfig.metaBaseUrl || siteUrl;
|
|
21
|
-
if (configured) return configured
|
|
22
|
-
|
|
23
|
-
xForwardedHost: i18nConfig.metaTrustForwardedHost !== false,
|
|
24
|
-
xForwardedProto: i18nConfig.metaTrustForwardedProto !== false
|
|
25
|
-
});
|
|
26
|
-
return url.origin.replace(/\/+$/, "");
|
|
31
|
+
if (configured) return trimTrailingSlashes(configured);
|
|
32
|
+
return trimTrailingSlashes(joinURL(requestOrigin(i18nConfig), useRuntimeConfig().app.baseURL));
|
|
27
33
|
}
|
|
34
|
+
let warnedCachedRender = false;
|
|
28
35
|
export default defineNuxtPlugin((nuxtApp) => {
|
|
29
36
|
const route = useRoute();
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const { pageHead } = useI18nHead();
|
|
33
|
-
const getRuntimeConfig = nuxtApp.$getI18nConfig;
|
|
34
|
-
const i18nConfig = resolveI18nConfigWithRuntimeOverrides(
|
|
35
|
-
typeof getRuntimeConfig === "function" ? getRuntimeConfig() : getI18nConfig()
|
|
36
|
-
);
|
|
37
|
+
const pageHead = usePageHead();
|
|
38
|
+
const i18nConfig = nuxtApp.$getI18nConfig();
|
|
37
39
|
const siteUrl = readSiteConfigUrl(nuxtApp);
|
|
38
40
|
const baseUrl = resolveSeoBaseUrl(i18nConfig, siteUrl);
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const event = nuxtApp.ssrContext?.event;
|
|
42
|
+
const hostless = event?.context?.cache && !event.node?.req?.headers?.host;
|
|
43
|
+
if (import.meta.server && !warnedCachedRender && hostless && !i18nConfig.metaBaseUrl && !siteUrl) {
|
|
44
|
+
warnedCachedRender = true;
|
|
45
|
+
console.warn(
|
|
46
|
+
"[i18n] A route cached by `swr` or `cache` renders without the request host, so canonical and hreflang URLs point to http://localhost. Set `i18n.metaBaseUrl` or `site.url`."
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const { build } = useLocaleHead({
|
|
43
50
|
baseUrl,
|
|
44
51
|
autoUpdate: false
|
|
45
52
|
});
|
|
46
|
-
|
|
53
|
+
useHead(() => {
|
|
47
54
|
const { $getLocales, $getLocale } = nuxtApp;
|
|
48
|
-
const allLocales = $getLocales
|
|
49
|
-
const locale = $getLocale
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return mergeI18nHead(metaObject.value, pageHead.value, {
|
|
55
|
+
const allLocales = $getLocales();
|
|
56
|
+
const locale = $getLocale() || i18nConfig.defaultLocale;
|
|
57
|
+
const head = build();
|
|
58
|
+
if (!pageHead.value || nuxtApp.$i18nStrategy.isMetaDisabled(route, locale)) return head;
|
|
59
|
+
return mergeI18nHead(head, pageHead.value, {
|
|
54
60
|
identifierAttribute: "id",
|
|
55
61
|
locales: allLocales,
|
|
56
62
|
currentLocale: locale
|
|
57
63
|
});
|
|
58
64
|
});
|
|
59
|
-
useHead(mergedHead);
|
|
60
|
-
const refreshMeta = () => updateMeta();
|
|
61
|
-
if (import.meta.server) {
|
|
62
|
-
nuxtApp.hook("app:rendered", refreshMeta);
|
|
63
|
-
} else {
|
|
64
|
-
refreshMeta();
|
|
65
|
-
nuxtApp.hook("page:finish", refreshMeta);
|
|
66
|
-
}
|
|
67
|
-
watch(() => i18nRouteParams.value, refreshMeta, { deep: true, flush: "post" });
|
|
68
|
-
watch(() => [route.fullPath, route.name, route.matched.length, localeState.value], refreshMeta, {
|
|
69
|
-
flush: "post"
|
|
70
|
-
});
|
|
71
|
-
watch(pageHead, refreshMeta, { deep: true, flush: "post" });
|
|
72
65
|
});
|
|
@@ -25,7 +25,7 @@ export default defineNuxtPlugin({
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
const notifyCurrentLoad = () => {
|
|
28
|
-
const locale = bridge.i18n.
|
|
28
|
+
const locale = bridge.i18n.getLocale();
|
|
29
29
|
const routeName = bridge.i18n.getCurrentRouteName() || "index";
|
|
30
30
|
notifier.notifyLoad(locale, routeName, countLoadedTranslationKeys(bridge.i18n, locale, routeName));
|
|
31
31
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Client SPA redirects
|
|
2
|
+
* Server-side locale redirects and 404s for i18n routes. Every decision is the strategy's
|
|
3
|
+
* (`shouldReturn404`, `getClientRedirect`); this plugin only reads the request (cookie,
|
|
4
|
+
* Accept-Language) and writes the response. Client SPA redirects: the i18n-redirect middleware.
|
|
5
5
|
*/
|
|
6
6
|
declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
|
|
7
7
|
export default _default;
|
|
@@ -1,103 +1,77 @@
|
|
|
1
1
|
import { getEnabledLocaleCodes } from "@i18n-micro/utils/active-locales";
|
|
2
2
|
import { isInternalPath, withoutAppBaseURL } from "@i18n-micro/utils/app-path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { getLocaleCookieName, getLocaleCookieOptions, getPreferenceCookieName } from "@i18n-micro/utils/cookie";
|
|
4
|
+
import { preferenceCounts, redirectLocale } from "@i18n-micro/utils/resolve-locale";
|
|
5
|
+
import { appendResponseHeader, getCookie, getHeader, setCookie } from "h3";
|
|
6
|
+
import { requestURL } from "../utils/request-url.js";
|
|
7
|
+
import { applyTrailingSlash } from "../utils/trailing-slash.js";
|
|
8
|
+
import { createError, defineNuxtPlugin, navigateTo, useRequestEvent, useRouter, useRuntimeConfig, useState } from "#imports";
|
|
8
9
|
const DEBUG = process.env.NUXT_I18N_DEBUG_REDIRECT === "1";
|
|
9
|
-
function resolveI18nStrategy(nuxtApp) {
|
|
10
|
-
if (nuxtApp.$i18nStrategy) {
|
|
11
|
-
return nuxtApp.$i18nStrategy;
|
|
12
|
-
}
|
|
13
|
-
throw new Error("[nuxt-i18n-micro] i18n redirect plugin requires the main i18n plugin ($i18nStrategy).");
|
|
14
|
-
}
|
|
15
10
|
export default defineNuxtPlugin({
|
|
16
11
|
name: "i18n-redirect",
|
|
17
12
|
dependsOn: ["i18n-plugin-loader"],
|
|
13
|
+
// annotated: the plugin types are inferred from every plugin's setup, this one included
|
|
18
14
|
setup(nuxtApp) {
|
|
19
|
-
const i18nStrategy =
|
|
15
|
+
const i18nStrategy = nuxtApp.$i18nStrategy;
|
|
20
16
|
const getRuntimeConfig = nuxtApp.$getI18nConfig;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
17
|
+
const event = useRequestEvent();
|
|
18
|
+
if (!event) return;
|
|
24
19
|
const i18nConfig = getRuntimeConfig();
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
const firstSegment = pathSegments[0];
|
|
41
|
-
const customRegex = i18nConfig.customRegexMatcher;
|
|
42
|
-
if (firstSegment && customRegex && !validLocales.includes(firstSegment)) {
|
|
43
|
-
const source = typeof customRegex === "string" ? customRegex : customRegex.source;
|
|
44
|
-
const regex = new RegExp(`^${source}$`);
|
|
45
|
-
if (regex.test(firstSegment)) {
|
|
46
|
-
if (DEBUG) console.error("[i18n-redirect] 404: unknown locale", firstSegment);
|
|
47
|
-
throw createError({ statusCode: 404, statusMessage: "Page Not Found" });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
const errorMessage = i18nStrategy.shouldReturn404(path);
|
|
51
|
-
if (errorMessage) {
|
|
52
|
-
if (DEBUG) console.error("[i18n-redirect] 404:", errorMessage, path);
|
|
53
|
-
throw createError({ statusCode: 404, statusMessage: "Page Not Found" });
|
|
54
|
-
}
|
|
55
|
-
const hasLocalePrefix = Boolean(firstSegment && validLocales.includes(firstSegment));
|
|
56
|
-
const shouldDeferCookieSync = i18nConfig.redirects !== false && autoDetectPath === "*";
|
|
57
|
-
if (hasLocalePrefix && cookieName && !shouldDeferCookieSync) {
|
|
58
|
-
const currentLocale = firstSegment;
|
|
59
|
-
const { watch: _w, ...cookieOpts } = getLocaleCookieOptions();
|
|
60
|
-
setCookie(event, cookieName, currentLocale, cookieOpts);
|
|
61
|
-
}
|
|
62
|
-
if (i18nConfig.redirects !== false) {
|
|
63
|
-
const allowPreferenceRedirect = shouldAttemptLocaleRedirect(path, { autoDetectPath, hasLocalePrefix });
|
|
64
|
-
const localeState = autoDetectPath !== "*" ? useState("i18n-locale", () => null) : null;
|
|
65
|
-
let preferredLocale = resolvePreferredLocale({
|
|
66
|
-
defaultLocale,
|
|
67
|
-
validLocales,
|
|
68
|
-
autoDetectLanguage: i18nConfig.autoDetectLanguage,
|
|
69
|
-
stateLocale: localeState?.value ?? null,
|
|
70
|
-
cookieLocale: cookieName ? getCookie(event, cookieName) : null,
|
|
71
|
-
acceptLanguageHeader: getHeader(event, "accept-language"),
|
|
72
|
-
ignoreStateLocale: autoDetectPath === "*"
|
|
73
|
-
});
|
|
74
|
-
if (!hasLocalePrefix && !allowPreferenceRedirect) {
|
|
75
|
-
preferredLocale = defaultLocale;
|
|
76
|
-
}
|
|
77
|
-
if (autoDetectPath === "*" && !hasLocalePrefix) {
|
|
78
|
-
preferredLocale = defaultLocale;
|
|
79
|
-
}
|
|
80
|
-
if (autoDetectPath === "*" && hasLocalePrefix && firstSegment !== preferredLocale) {
|
|
81
|
-
const rest = pathSegments.slice(1).join("/");
|
|
82
|
-
let targetPath;
|
|
83
|
-
if (preferredLocale === defaultLocale && i18nConfig.strategy === "prefix_except_default") {
|
|
84
|
-
targetPath = rest ? `/${rest}` : "/";
|
|
85
|
-
} else {
|
|
86
|
-
targetPath = rest ? `/${preferredLocale}/${rest}` : `/${preferredLocale}`;
|
|
87
|
-
}
|
|
88
|
-
if (cookieName) {
|
|
89
|
-
const { watch: _w2, ...cookieOpts2 } = getLocaleCookieOptions();
|
|
90
|
-
setCookie(event, cookieName, preferredLocale, cookieOpts2);
|
|
91
|
-
}
|
|
92
|
-
if (DEBUG) console.error("[i18n-redirect] REDIRECT autoDetectPath *", { path, targetPath, preferredLocale });
|
|
93
|
-
return performRedirect(targetPath + (url.search || "") + (url.hash || ""));
|
|
94
|
-
}
|
|
95
|
-
const redirectPath = i18nStrategy.getClientRedirect(path, preferredLocale);
|
|
96
|
-
if (redirectPath) {
|
|
97
|
-
if (DEBUG) console.error("[i18n-redirect] REDIRECT", { path, redirectPath, preferredLocale });
|
|
98
|
-
return performRedirect(redirectPath + (url.search || "") + (url.hash || ""));
|
|
99
|
-
}
|
|
100
|
-
}
|
|
20
|
+
const url = requestURL(event);
|
|
21
|
+
const path = withoutAppBaseURL(url.pathname, useRuntimeConfig().app.baseURL);
|
|
22
|
+
const alwaysPrefixed = i18nConfig.strategy === "prefix";
|
|
23
|
+
const preferenceCookie = getPreferenceCookieName(i18nConfig);
|
|
24
|
+
const varyOnVisitor = () => {
|
|
25
|
+
const vary = [preferenceCookie && "Cookie", i18nConfig.autoDetectLanguage && "Accept-Language"].filter(Boolean);
|
|
26
|
+
if (vary.length) appendResponseHeader(event, "Vary", vary.join(", "));
|
|
27
|
+
};
|
|
28
|
+
const urlless = i18nConfig.strategy === "no_prefix" || i18nConfig.hashMode && !alwaysPrefixed;
|
|
29
|
+
if (urlless) varyOnVisitor();
|
|
30
|
+
if (isInternalPath(path, i18nConfig.excludePatterns, i18nConfig.apiBaseUrl)) return;
|
|
31
|
+
const notFound = i18nStrategy.shouldReturn404(path);
|
|
32
|
+
if (notFound) {
|
|
33
|
+
if (DEBUG) console.error("[i18n-redirect] 404:", notFound, path);
|
|
34
|
+
throw createError({ statusCode: 404, statusMessage: "Page Not Found" });
|
|
101
35
|
}
|
|
36
|
+
const defaultLocale = i18nConfig.defaultLocale;
|
|
37
|
+
const autoDetectPath = i18nConfig.autoDetectPath;
|
|
38
|
+
const anyPath = autoDetectPath === "*";
|
|
39
|
+
const redirects = i18nConfig.redirects !== false;
|
|
40
|
+
const cookieName = getLocaleCookieName(i18nConfig);
|
|
41
|
+
const urlLocale = i18nStrategy.getLocaleFromPath(path);
|
|
42
|
+
const writeCookie = (locale) => {
|
|
43
|
+
if (!cookieName) return;
|
|
44
|
+
const { watch: _watch, decode: _decode, ...options } = getLocaleCookieOptions();
|
|
45
|
+
setCookie(event, cookieName, locale, options);
|
|
46
|
+
};
|
|
47
|
+
const redirectable = redirects && !urlless && !i18nConfig.hashMode && (urlLocale || i18nConfig.strategy !== "prefix_and_default");
|
|
48
|
+
if (redirectable && preferenceCounts({ path, urlLocale, autoDetectPath, alwaysPrefixed })) varyOnVisitor();
|
|
49
|
+
const router = useRouter();
|
|
50
|
+
const served = i18nConfig.strategy !== "no_prefix" && (router.getRoutes().length === 0 || router.currentRoute.value.matched.length > 0);
|
|
51
|
+
if (urlLocale && served && !(redirects && anyPath)) writeCookie(urlLocale);
|
|
52
|
+
if (i18nConfig.hashMode && alwaysPrefixed)
|
|
53
|
+
return navigateTo(`/#${applyTrailingSlash(path, i18nConfig.trailingSlash)}${url.search}`, { redirectCode: 302 });
|
|
54
|
+
if (!redirects || i18nConfig.hashMode) return;
|
|
55
|
+
const preferred = redirectLocale({
|
|
56
|
+
path,
|
|
57
|
+
urlLocale,
|
|
58
|
+
autoDetectPath,
|
|
59
|
+
alwaysPrefixed,
|
|
60
|
+
defaultLocale,
|
|
61
|
+
validLocales: getEnabledLocaleCodes(i18nConfig.locales),
|
|
62
|
+
autoDetectLanguage: i18nConfig.autoDetectLanguage,
|
|
63
|
+
cookie: preferenceCookie ? getCookie(event, preferenceCookie) : null,
|
|
64
|
+
acceptLanguage: getHeader(event, "accept-language"),
|
|
65
|
+
// a state differing from what the URL resolved to was set by the app (a plugin): a choice
|
|
66
|
+
state: anyPath ? null : chosenState(useState("i18n-locale", () => null).value, event.context.i18n?.locale)
|
|
67
|
+
});
|
|
68
|
+
const target = i18nStrategy.getClientRedirect(path, preferred, { switchPrefixed: anyPath });
|
|
69
|
+
if (!target) return;
|
|
70
|
+
if (anyPath && urlLocale) writeCookie(preferred);
|
|
71
|
+
if (DEBUG) console.error("[i18n-redirect] REDIRECT", { path, target, preferred });
|
|
72
|
+
return navigateTo(applyTrailingSlash(target, i18nConfig.trailingSlash) + url.search + url.hash, { redirectCode: 302 });
|
|
102
73
|
}
|
|
103
74
|
});
|
|
75
|
+
function chosenState(state, urlLocale) {
|
|
76
|
+
return state && state !== urlLocale ? state : null;
|
|
77
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { createTranslationDirReader, mergeDevTranslations, resolveLiveTranslationDirs } from "
|
|
1
|
+
import { createTranslationDirReader, mergeDevTranslations, resolveLiveTranslationDirs } from "./translation-watcher.js";
|
|
2
2
|
import { toTranslations } from "@i18n-micro/utils/normalize";
|
|
3
3
|
import { getI18nPrivateConfig } from "#i18n-internal/config";
|
|
4
4
|
export async function loadLiveDevTranslations(locale, pageName, config) {
|
|
5
5
|
const privateConfig = getI18nPrivateConfig();
|
|
6
6
|
const { translationRoots, additionalRoots } = resolveLiveTranslationDirs({
|
|
7
|
-
rootDirs: privateConfig.rootDirs
|
|
7
|
+
rootDirs: privateConfig.rootDirs,
|
|
8
8
|
translationDir: privateConfig.translationDir,
|
|
9
9
|
additionalTranslationDirs: privateConfig.additionalTranslationDirs
|
|
10
10
|
});
|
|
@@ -12,7 +12,8 @@ export async function loadLiveDevTranslations(locale, pageName, config) {
|
|
|
12
12
|
const merged = await mergeDevTranslations({
|
|
13
13
|
locale,
|
|
14
14
|
pageName,
|
|
15
|
-
|
|
15
|
+
// a fallback may live on another domain, as in production
|
|
16
|
+
locales: config.linkLocales ?? config.locales,
|
|
16
17
|
fallbackLocale: config.fallbackLocale,
|
|
17
18
|
disablePageLocales: config.disablePageLocales,
|
|
18
19
|
readLocaleFile
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Translations } from '@i18n-micro/types';
|
|
2
|
+
import { CacheControl } from '@i18n-micro/utils/cache-control';
|
|
3
|
+
export type ServerTranslationCacheEntry = {
|
|
4
|
+
data: Translations;
|
|
5
|
+
json: string;
|
|
6
|
+
/** Of a root read from a file: what a delta names. */
|
|
7
|
+
mark?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function getOrCreateServerTranslationCache(): CacheControl<ServerTranslationCacheEntry>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DEFAULT_SERVER_CACHE_SIZE } from "../../utils/cache-keys.js";
|
|
2
|
+
import { CacheControl } from "@i18n-micro/utils/cache-control";
|
|
3
|
+
import { getI18nConfig } from "#i18n-internal/strategy";
|
|
4
|
+
let cache;
|
|
5
|
+
export function getOrCreateServerTranslationCache() {
|
|
6
|
+
if (cache) return cache;
|
|
7
|
+
const { cacheMaxSize, cacheTtl } = getI18nConfig();
|
|
8
|
+
cache = new CacheControl({ maxSize: cacheMaxSize || DEFAULT_SERVER_CACHE_SIZE, ttl: cacheTtl });
|
|
9
|
+
return cache;
|
|
10
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional level between the process LRU and the dictionary source: a Nitro storage mount several
|
|
3
|
+
* instances share (`cacheStorage`), so one of them reads the source and the others read its copy.
|
|
4
|
+
*
|
|
5
|
+
* Only ever an optimisation: every failure is a miss, and the source answers. The process LRU stays
|
|
6
|
+
* in front because `$t` is synchronous and needs the tree in memory. No lock: two instances that
|
|
7
|
+
* miss at once both read the source and write the same value.
|
|
8
|
+
*/
|
|
9
|
+
import type { ResolvedI18nConfig } from '@i18n-micro/types';
|
|
10
|
+
import type { ServerTranslationCacheEntry } from './server-translation-cache.js';
|
|
11
|
+
/**
|
|
12
|
+
* The chunk's key, or null when it must not touch the layer. `cacheStorageBuild` keeps two builds
|
|
13
|
+
* apart in a rolling deploy. `dateBuild` alone would not: it hashes only the translation files, and
|
|
14
|
+
* `0`/`''` turns it off. A runtime `fallbackLocale` changes what `source` and remote chunks hold, and
|
|
15
|
+
* a runtime host is another backend: one artifact can run against several.
|
|
16
|
+
*/
|
|
17
|
+
export declare function sharedChunkKey(locale: string, page: string, config: ResolvedI18nConfig, host?: string): string | null;
|
|
18
|
+
export declare function readSharedChunk(key: string): Promise<ServerTranslationCacheEntry | null>;
|
|
19
|
+
/**
|
|
20
|
+
* A string reaches the driver as is: no second serialization. Awaited: an edge runtime drops what outlives
|
|
21
|
+
* the response. Only while the store is trusted: a write must not wait out a store whose reads just failed.
|
|
22
|
+
* A failed write closes nothing: a read-only replica or a KV write limit still serves reads.
|
|
23
|
+
*/
|
|
24
|
+
export declare function writeSharedChunk(key: string, json: string, config: ResolvedI18nConfig): Promise<void>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useStorage } from "nitropack/runtime";
|
|
2
|
+
import { getI18nPrivateConfig } from "#i18n-internal/config";
|
|
3
|
+
import { toEntry } from "../../utils/translation-entry.js";
|
|
4
|
+
const TRUSTED_TIMEOUT_MS = 1e3;
|
|
5
|
+
const PROBATION_TIMEOUT_MS = 250;
|
|
6
|
+
const FAILURES_TO_CLOSE = 3;
|
|
7
|
+
const COOLDOWN_MS = 5e3;
|
|
8
|
+
const WARN_EVERY_MS = 6e4;
|
|
9
|
+
const DEFAULT_TTL_SECONDS = 86400;
|
|
10
|
+
const MAX_KEY_LENGTH = 400;
|
|
11
|
+
const MAX_PAGE_LENGTH = 200;
|
|
12
|
+
let shared;
|
|
13
|
+
let failures = 0;
|
|
14
|
+
let closedUntil = 0;
|
|
15
|
+
let warnedAt = Number.NEGATIVE_INFINITY;
|
|
16
|
+
function sharedStorage() {
|
|
17
|
+
if (shared !== void 0) return shared;
|
|
18
|
+
const name = getI18nPrivateConfig().cacheStorage;
|
|
19
|
+
if (import.meta.dev || import.meta.prerender || !name) return shared = null;
|
|
20
|
+
if (useStorage().getMount(name).base) return shared = useStorage(name);
|
|
21
|
+
console.warn(`[nuxt-i18n-micro] cacheStorage: no Nitro storage is mounted as "${name}" (nitro.storage). Working without the shared cache.`);
|
|
22
|
+
return shared = null;
|
|
23
|
+
}
|
|
24
|
+
function warn(error) {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
if (now - warnedAt < WARN_EVERY_MS) return;
|
|
27
|
+
warnedAt = now;
|
|
28
|
+
console.warn(`[nuxt-i18n-micro] cacheStorage failed, reading dictionaries from their source: ${error?.message ?? error}`);
|
|
29
|
+
}
|
|
30
|
+
async function withDeadline(call, ms) {
|
|
31
|
+
let timer;
|
|
32
|
+
const deadline = new Promise((_, reject) => {
|
|
33
|
+
timer = setTimeout(() => reject(new Error(`no answer in ${ms} ms`)), ms);
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
return await Promise.race([call(), deadline]);
|
|
37
|
+
} finally {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const segment = (value) => String(value).replace(/[^\w-]/g, (char) => `%${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
42
|
+
export function sharedChunkKey(locale, page, config, host = "") {
|
|
43
|
+
if (!sharedStorage()) return null;
|
|
44
|
+
const pageSegment = segment(page);
|
|
45
|
+
if (pageSegment.length > MAX_PAGE_LENGTH) return null;
|
|
46
|
+
const build = [getI18nPrivateConfig().cacheStorageBuild ?? "", config.dateBuild, config.fallbackLocale ?? "", host].map(segment).join("~");
|
|
47
|
+
const key = `i18n-micro:${build}:${segment(locale)}:${pageSegment}`;
|
|
48
|
+
return key.length > MAX_KEY_LENGTH ? null : key;
|
|
49
|
+
}
|
|
50
|
+
function toChunk(value) {
|
|
51
|
+
if (!value || Object.getPrototypeOf(value) !== Object.prototype || !Object.keys(value).length) return null;
|
|
52
|
+
return toEntry(value);
|
|
53
|
+
}
|
|
54
|
+
export async function readSharedChunk(key) {
|
|
55
|
+
if (Date.now() < closedUntil) return null;
|
|
56
|
+
try {
|
|
57
|
+
const value = await withDeadline(() => sharedStorage().getItem(key), failures ? PROBATION_TIMEOUT_MS : TRUSTED_TIMEOUT_MS);
|
|
58
|
+
failures = 0;
|
|
59
|
+
return toChunk(value);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (++failures >= FAILURES_TO_CLOSE) closedUntil = Date.now() + COOLDOWN_MS;
|
|
62
|
+
warn(error);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export async function writeSharedChunk(key, json, config) {
|
|
67
|
+
if (failures) return;
|
|
68
|
+
const { cacheTtl } = config;
|
|
69
|
+
const ttl = cacheTtl > 0 && Number.isFinite(cacheTtl) ? Math.ceil(cacheTtl) : DEFAULT_TTL_SECONDS;
|
|
70
|
+
await withDeadline(() => sharedStorage().setItem(key, json, { ttl }), TRUSTED_TIMEOUT_MS).catch(warn);
|
|
71
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Locale } from '@i18n-micro/types';
|
|
2
|
+
import { type ParsedTranslationRelativePath } from '@i18n-micro/utils/parse-path';
|
|
3
|
+
/** A cache the watcher drops on change (the server cache, the SSR storage cache). */
|
|
4
|
+
interface TranslationWatchCache {
|
|
5
|
+
clear(): void;
|
|
6
|
+
}
|
|
7
|
+
export interface DevTranslationMergeInput {
|
|
8
|
+
locale: string;
|
|
9
|
+
pageName: string;
|
|
10
|
+
locales?: Array<string | Locale>;
|
|
11
|
+
fallbackLocale?: string;
|
|
12
|
+
disablePageLocales?: boolean;
|
|
13
|
+
readLocaleFile: (relativePath: string) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
14
|
+
}
|
|
15
|
+
export interface HandleTranslationWatchChangeInput {
|
|
16
|
+
relativePath: string;
|
|
17
|
+
configuredLocales: Set<string>;
|
|
18
|
+
serverCache: TranslationWatchCache;
|
|
19
|
+
storageCache?: TranslationWatchCache | null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A locale file as an object: `{}` when missing; broken JSON warns with the path and is `{}`
|
|
23
|
+
* (one bad file must not silently empty a dictionary, nor crash dev).
|
|
24
|
+
*/
|
|
25
|
+
export declare function readTranslationFile(filePath: string): Record<string, unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* Absolute translation + additional roots for live/HMR reads.
|
|
28
|
+
* `rootDirs` order matches build: extends first, project last (last wins).
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveLiveTranslationDirs(input: {
|
|
31
|
+
rootDirs: string[];
|
|
32
|
+
translationDir: string;
|
|
33
|
+
additionalTranslationDirs?: string[];
|
|
34
|
+
}): {
|
|
35
|
+
translationRoots: string[];
|
|
36
|
+
additionalRoots: string[];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The path of a live translation file relative to its root, or null when the file is not one:
|
|
40
|
+
* any `.json` under a `translationDir` root, a top-level `{locale}.json` in an additional root
|
|
41
|
+
* (only those take part in the global merge).
|
|
42
|
+
*/
|
|
43
|
+
export declare function translationRelativePath(filePath: string, dirs: {
|
|
44
|
+
translationRoots: string[];
|
|
45
|
+
additionalRoots: string[];
|
|
46
|
+
}): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* Reader for live translation dirs (Nuxt layers + optional additional roots).
|
|
49
|
+
* Matches build merge: additional first (all layers), then `translationDir` layers (last wins).
|
|
50
|
+
*/
|
|
51
|
+
export declare function createTranslationDirReader(translationsRoot: string | string[], additionalRoots?: string[]): (relativeFilePath: string) => Record<string, unknown>;
|
|
52
|
+
/**
|
|
53
|
+
* Merge live locale files for HMR + `import.meta.dev` cache misses.
|
|
54
|
+
* Always source-style fallback (buildDir snapshots bake fallback; live files do not).
|
|
55
|
+
*/
|
|
56
|
+
export declare function mergeDevTranslations(input: DevTranslationMergeInput): Promise<Record<string, unknown>>;
|
|
57
|
+
/**
|
|
58
|
+
* A translation file changed: drop every cached dictionary. A root file feeds every page, and
|
|
59
|
+
* through fallback chains other locales too, so any narrower invalidation leaves stale chunks
|
|
60
|
+
* (pages it did not list, `disablePageLocales` keys, fallback locales). Dev reads are live
|
|
61
|
+
* merges, so the next request rebuilds only what is asked for.
|
|
62
|
+
*/
|
|
63
|
+
export declare function handleTranslationWatchChange(input: HandleTranslationWatchChangeInput): Promise<ParsedTranslationRelativePath & {
|
|
64
|
+
dropped: boolean;
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* Tracks translation file contents so a write that changes nothing is not acted on.
|
|
68
|
+
*
|
|
69
|
+
* Editors save on focus loss and formatters rewrite files byte-for-byte; a change drops
|
|
70
|
+
* the caches and reloads the page, which is far too much to spend on a file identical to
|
|
71
|
+
* the one already merged.
|
|
72
|
+
*/
|
|
73
|
+
export declare class TranslationContentTracker {
|
|
74
|
+
private readonly hashes;
|
|
75
|
+
/** `true` when the file should be processed: new content, or unreadable so assume changed. */
|
|
76
|
+
shouldProcess(filePath: string): boolean;
|
|
77
|
+
forget(filePath: string): void;
|
|
78
|
+
}
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { forwardSlashes } from "@i18n-micro/utils/app-path";
|
|
5
|
+
import { deepMergeTranslationsRecursive } from "@i18n-micro/utils/deep-merge";
|
|
6
|
+
import { parseJsonFile } from "@i18n-micro/utils/load-dir";
|
|
7
|
+
import { mergeSourceTranslations, normalizeConfiguredLocales } from "@i18n-micro/utils/merge-source";
|
|
8
|
+
import { parseTranslationRelativePath } from "@i18n-micro/utils/parse-path";
|
|
9
|
+
export function readTranslationFile(filePath) {
|
|
10
|
+
if (!existsSync(filePath)) return {};
|
|
11
|
+
let parsed;
|
|
12
|
+
try {
|
|
13
|
+
parsed = parseJsonFile(readFileSync(filePath, "utf-8"));
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return skip(filePath, error.message);
|
|
16
|
+
}
|
|
17
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return skip(filePath, "not a JSON object");
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
function skip(filePath, reason) {
|
|
21
|
+
if (!WARNED.has(filePath)) console.warn(`[nuxt-i18n-micro] Skipping invalid JSON in ${filePath}: ${reason}`);
|
|
22
|
+
WARNED.add(filePath);
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
const WARNED = /* @__PURE__ */ new Set();
|
|
26
|
+
export function resolveLiveTranslationDirs(input) {
|
|
27
|
+
const translationRoots = input.rootDirs.map((root) => resolve(root, input.translationDir));
|
|
28
|
+
const additionalRoots = [];
|
|
29
|
+
for (const dir of input.additionalTranslationDirs ?? []) {
|
|
30
|
+
for (const root of input.rootDirs) {
|
|
31
|
+
additionalRoots.push(resolve(root, dir));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { translationRoots, additionalRoots };
|
|
35
|
+
}
|
|
36
|
+
export function translationRelativePath(filePath, dirs) {
|
|
37
|
+
const inside = (root) => {
|
|
38
|
+
const rel = relative(root, filePath);
|
|
39
|
+
return rel.startsWith("..") || isAbsolute(rel) ? null : forwardSlashes(rel);
|
|
40
|
+
};
|
|
41
|
+
for (const root of dirs.additionalRoots) {
|
|
42
|
+
const rel = inside(root);
|
|
43
|
+
if (rel && !rel.includes("/") && rel.endsWith(".json")) return rel;
|
|
44
|
+
}
|
|
45
|
+
for (const root of dirs.translationRoots) {
|
|
46
|
+
const rel = inside(root);
|
|
47
|
+
if (rel?.endsWith(".json")) return rel;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
export function createTranslationDirReader(translationsRoot, additionalRoots = []) {
|
|
52
|
+
const translationRoots = Array.isArray(translationsRoot) ? translationsRoot : [translationsRoot];
|
|
53
|
+
return (relativeFilePath) => {
|
|
54
|
+
let content = {};
|
|
55
|
+
if (!relativeFilePath.includes("/") && relativeFilePath.endsWith(".json")) {
|
|
56
|
+
const locale = relativeFilePath.slice(0, -".json".length);
|
|
57
|
+
for (const extraRoot of additionalRoots) {
|
|
58
|
+
content = deepMergeTranslationsRecursive(content, readTranslationFile(join(extraRoot, `${locale}.json`)));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const root of translationRoots) {
|
|
62
|
+
content = deepMergeTranslationsRecursive(content, readTranslationFile(join(root, relativeFilePath)));
|
|
63
|
+
}
|
|
64
|
+
return content;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export async function mergeDevTranslations(input) {
|
|
68
|
+
const locales = normalizeConfiguredLocales(input.locales);
|
|
69
|
+
const effectiveLocales = locales.length > 0 ? locales : [{ code: input.locale }];
|
|
70
|
+
return mergeSourceTranslations({
|
|
71
|
+
locale: input.locale,
|
|
72
|
+
pageName: input.pageName,
|
|
73
|
+
locales: effectiveLocales,
|
|
74
|
+
globalFallbackLocale: input.fallbackLocale,
|
|
75
|
+
disablePageLocales: input.disablePageLocales,
|
|
76
|
+
readLocaleFile: input.readLocaleFile
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export async function handleTranslationWatchChange(input) {
|
|
80
|
+
const parsed = parseTranslationRelativePath(input.relativePath);
|
|
81
|
+
const dropped = parsed.type === "page" || parsed.type === "root" && input.configuredLocales.has(parsed.locale);
|
|
82
|
+
if (dropped) {
|
|
83
|
+
input.serverCache.clear();
|
|
84
|
+
input.storageCache?.clear();
|
|
85
|
+
}
|
|
86
|
+
return { ...parsed, dropped };
|
|
87
|
+
}
|
|
88
|
+
export class TranslationContentTracker {
|
|
89
|
+
hashes = /* @__PURE__ */ new Map();
|
|
90
|
+
/** `true` when the file should be processed: new content, or unreadable so assume changed. */
|
|
91
|
+
shouldProcess(filePath) {
|
|
92
|
+
let hash;
|
|
93
|
+
try {
|
|
94
|
+
hash = createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
|
95
|
+
} catch {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (this.hashes.get(filePath) === hash) return false;
|
|
99
|
+
this.hashes.set(filePath, hash);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
forget(filePath) {
|
|
103
|
+
this.hashes.delete(filePath);
|
|
104
|
+
}
|
|
105
|
+
}
|