@common.js/next-intl 4.13.1
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 +21 -0
- package/README.md +5 -0
- package/config.d.ts +4 -0
- package/dist/cjs/development/ExtractorCodec-D9Tw618d.cjs +7 -0
- package/dist/cjs/development/JSONCodec-CzA8ubPy.cjs +55 -0
- package/dist/cjs/development/POCodec-CWGHK-Gp.cjs +111 -0
- package/dist/cjs/development/plugin-DlFYUFWh.cjs +1613 -0
- package/dist/cjs/development/plugin.cjs +13 -0
- package/dist/esm/development/config.js +13 -0
- package/dist/esm/development/extractor/ExtractionCompiler.js +284 -0
- package/dist/esm/development/extractor/catalog/CatalogLocales.js +398 -0
- package/dist/esm/development/extractor/catalog/CatalogManager.js +1515 -0
- package/dist/esm/development/extractor/catalog/CatalogPersister.js +341 -0
- package/dist/esm/development/extractor/catalog/SaveScheduler.js +314 -0
- package/dist/esm/development/extractor/catalogLoader.js +288 -0
- package/dist/esm/development/extractor/extractMessages.js +180 -0
- package/dist/esm/development/extractor/extractionLoader.js +37 -0
- package/dist/esm/development/extractor/extractor/LRUCache.js +68 -0
- package/dist/esm/development/extractor/extractor/MessageExtractor.js +273 -0
- package/dist/esm/development/extractor/format/ExtractorCodec.js +13 -0
- package/dist/esm/development/extractor/format/codecs/JSONCodec.js +91 -0
- package/dist/esm/development/extractor/format/codecs/POCodec.js +227 -0
- package/dist/esm/development/extractor/format/index.js +289 -0
- package/dist/esm/development/extractor/normalizeExtractorConfig.js +91 -0
- package/dist/esm/development/extractor/source/SourceFileFilter.js +104 -0
- package/dist/esm/development/extractor/source/SourceFileScanner.js +297 -0
- package/dist/esm/development/extractor/source/SourceFileWatcher.js +610 -0
- package/dist/esm/development/extractor/utils.js +133 -0
- package/dist/esm/development/extractor.js +25 -0
- package/dist/esm/development/index.react-client.js +59 -0
- package/dist/esm/development/index.react-server.js +61 -0
- package/dist/esm/development/middleware/getAlternateLinksHeaderValue.js +142 -0
- package/dist/esm/development/middleware/middleware.js +231 -0
- package/dist/esm/development/middleware/resolveLocale.js +150 -0
- package/dist/esm/development/middleware/syncCookie.js +87 -0
- package/dist/esm/development/middleware/utils.js +403 -0
- package/dist/esm/development/middleware.js +16 -0
- package/dist/esm/development/navigation/react-client/createNavigation.js +195 -0
- package/dist/esm/development/navigation/react-client/useBasePathname.js +46 -0
- package/dist/esm/development/navigation/react-server/createNavigation.js +111 -0
- package/dist/esm/development/navigation/react-server/getServerLocale.js +167 -0
- package/dist/esm/development/navigation/shared/BaseLink.js +115 -0
- package/dist/esm/development/navigation/shared/createSharedNavigationFns.js +214 -0
- package/dist/esm/development/navigation/shared/syncLocaleCookie.js +139 -0
- package/dist/esm/development/navigation/shared/utils.js +352 -0
- package/dist/esm/development/navigation.react-client.js +16 -0
- package/dist/esm/development/navigation.react-server.js +16 -0
- package/dist/esm/development/plugin/config.js +28 -0
- package/dist/esm/development/plugin/createNextIntlPlugin.js +66 -0
- package/dist/esm/development/plugin/declaration/createMessagesDeclaration.js +91 -0
- package/dist/esm/development/plugin/extractor/initExtractionCompiler.js +64 -0
- package/dist/esm/development/plugin/getNextConfig.js +355 -0
- package/dist/esm/development/plugin/nextFlags.js +47 -0
- package/dist/esm/development/plugin/utils.js +43 -0
- package/dist/esm/development/plugin/watchFile.js +32 -0
- package/dist/esm/development/plugin.js +16 -0
- package/dist/esm/development/react-client/index.js +67 -0
- package/dist/esm/development/react-server/NextIntlClientProviderServer.js +327 -0
- package/dist/esm/development/react-server/useConfig.js +40 -0
- package/dist/esm/development/react-server/useExtracted.js +21 -0
- package/dist/esm/development/react-server/useFormatter.js +21 -0
- package/dist/esm/development/react-server/useLocale.js +20 -0
- package/dist/esm/development/react-server/useMessages.js +21 -0
- package/dist/esm/development/react-server/useNow.js +25 -0
- package/dist/esm/development/react-server/useTimeZone.js +20 -0
- package/dist/esm/development/react-server/useTranslations.js +71 -0
- package/dist/esm/development/routing/config.js +82 -0
- package/dist/esm/development/routing/defineRouting.js +116 -0
- package/dist/esm/development/routing.js +16 -0
- package/dist/esm/development/server/react-client/index.js +68 -0
- package/dist/esm/development/server/react-server/RequestLocale.js +267 -0
- package/dist/esm/development/server/react-server/RequestLocaleCache.js +33 -0
- package/dist/esm/development/server/react-server/createRequestConfig.js +16 -0
- package/dist/esm/development/server/react-server/getConfig.js +283 -0
- package/dist/esm/development/server/react-server/getConfigNow.js +166 -0
- package/dist/esm/development/server/react-server/getDefaultNow.js +16 -0
- package/dist/esm/development/server/react-server/getExtracted.js +176 -0
- package/dist/esm/development/server/react-server/getFormats.js +166 -0
- package/dist/esm/development/server/react-server/getFormatter.js +186 -0
- package/dist/esm/development/server/react-server/getLocale.js +166 -0
- package/dist/esm/development/server/react-server/getMessages.js +194 -0
- package/dist/esm/development/server/react-server/getNow.js +164 -0
- package/dist/esm/development/server/react-server/getRequestConfig.js +15 -0
- package/dist/esm/development/server/react-server/getServerExtractor.js +108 -0
- package/dist/esm/development/server/react-server/getServerFormatter.js +81 -0
- package/dist/esm/development/server/react-server/getServerTranslator.js +70 -0
- package/dist/esm/development/server/react-server/getTimeZone.js +180 -0
- package/dist/esm/development/server/react-server/getTranslations.js +179 -0
- package/dist/esm/development/server/react-server/validateLocale.js +20 -0
- package/dist/esm/development/server.react-client.js +40 -0
- package/dist/esm/development/server.react-server.js +53 -0
- package/dist/esm/development/shared/NextIntlClientProvider.js +79 -0
- package/dist/esm/development/shared/constants.js +12 -0
- package/dist/esm/development/shared/use.js +59 -0
- package/dist/esm/development/shared/utils.js +201 -0
- package/dist/esm/production/config.js +13 -0
- package/dist/esm/production/extractor/ExtractionCompiler.js +275 -0
- package/dist/esm/production/extractor/catalog/CatalogLocales.js +384 -0
- package/dist/esm/production/extractor/catalog/CatalogManager.js +1401 -0
- package/dist/esm/production/extractor/catalog/CatalogPersister.js +334 -0
- package/dist/esm/production/extractor/catalog/SaveScheduler.js +278 -0
- package/dist/esm/production/extractor/catalogLoader.js +251 -0
- package/dist/esm/production/extractor/extractMessages.js +178 -0
- package/dist/esm/production/extractor/extractionLoader.js +27 -0
- package/dist/esm/production/extractor/extractor/LRUCache.js +59 -0
- package/dist/esm/production/extractor/extractor/MessageExtractor.js +259 -0
- package/dist/esm/production/extractor/format/ExtractorCodec.js +13 -0
- package/dist/esm/production/extractor/format/codecs/JSONCodec.js +83 -0
- package/dist/esm/production/extractor/format/codecs/POCodec.js +210 -0
- package/dist/esm/production/extractor/format/index.js +278 -0
- package/dist/esm/production/extractor/normalizeExtractorConfig.js +54 -0
- package/dist/esm/production/extractor/source/SourceFileFilter.js +99 -0
- package/dist/esm/production/extractor/source/SourceFileScanner.js +293 -0
- package/dist/esm/production/extractor/source/SourceFileWatcher.js +579 -0
- package/dist/esm/production/extractor/utils.js +108 -0
- package/dist/esm/production/extractor.js +25 -0
- package/dist/esm/production/index.react-client.js +59 -0
- package/dist/esm/production/index.react-server.js +61 -0
- package/dist/esm/production/middleware/getAlternateLinksHeaderValue.js +104 -0
- package/dist/esm/production/middleware/middleware.js +155 -0
- package/dist/esm/production/middleware/resolveLocale.js +99 -0
- package/dist/esm/production/middleware/syncCookie.js +81 -0
- package/dist/esm/production/middleware/utils.js +315 -0
- package/dist/esm/production/middleware.js +16 -0
- package/dist/esm/production/navigation/react-client/createNavigation.js +174 -0
- package/dist/esm/production/navigation/react-client/useBasePathname.js +32 -0
- package/dist/esm/production/navigation/react-server/createNavigation.js +110 -0
- package/dist/esm/production/navigation/react-server/getServerLocale.js +162 -0
- package/dist/esm/production/navigation/shared/BaseLink.js +98 -0
- package/dist/esm/production/navigation/shared/createSharedNavigationFns.js +182 -0
- package/dist/esm/production/navigation/shared/syncLocaleCookie.js +113 -0
- package/dist/esm/production/navigation/shared/utils.js +264 -0
- package/dist/esm/production/navigation.react-client.js +16 -0
- package/dist/esm/production/navigation.react-server.js +16 -0
- package/dist/esm/production/plugin/config.js +22 -0
- package/dist/esm/production/plugin/createNextIntlPlugin.js +48 -0
- package/dist/esm/production/plugin/declaration/createMessagesDeclaration.js +72 -0
- package/dist/esm/production/plugin/extractor/initExtractionCompiler.js +33 -0
- package/dist/esm/production/plugin/getNextConfig.js +242 -0
- package/dist/esm/production/plugin/nextFlags.js +44 -0
- package/dist/esm/production/plugin/utils.js +35 -0
- package/dist/esm/production/plugin/watchFile.js +26 -0
- package/dist/esm/production/plugin.js +16 -0
- package/dist/esm/production/react-client/index.js +57 -0
- package/dist/esm/production/react-server/NextIntlClientProviderServer.js +327 -0
- package/dist/esm/production/react-server/useConfig.js +35 -0
- package/dist/esm/production/react-server/useExtracted.js +21 -0
- package/dist/esm/production/react-server/useFormatter.js +21 -0
- package/dist/esm/production/react-server/useLocale.js +19 -0
- package/dist/esm/production/react-server/useMessages.js +21 -0
- package/dist/esm/production/react-server/useNow.js +22 -0
- package/dist/esm/production/react-server/useTimeZone.js +19 -0
- package/dist/esm/production/react-server/useTranslations.js +71 -0
- package/dist/esm/production/routing/config.js +77 -0
- package/dist/esm/production/routing/defineRouting.js +13 -0
- package/dist/esm/production/routing.js +16 -0
- package/dist/esm/production/server/react-client/index.js +51 -0
- package/dist/esm/production/server/react-server/RequestLocale.js +246 -0
- package/dist/esm/production/server/react-server/RequestLocaleCache.js +30 -0
- package/dist/esm/production/server/react-server/createRequestConfig.js +16 -0
- package/dist/esm/production/server/react-server/getConfig.js +262 -0
- package/dist/esm/production/server/react-server/getConfigNow.js +162 -0
- package/dist/esm/production/server/react-server/getDefaultNow.js +14 -0
- package/dist/esm/production/server/react-server/getExtracted.js +166 -0
- package/dist/esm/production/server/react-server/getFormats.js +157 -0
- package/dist/esm/production/server/react-server/getFormatter.js +179 -0
- package/dist/esm/production/server/react-server/getLocale.js +157 -0
- package/dist/esm/production/server/react-server/getMessages.js +190 -0
- package/dist/esm/production/server/react-server/getNow.js +164 -0
- package/dist/esm/production/server/react-server/getRequestConfig.js +13 -0
- package/dist/esm/production/server/react-server/getServerExtractor.js +20 -0
- package/dist/esm/production/server/react-server/getServerFormatter.js +78 -0
- package/dist/esm/production/server/react-server/getServerTranslator.js +69 -0
- package/dist/esm/production/server/react-server/getTimeZone.js +176 -0
- package/dist/esm/production/server/react-server/getTranslations.js +166 -0
- package/dist/esm/production/server.react-client.js +40 -0
- package/dist/esm/production/server.react-server.js +53 -0
- package/dist/esm/production/shared/NextIntlClientProvider.js +77 -0
- package/dist/esm/production/shared/constants.js +11 -0
- package/dist/esm/production/shared/use.js +51 -0
- package/dist/esm/production/shared/utils.js +156 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/extractor/ExtractionCompiler.d.ts +16 -0
- package/dist/types/extractor/catalog/CatalogLocales.d.ts +29 -0
- package/dist/types/extractor/catalog/CatalogManager.d.ts +76 -0
- package/dist/types/extractor/catalog/CatalogPersister.d.ts +20 -0
- package/dist/types/extractor/catalog/SaveScheduler.d.ts +18 -0
- package/dist/types/extractor/extractMessages.d.ts +2 -0
- package/dist/types/extractor/extractor/LRUCache.d.ts +7 -0
- package/dist/types/extractor/extractor/MessageExtractor.bench.d.ts +1 -0
- package/dist/types/extractor/extractor/MessageExtractor.d.ts +17 -0
- package/dist/types/extractor/format/ExtractorCodec.d.ts +33 -0
- package/dist/types/extractor/format/codecs/JSONCodec.d.ts +2 -0
- package/dist/types/extractor/format/codecs/POCodec.d.ts +2 -0
- package/dist/types/extractor/format/codecs/fixtures/JSONCodecStructured.d.ts +2 -0
- package/dist/types/extractor/format/codecs/fixtures/POCodecSourceMessageKey.d.ts +2 -0
- package/dist/types/extractor/format/index.d.ts +15 -0
- package/dist/types/extractor/format/types.d.ts +8 -0
- package/dist/types/extractor/index.d.ts +2 -0
- package/dist/types/extractor/normalizeExtractorConfig.d.ts +5 -0
- package/dist/types/extractor/source/SourceFileFilter.d.ts +8 -0
- package/dist/types/extractor/source/SourceFileScanner.d.ts +4 -0
- package/dist/types/extractor/source/SourceFileWatcher.d.ts +15 -0
- package/dist/types/extractor/types.d.ts +79 -0
- package/dist/types/extractor/utils.d.ts +9 -0
- package/dist/types/extractor.d.ts +1 -0
- package/dist/types/index.react-client.d.ts +8 -0
- package/dist/types/index.react-server.d.ts +1 -0
- package/dist/types/middleware/getAlternateLinksHeaderValue.d.ts +13 -0
- package/dist/types/middleware/index.d.ts +4 -0
- package/dist/types/middleware/middleware.d.ts +4 -0
- package/dist/types/middleware/resolveLocale.d.ts +9 -0
- package/dist/types/middleware/syncCookie.d.ts +7 -0
- package/dist/types/middleware/utils.d.ts +26 -0
- package/dist/types/middleware.d.ts +1 -0
- package/dist/types/navigation/react-client/createNavigation.d.ts +465 -0
- package/dist/types/navigation/react-client/index.d.ts +2 -0
- package/dist/types/navigation/react-client/useBasePathname.d.ts +5 -0
- package/dist/types/navigation/react-server/createNavigation.d.ts +384 -0
- package/dist/types/navigation/react-server/getServerLocale.d.ts +5 -0
- package/dist/types/navigation/react-server/index.d.ts +2 -0
- package/dist/types/navigation/shared/BaseLink.d.ts +11 -0
- package/dist/types/navigation/shared/StrictParams.d.ts +8 -0
- package/dist/types/navigation/shared/createSharedNavigationFns.d.ts +370 -0
- package/dist/types/navigation/shared/syncLocaleCookie.d.ts +8 -0
- package/dist/types/navigation/shared/utils.d.ts +53 -0
- package/dist/types/navigation.react-client.d.ts +1 -0
- package/dist/types/navigation.react-server.d.ts +1 -0
- package/dist/types/plugin/catalog/catalogLoader.d.ts +10 -0
- package/dist/types/plugin/config.d.ts +3 -0
- package/dist/types/plugin/createNextIntlPlugin.d.ts +3 -0
- package/dist/types/plugin/declaration/createMessagesDeclaration.d.ts +1 -0
- package/dist/types/plugin/declaration/index.d.ts +1 -0
- package/dist/types/plugin/extractor/extractionLoader.d.ts +3 -0
- package/dist/types/plugin/extractor/initExtractionCompiler.d.ts +2 -0
- package/dist/types/plugin/getNextConfig.d.ts +4 -0
- package/dist/types/plugin/index.d.ts +1 -0
- package/dist/types/plugin/nextFlags.d.ts +2 -0
- package/dist/types/plugin/types.d.ts +25 -0
- package/dist/types/plugin/utils.d.ts +8 -0
- package/dist/types/plugin/watchFile.d.ts +6 -0
- package/dist/types/plugin.d.ts +1 -0
- package/dist/types/react-client/index.d.ts +16 -0
- package/dist/types/react-server/NextIntlClientProviderServer.d.ts +5 -0
- package/dist/types/react-server/index.d.ts +16 -0
- package/dist/types/react-server/testUtils.d.ts +2 -0
- package/dist/types/react-server/useConfig.d.ts +2 -0
- package/dist/types/react-server/useExtracted.d.ts +2 -0
- package/dist/types/react-server/useFormatter.d.ts +2 -0
- package/dist/types/react-server/useLocale.d.ts +2 -0
- package/dist/types/react-server/useMessages.d.ts +2 -0
- package/dist/types/react-server/useNow.d.ts +2 -0
- package/dist/types/react-server/useTimeZone.d.ts +2 -0
- package/dist/types/react-server/useTranslations.d.ts +2 -0
- package/dist/types/routing/config.d.ts +73 -0
- package/dist/types/routing/defineRouting.d.ts +3 -0
- package/dist/types/routing/index.d.ts +3 -0
- package/dist/types/routing/types.d.ts +25 -0
- package/dist/types/routing.d.ts +1 -0
- package/dist/types/server/react-client/index.d.ts +10 -0
- package/dist/types/server/react-server/RequestLocale.d.ts +1 -0
- package/dist/types/server/react-server/RequestLocaleCache.d.ts +3 -0
- package/dist/types/server/react-server/createRequestConfig.d.ts +3 -0
- package/dist/types/server/react-server/getConfig.d.ts +13 -0
- package/dist/types/server/react-server/getConfigNow.d.ts +4 -0
- package/dist/types/server/react-server/getDefaultNow.d.ts +3 -0
- package/dist/types/server/react-server/getExtracted.d.ts +10 -0
- package/dist/types/server/react-server/getFormats.d.ts +3 -0
- package/dist/types/server/react-server/getFormatter.d.ts +10 -0
- package/dist/types/server/react-server/getLocale.d.ts +4 -0
- package/dist/types/server/react-server/getMessages.d.ts +6 -0
- package/dist/types/server/react-server/getNow.d.ts +4 -0
- package/dist/types/server/react-server/getRequestConfig.d.ts +35 -0
- package/dist/types/server/react-server/getServerExtractor.d.ts +5 -0
- package/dist/types/server/react-server/getServerFormatter.d.ts +29 -0
- package/dist/types/server/react-server/getServerTranslator.d.ts +4 -0
- package/dist/types/server/react-server/getTimeZone.d.ts +4 -0
- package/dist/types/server/react-server/getTranslations.d.ts +8 -0
- package/dist/types/server/react-server/index.d.ts +12 -0
- package/dist/types/server/react-server/validateLocale.d.ts +1 -0
- package/dist/types/server.react-client.d.ts +1 -0
- package/dist/types/server.react-server.d.ts +1 -0
- package/dist/types/shared/NextIntlClientProvider.d.ts +9 -0
- package/dist/types/shared/constants.d.ts +1 -0
- package/dist/types/shared/types.d.ts +5 -0
- package/dist/types/shared/use.d.ts +3 -0
- package/dist/types/shared/utils.d.ts +20 -0
- package/extractor/catalogLoader.d.ts +4 -0
- package/extractor/extractionLoader.d.ts +4 -0
- package/extractor.d.ts +2 -0
- package/middleware.d.ts +4 -0
- package/navigation.d.ts +2 -0
- package/package.json +60 -0
- package/plugin.d.cts +3 -0
- package/plugin.d.ts +4 -0
- package/routing.d.ts +2 -0
- package/server.d.ts +2 -0
|
@@ -0,0 +1,1613 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs$1 = require('fs/promises');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var watcher = require('@parcel/watcher');
|
|
6
|
+
var fs = require('fs');
|
|
7
|
+
var module$1 = require('module');
|
|
8
|
+
var core = require('@swc/core');
|
|
9
|
+
|
|
10
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
11
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
12
|
+
|
|
13
|
+
var fs__default$1 = /*#__PURE__*/_interopDefaultCompat(fs$1);
|
|
14
|
+
var path__default = /*#__PURE__*/_interopDefaultCompat(path);
|
|
15
|
+
var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
|
|
16
|
+
|
|
17
|
+
function formatMessage(message) {
|
|
18
|
+
return `\n[next-intl] ${message}\n`;
|
|
19
|
+
}
|
|
20
|
+
function throwError(message) {
|
|
21
|
+
throw new Error(formatMessage(message));
|
|
22
|
+
}
|
|
23
|
+
function warn(message) {
|
|
24
|
+
console.warn(formatMessage(message));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns a function that runs the provided callback only once per process.
|
|
29
|
+
* Next.js can call the config multiple times - this ensures we only run once.
|
|
30
|
+
* Uses an environment variable to track execution across config loads.
|
|
31
|
+
*/
|
|
32
|
+
function once(namespace) {
|
|
33
|
+
return function runOnce(fn) {
|
|
34
|
+
if (process.env[namespace] === '1') {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
process.env[namespace] = '1';
|
|
38
|
+
fn();
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stripTrailingSlash(dirPath) {
|
|
43
|
+
if (dirPath.endsWith('/')) {
|
|
44
|
+
return dirPath.slice(0, -1);
|
|
45
|
+
} else {
|
|
46
|
+
return dirPath;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function normalizeMessagesCatalogPaths(messagesPath) {
|
|
50
|
+
const rawPaths = Array.isArray(messagesPath) ? messagesPath : [messagesPath];
|
|
51
|
+
return rawPaths.map(dirPath => stripTrailingSlash(String(dirPath).trim())).filter(dirPath => dirPath.length > 0);
|
|
52
|
+
}
|
|
53
|
+
function normalizeExtractorConfig(input) {
|
|
54
|
+
if (input.messages == null) {
|
|
55
|
+
throwError('`messages` is required when extracting messages.');
|
|
56
|
+
}
|
|
57
|
+
const extract = input.extract;
|
|
58
|
+
let extractPath;
|
|
59
|
+
let sourceLocale;
|
|
60
|
+
if (extract !== undefined && extract !== true) {
|
|
61
|
+
if (extract.sourceLocale) {
|
|
62
|
+
warn('`extract.sourceLocale` is deprecated in favor of `messages.sourceLocale`.');
|
|
63
|
+
sourceLocale = extract.sourceLocale;
|
|
64
|
+
}
|
|
65
|
+
if (extract.path) {
|
|
66
|
+
extractPath = stripTrailingSlash(extract.path);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const locales = input.messages.locales;
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
71
|
+
if (!locales) {
|
|
72
|
+
throwError('`messages.locales` is required when extracting messages.');
|
|
73
|
+
}
|
|
74
|
+
if (input.messages.sourceLocale) {
|
|
75
|
+
sourceLocale = input.messages.sourceLocale;
|
|
76
|
+
}
|
|
77
|
+
if (!sourceLocale) {
|
|
78
|
+
throwError('`messages.sourceLocale` is required when extracting messages.');
|
|
79
|
+
}
|
|
80
|
+
const srcPath = input.srcPath;
|
|
81
|
+
if (srcPath == null) {
|
|
82
|
+
throwError('`srcPath` is required when extracting messages.');
|
|
83
|
+
}
|
|
84
|
+
const pathIsArray = Array.isArray(input.messages.path);
|
|
85
|
+
const messagesPath = normalizeMessagesCatalogPaths(input.messages.path);
|
|
86
|
+
if (messagesPath.length === 0) {
|
|
87
|
+
throwError('`messages.path` must not be empty.');
|
|
88
|
+
}
|
|
89
|
+
if (extractPath == null) {
|
|
90
|
+
if (pathIsArray) {
|
|
91
|
+
throwError('When `messages.path` is an array, `extract.path` is required to select the writable catalog directory.');
|
|
92
|
+
}
|
|
93
|
+
extractPath = messagesPath[0];
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
extract: {
|
|
97
|
+
locales,
|
|
98
|
+
path: extractPath,
|
|
99
|
+
sourceLocale,
|
|
100
|
+
srcPath
|
|
101
|
+
},
|
|
102
|
+
messages: {
|
|
103
|
+
format: input.messages.format,
|
|
104
|
+
path: messagesPath
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Wrapper around `fs.watch` that provides a workaround
|
|
111
|
+
* for https://github.com/nodejs/node/issues/5039.
|
|
112
|
+
*/
|
|
113
|
+
function watchFile(filepath, callback) {
|
|
114
|
+
const directory = path__default.default.dirname(filepath);
|
|
115
|
+
const filename = path__default.default.basename(filepath);
|
|
116
|
+
return fs__default.default.watch(directory, {
|
|
117
|
+
persistent: false,
|
|
118
|
+
recursive: false
|
|
119
|
+
}, (event, changedFilename) => {
|
|
120
|
+
if (changedFilename === filename) {
|
|
121
|
+
callback();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const runOnce$1 = once('_NEXT_INTL_COMPILE_MESSAGES');
|
|
127
|
+
function createMessagesDeclaration(messagesPaths) {
|
|
128
|
+
// Instead of running _only_ in certain cases, it's
|
|
129
|
+
// safer to _avoid_ running for certain known cases.
|
|
130
|
+
// https://github.com/amannn/next-intl/issues/2006
|
|
131
|
+
const shouldBailOut = ['info', 'start'
|
|
132
|
+
|
|
133
|
+
// Note: These commands don't consult the
|
|
134
|
+
// Next.js config, so we can't detect them here.
|
|
135
|
+
// - telemetry (however, the `detached-flush` DOES - see `createNextIntlPlugin`)
|
|
136
|
+
// - lint
|
|
137
|
+
//
|
|
138
|
+
// What remains are:
|
|
139
|
+
// - dev
|
|
140
|
+
// - build
|
|
141
|
+
// - typegen
|
|
142
|
+
].some(arg => process.argv.includes(arg));
|
|
143
|
+
if (shouldBailOut) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
runOnce$1(() => {
|
|
147
|
+
for (const messagesPath of messagesPaths) {
|
|
148
|
+
const fullPath = path__default.default.resolve(messagesPath);
|
|
149
|
+
if (!fs__default.default.existsSync(fullPath)) {
|
|
150
|
+
throwError(`\`createMessagesDeclaration\` points to a non-existent file: ${fullPath}`);
|
|
151
|
+
}
|
|
152
|
+
if (!fullPath.endsWith('.json')) {
|
|
153
|
+
throwError(`\`createMessagesDeclaration\` needs to point to a JSON file. Received: ${fullPath}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Keep this as a runtime check and don't replace
|
|
157
|
+
// this with a constant during the build process
|
|
158
|
+
const env = process.env['NODE_ENV'.trim()];
|
|
159
|
+
compileDeclaration(messagesPath);
|
|
160
|
+
if (env === 'development') {
|
|
161
|
+
startWatching(messagesPath);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function startWatching(messagesPath) {
|
|
167
|
+
const watcher = watchFile(messagesPath, () => {
|
|
168
|
+
compileDeclaration(messagesPath, true);
|
|
169
|
+
});
|
|
170
|
+
process.on('exit', () => {
|
|
171
|
+
watcher.close();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function compileDeclaration(messagesPath, async = false) {
|
|
175
|
+
const declarationPath = messagesPath.replace(/\.json$/, '.d.json.ts');
|
|
176
|
+
function createDeclaration(content) {
|
|
177
|
+
return `// This file is auto-generated by next-intl, do not edit directly.
|
|
178
|
+
// See: https://next-intl.dev/docs/workflows/typescript#messages-arguments
|
|
179
|
+
|
|
180
|
+
declare const messages: ${content.trim()};
|
|
181
|
+
export default messages;`;
|
|
182
|
+
}
|
|
183
|
+
if (async) {
|
|
184
|
+
return fs__default.default.promises.readFile(messagesPath, 'utf-8').then(content => fs__default.default.promises.writeFile(declarationPath, createDeclaration(content)));
|
|
185
|
+
}
|
|
186
|
+
const content = fs__default.default.readFileSync(messagesPath, 'utf-8');
|
|
187
|
+
fs__default.default.writeFileSync(declarationPath, createDeclaration(content));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const formats = {
|
|
191
|
+
json: {
|
|
192
|
+
codec: () => Promise.resolve().then(function () { return require('./JSONCodec-CzA8ubPy.cjs'); }),
|
|
193
|
+
extension: '.json'
|
|
194
|
+
},
|
|
195
|
+
po: {
|
|
196
|
+
codec: () => Promise.resolve().then(function () { return require('./POCodec-CWGHK-Gp.cjs'); }),
|
|
197
|
+
extension: '.po'
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
function isBuiltInFormat(format) {
|
|
201
|
+
return typeof format === 'string' && format in formats;
|
|
202
|
+
}
|
|
203
|
+
function getFormatExtension(format) {
|
|
204
|
+
if (isBuiltInFormat(format)) {
|
|
205
|
+
return formats[format].extension;
|
|
206
|
+
} else {
|
|
207
|
+
return format.extension;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function resolveCodec(format, projectRoot) {
|
|
211
|
+
if (isBuiltInFormat(format)) {
|
|
212
|
+
const factory = (await formats[format].codec()).default;
|
|
213
|
+
return factory();
|
|
214
|
+
} else {
|
|
215
|
+
const resolvedPath = path__default.default.isAbsolute(format.codec) ? format.codec : path__default.default.resolve(projectRoot, format.codec);
|
|
216
|
+
let module;
|
|
217
|
+
try {
|
|
218
|
+
module = await import(resolvedPath);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throwError(`Could not load codec from "${resolvedPath}".\n${error}`);
|
|
221
|
+
}
|
|
222
|
+
const factory = module.default;
|
|
223
|
+
if (!factory || typeof factory !== 'function') {
|
|
224
|
+
throwError(`Codec at "${resolvedPath}" must have a default export returned from \`defineCodec\`.`);
|
|
225
|
+
}
|
|
226
|
+
return factory();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
class SourceFileFilter {
|
|
231
|
+
static EXTENSIONS = ['ts', 'tsx', 'js', 'jsx'];
|
|
232
|
+
|
|
233
|
+
// Will not be entered, except if explicitly asked for
|
|
234
|
+
// TODO: At some point we should infer these from .gitignore
|
|
235
|
+
static IGNORED_DIRECTORIES = ['node_modules', '.next', '.git'];
|
|
236
|
+
static isSourceFile(filePath) {
|
|
237
|
+
const ext = path__default.default.extname(filePath);
|
|
238
|
+
return SourceFileFilter.EXTENSIONS.map(cur => '.' + cur).includes(ext);
|
|
239
|
+
}
|
|
240
|
+
static shouldEnterDirectory(dirPath, srcPaths) {
|
|
241
|
+
const dirName = path__default.default.basename(dirPath);
|
|
242
|
+
if (SourceFileFilter.IGNORED_DIRECTORIES.includes(dirName)) {
|
|
243
|
+
return SourceFileFilter.isIgnoredDirectoryExplicitlyIncluded(dirPath, srcPaths);
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
static isIgnoredDirectoryExplicitlyIncluded(ignoredDirPath, srcPaths) {
|
|
248
|
+
return srcPaths.some(srcPath => SourceFileFilter.isWithinPath(srcPath, ignoredDirPath));
|
|
249
|
+
}
|
|
250
|
+
static isWithinPath(targetPath, basePath) {
|
|
251
|
+
const relativePath = path__default.default.relative(basePath, targetPath);
|
|
252
|
+
return relativePath === '' || !relativePath.startsWith('..');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
class SourceFileScanner {
|
|
257
|
+
static async walkSourceFiles(dir, srcPaths, acc = []) {
|
|
258
|
+
const entries = await fs__default$1.default.readdir(dir, {
|
|
259
|
+
withFileTypes: true
|
|
260
|
+
});
|
|
261
|
+
for (const entry of entries) {
|
|
262
|
+
const entryPath = path__default.default.join(dir, entry.name);
|
|
263
|
+
if (entry.isDirectory()) {
|
|
264
|
+
if (!SourceFileFilter.shouldEnterDirectory(entryPath, srcPaths)) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
await SourceFileScanner.walkSourceFiles(entryPath, srcPaths, acc);
|
|
268
|
+
} else {
|
|
269
|
+
if (SourceFileFilter.isSourceFile(entry.name)) {
|
|
270
|
+
acc.push(entryPath);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return acc;
|
|
275
|
+
}
|
|
276
|
+
static async getSourceFiles(srcPaths) {
|
|
277
|
+
const files = (await Promise.all(srcPaths.map(srcPath => SourceFileScanner.walkSourceFiles(srcPath, srcPaths)))).flat();
|
|
278
|
+
return new Set(files);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
class SourceFileWatcher {
|
|
283
|
+
subscriptions = [];
|
|
284
|
+
constructor(roots, onChange) {
|
|
285
|
+
this.roots = roots;
|
|
286
|
+
this.onChange = onChange;
|
|
287
|
+
}
|
|
288
|
+
async start() {
|
|
289
|
+
if (this.subscriptions.length > 0) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const ignore = SourceFileFilter.IGNORED_DIRECTORIES.map(dir => `**/${dir}/**`);
|
|
293
|
+
for (const root of this.roots) {
|
|
294
|
+
const sub = await watcher.subscribe(root, async (err, events) => {
|
|
295
|
+
if (err) {
|
|
296
|
+
console.error(err);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const filtered = await this.normalizeEvents(events);
|
|
300
|
+
if (filtered.length > 0) {
|
|
301
|
+
await this.onChange(filtered);
|
|
302
|
+
}
|
|
303
|
+
}, {
|
|
304
|
+
ignore
|
|
305
|
+
});
|
|
306
|
+
this.subscriptions.push(sub);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async normalizeEvents(events) {
|
|
310
|
+
const directoryCreatePaths = [];
|
|
311
|
+
const otherEvents = [];
|
|
312
|
+
|
|
313
|
+
// We need to expand directory creates because during rename operations,
|
|
314
|
+
// @parcel/watcher emits a directory create event but may not emit individual
|
|
315
|
+
// file events for the moved files
|
|
316
|
+
await Promise.all(events.map(async event => {
|
|
317
|
+
if (event.type === 'create') {
|
|
318
|
+
try {
|
|
319
|
+
const stats = await fs__default$1.default.stat(event.path);
|
|
320
|
+
if (stats.isDirectory()) {
|
|
321
|
+
directoryCreatePaths.push(event.path);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
// Path doesn't exist or is inaccessible, treat as file
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
otherEvents.push(event);
|
|
329
|
+
}));
|
|
330
|
+
|
|
331
|
+
// Expand directory create events to find source files inside
|
|
332
|
+
let expandedCreateEvents = [];
|
|
333
|
+
if (directoryCreatePaths.length > 0) {
|
|
334
|
+
try {
|
|
335
|
+
const sourceFiles = await SourceFileScanner.getSourceFiles(directoryCreatePaths);
|
|
336
|
+
expandedCreateEvents = Array.from(sourceFiles).map(filePath => ({
|
|
337
|
+
type: 'create',
|
|
338
|
+
path: filePath
|
|
339
|
+
}));
|
|
340
|
+
} catch {
|
|
341
|
+
// Directories might have been deleted or are inaccessible
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Combine original events with expanded directory creates.
|
|
346
|
+
// Deduplicate by path to avoid processing the same file twice
|
|
347
|
+
// in case @parcel/watcher also emitted individual file events.
|
|
348
|
+
const allEvents = [...otherEvents, ...expandedCreateEvents];
|
|
349
|
+
const seenPaths = new Set();
|
|
350
|
+
const deduplicated = [];
|
|
351
|
+
for (const event of allEvents) {
|
|
352
|
+
const key = `${event.type}:${event.path}`;
|
|
353
|
+
if (!seenPaths.has(key)) {
|
|
354
|
+
seenPaths.add(key);
|
|
355
|
+
deduplicated.push(event);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return deduplicated.filter(event => {
|
|
359
|
+
// Keep all delete events (might be deleted directories that no longer exist)
|
|
360
|
+
if (event.type === 'delete') {
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
// Keep source files
|
|
364
|
+
return SourceFileFilter.isSourceFile(event.path);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
async expandDirectoryDeleteEvents(events, prevKnownFiles) {
|
|
368
|
+
const expanded = [];
|
|
369
|
+
for (const event of events) {
|
|
370
|
+
if (event.type === 'delete' && !SourceFileFilter.isSourceFile(event.path)) {
|
|
371
|
+
const dirPath = path__default.default.resolve(event.path);
|
|
372
|
+
const filesInDirectory = [];
|
|
373
|
+
for (const filePath of prevKnownFiles) {
|
|
374
|
+
if (SourceFileFilter.isWithinPath(filePath, dirPath)) {
|
|
375
|
+
filesInDirectory.push(filePath);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// If we found files within this path, it was a directory
|
|
380
|
+
if (filesInDirectory.length > 0) {
|
|
381
|
+
for (const filePath of filesInDirectory) {
|
|
382
|
+
expanded.push({
|
|
383
|
+
type: 'delete',
|
|
384
|
+
path: filePath
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
// Not a directory or no files in it, pass through as-is
|
|
389
|
+
expanded.push(event);
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
// Pass through as-is
|
|
393
|
+
expanded.push(event);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return expanded;
|
|
397
|
+
}
|
|
398
|
+
async stop() {
|
|
399
|
+
await Promise.all(this.subscriptions.map(sub => sub.unsubscribe()));
|
|
400
|
+
this.subscriptions = [];
|
|
401
|
+
}
|
|
402
|
+
[Symbol.dispose]() {
|
|
403
|
+
void this.stop();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function normalizePathToPosix(filePath) {
|
|
408
|
+
// `path.relative` uses OS-specific separators. For stable `.po` references we
|
|
409
|
+
// always use POSIX separators, regardless of the OS that ran extraction.
|
|
410
|
+
return path__default.default.posix.normalize(filePath.split(path__default.default.win32.sep).join(path__default.default.posix.sep));
|
|
411
|
+
}
|
|
412
|
+
const FORBIDDEN_OBJECT_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
413
|
+
function isForbiddenObjectKey(key) {
|
|
414
|
+
return FORBIDDEN_OBJECT_KEYS.has(key);
|
|
415
|
+
}
|
|
416
|
+
function hasLocalesToExtract(config) {
|
|
417
|
+
const {
|
|
418
|
+
locales
|
|
419
|
+
} = config.extract;
|
|
420
|
+
return locales === 'infer' || locales.length > 0;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Essentialls lodash/set, but we avoid this dependency
|
|
424
|
+
function setNestedProperty(obj, keyPath, value) {
|
|
425
|
+
const keys = keyPath.split('.');
|
|
426
|
+
for (const key of keys) {
|
|
427
|
+
if (isForbiddenObjectKey(key)) {
|
|
428
|
+
throw new Error(`Invalid message id segment: ${key}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
let current = obj;
|
|
432
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
433
|
+
const key = keys[i];
|
|
434
|
+
if (!Object.prototype.hasOwnProperty.call(current, key) || typeof current[key] !== 'object' || current[key] === null) {
|
|
435
|
+
current[key] = Object.create(null);
|
|
436
|
+
}
|
|
437
|
+
current = current[key];
|
|
438
|
+
}
|
|
439
|
+
current[keys[keys.length - 1]] = value;
|
|
440
|
+
}
|
|
441
|
+
function getSortedMessages(messages) {
|
|
442
|
+
const warnedMissingReferenceIds = new Set();
|
|
443
|
+
return messages.toSorted((messageA, messageB) => {
|
|
444
|
+
const refA = messageA.references[0];
|
|
445
|
+
const refB = messageB.references[0];
|
|
446
|
+
|
|
447
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
448
|
+
if (refA == null) {
|
|
449
|
+
warnAboutMissingReference(messageA.id, warnedMissingReferenceIds);
|
|
450
|
+
}
|
|
451
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
452
|
+
if (refB == null) {
|
|
453
|
+
warnAboutMissingReference(messageB.id, warnedMissingReferenceIds);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
457
|
+
if (refA == null || refB == null) {
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Sort by path, then line. Same path+line: preserve original order
|
|
462
|
+
return compareReferences(refA, refB);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
function warnAboutMissingReference(id, warnedMissingReferenceIds) {
|
|
466
|
+
if (warnedMissingReferenceIds.has(id)) return;
|
|
467
|
+
warnedMissingReferenceIds.add(id);
|
|
468
|
+
warn(`Missing file reference for extracted message: ${id}`);
|
|
469
|
+
}
|
|
470
|
+
function localeCompare(a, b) {
|
|
471
|
+
return a.localeCompare(b, 'en');
|
|
472
|
+
}
|
|
473
|
+
function compareReferences(refA, refB) {
|
|
474
|
+
const pathCompare = localeCompare(refA.path, refB.path);
|
|
475
|
+
if (pathCompare !== 0) return pathCompare;
|
|
476
|
+
return (refA.line ?? 0) - (refB.line ?? 0);
|
|
477
|
+
}
|
|
478
|
+
function getDefaultProjectRoot() {
|
|
479
|
+
return process.cwd();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
class CatalogLocales {
|
|
483
|
+
onChangeCallbacks = new Set();
|
|
484
|
+
constructor(params) {
|
|
485
|
+
this.messagesDir = params.messagesDir;
|
|
486
|
+
this.sourceLocale = params.sourceLocale;
|
|
487
|
+
this.extension = params.extension;
|
|
488
|
+
this.locales = params.locales;
|
|
489
|
+
}
|
|
490
|
+
async getTargetLocales() {
|
|
491
|
+
if (this.targetLocales) {
|
|
492
|
+
return this.targetLocales;
|
|
493
|
+
}
|
|
494
|
+
if (this.locales === 'infer') {
|
|
495
|
+
this.targetLocales = await this.readTargetLocales();
|
|
496
|
+
} else {
|
|
497
|
+
this.targetLocales = this.locales.filter(locale => locale !== this.sourceLocale);
|
|
498
|
+
}
|
|
499
|
+
return this.targetLocales;
|
|
500
|
+
}
|
|
501
|
+
async readTargetLocales() {
|
|
502
|
+
try {
|
|
503
|
+
const files = await fs__default$1.default.readdir(this.messagesDir);
|
|
504
|
+
return files.filter(file => file.endsWith(this.extension)).map(file => path__default.default.basename(file, this.extension)).filter(locale => locale !== this.sourceLocale);
|
|
505
|
+
} catch {
|
|
506
|
+
return [];
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
subscribeLocalesChange(callback) {
|
|
510
|
+
this.onChangeCallbacks.add(callback);
|
|
511
|
+
if (this.locales === 'infer' && !this.watcher) {
|
|
512
|
+
void this.startWatcher();
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
unsubscribeLocalesChange(callback) {
|
|
516
|
+
this.onChangeCallbacks.delete(callback);
|
|
517
|
+
if (this.onChangeCallbacks.size === 0) {
|
|
518
|
+
this.stopWatcher();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async startWatcher() {
|
|
522
|
+
if (this.watcher) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
await fs__default$1.default.mkdir(this.messagesDir, {
|
|
526
|
+
recursive: true
|
|
527
|
+
});
|
|
528
|
+
this.watcher = fs__default.default.watch(this.messagesDir, {
|
|
529
|
+
persistent: false,
|
|
530
|
+
recursive: false
|
|
531
|
+
}, (event, filename) => {
|
|
532
|
+
const isCatalogFile = filename != null && filename.endsWith(this.extension) && !filename.includes(path__default.default.sep);
|
|
533
|
+
if (isCatalogFile) {
|
|
534
|
+
void this.onChange();
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
stopWatcher() {
|
|
539
|
+
if (this.watcher) {
|
|
540
|
+
this.watcher.close();
|
|
541
|
+
this.watcher = undefined;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async onChange() {
|
|
545
|
+
const oldLocales = new Set(this.targetLocales || []);
|
|
546
|
+
this.targetLocales = await this.readTargetLocales();
|
|
547
|
+
const newLocalesSet = new Set(this.targetLocales);
|
|
548
|
+
const added = this.targetLocales.filter(locale => !oldLocales.has(locale));
|
|
549
|
+
const removed = Array.from(oldLocales).filter(locale => !newLocalesSet.has(locale));
|
|
550
|
+
if (added.length > 0 || removed.length > 0) {
|
|
551
|
+
for (const callback of this.onChangeCallbacks) {
|
|
552
|
+
callback({
|
|
553
|
+
added,
|
|
554
|
+
removed
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
class CatalogPersister {
|
|
562
|
+
constructor(params) {
|
|
563
|
+
this.messagesPath = params.messagesPath;
|
|
564
|
+
this.codec = params.codec;
|
|
565
|
+
this.extension = params.extension;
|
|
566
|
+
}
|
|
567
|
+
getFileName(locale) {
|
|
568
|
+
return locale + this.extension;
|
|
569
|
+
}
|
|
570
|
+
getFilePath(locale) {
|
|
571
|
+
return path__default.default.join(this.messagesPath, this.getFileName(locale));
|
|
572
|
+
}
|
|
573
|
+
async read(locale) {
|
|
574
|
+
const filePath = this.getFilePath(locale);
|
|
575
|
+
let content;
|
|
576
|
+
try {
|
|
577
|
+
content = await fs__default$1.default.readFile(filePath, 'utf8');
|
|
578
|
+
} catch (error) {
|
|
579
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
throw new Error(`Error while reading ${this.getFileName(locale)}:\n> ${error}`, {
|
|
583
|
+
cause: error
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
return this.codec.decode(content, {
|
|
588
|
+
locale
|
|
589
|
+
});
|
|
590
|
+
} catch (error) {
|
|
591
|
+
throw new Error(`Error while decoding ${this.getFileName(locale)}:\n> ${error}`, {
|
|
592
|
+
cause: error
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async write(messages, context) {
|
|
597
|
+
const filePath = this.getFilePath(context.locale);
|
|
598
|
+
const content = this.codec.encode(messages, context);
|
|
599
|
+
try {
|
|
600
|
+
const outputDir = path__default.default.dirname(filePath);
|
|
601
|
+
await fs__default$1.default.mkdir(outputDir, {
|
|
602
|
+
recursive: true
|
|
603
|
+
});
|
|
604
|
+
await fs__default$1.default.writeFile(filePath, content);
|
|
605
|
+
} catch (error) {
|
|
606
|
+
console.error(`❌ Failed to write catalog: ${error}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async getLastModified(locale) {
|
|
610
|
+
const filePath = this.getFilePath(locale);
|
|
611
|
+
try {
|
|
612
|
+
const stats = await fs__default$1.default.stat(filePath);
|
|
613
|
+
return stats.mtime;
|
|
614
|
+
} catch {
|
|
615
|
+
return undefined;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* De-duplicates excessive save invocations,
|
|
622
|
+
* while keeping a single one instant.
|
|
623
|
+
*/
|
|
624
|
+
class SaveScheduler {
|
|
625
|
+
isSaving = false;
|
|
626
|
+
pendingResolvers = [];
|
|
627
|
+
constructor(delayMs = 50) {
|
|
628
|
+
this.delayMs = delayMs;
|
|
629
|
+
}
|
|
630
|
+
async schedule(saveTask) {
|
|
631
|
+
return new Promise((resolve, reject) => {
|
|
632
|
+
this.pendingResolvers.push({
|
|
633
|
+
resolve,
|
|
634
|
+
reject
|
|
635
|
+
});
|
|
636
|
+
this.nextSaveTask = saveTask;
|
|
637
|
+
if (!this.isSaving && !this.saveTimeout) {
|
|
638
|
+
// Not currently saving and no scheduled save, save immediately
|
|
639
|
+
this.executeSave();
|
|
640
|
+
} else if (this.saveTimeout) {
|
|
641
|
+
// A save is already scheduled, reschedule to debounce
|
|
642
|
+
this.scheduleSave();
|
|
643
|
+
}
|
|
644
|
+
// If isSaving is true and no timeout is scheduled, the current save
|
|
645
|
+
// will check for pending resolvers when it completes and schedule
|
|
646
|
+
// another save if needed (see finally block in executeSave)
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
scheduleSave() {
|
|
650
|
+
if (this.saveTimeout) {
|
|
651
|
+
clearTimeout(this.saveTimeout);
|
|
652
|
+
}
|
|
653
|
+
this.saveTimeout = setTimeout(() => {
|
|
654
|
+
this.saveTimeout = undefined;
|
|
655
|
+
this.executeSave();
|
|
656
|
+
}, this.delayMs);
|
|
657
|
+
}
|
|
658
|
+
async executeSave() {
|
|
659
|
+
if (this.isSaving) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const saveTask = this.nextSaveTask;
|
|
663
|
+
if (!saveTask) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Capture current pending resolvers for this save
|
|
668
|
+
const resolversForThisSave = this.pendingResolvers;
|
|
669
|
+
this.pendingResolvers = [];
|
|
670
|
+
this.nextSaveTask = undefined;
|
|
671
|
+
this.isSaving = true;
|
|
672
|
+
try {
|
|
673
|
+
const result = await saveTask();
|
|
674
|
+
|
|
675
|
+
// Resolve only the promises that were pending when this save started
|
|
676
|
+
resolversForThisSave.forEach(({
|
|
677
|
+
resolve
|
|
678
|
+
}) => resolve(result));
|
|
679
|
+
} catch (error) {
|
|
680
|
+
// Reject only the promises that were pending when this save started
|
|
681
|
+
resolversForThisSave.forEach(({
|
|
682
|
+
reject
|
|
683
|
+
}) => reject(error));
|
|
684
|
+
} finally {
|
|
685
|
+
this.isSaving = false;
|
|
686
|
+
|
|
687
|
+
// If new saves were requested during this save, schedule another
|
|
688
|
+
if (this.pendingResolvers.length > 0) {
|
|
689
|
+
this.scheduleSave();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
[Symbol.dispose]() {
|
|
694
|
+
if (this.saveTimeout) {
|
|
695
|
+
clearTimeout(this.saveTimeout);
|
|
696
|
+
this.saveTimeout = undefined;
|
|
697
|
+
}
|
|
698
|
+
this.pendingResolvers = [];
|
|
699
|
+
this.nextSaveTask = undefined;
|
|
700
|
+
this.isSaving = false;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
class CatalogManager {
|
|
705
|
+
/**
|
|
706
|
+
* Extraction-derived fields aggregated into `ExtractorMessage`.
|
|
707
|
+
* Source code is the source of truth for these fields, only ancillary
|
|
708
|
+
* codec fields may merge from disk (e.g. flags).
|
|
709
|
+
*/
|
|
710
|
+
static extractorOwnedAggregatorKeys = new Set(['description', 'id', 'message', 'references']);
|
|
711
|
+
/**
|
|
712
|
+
* Source of truth for statically extracted source messages,
|
|
713
|
+
* grouped by file and message ID.
|
|
714
|
+
*/
|
|
715
|
+
sourceMessagesByFile = new Map();
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Reverse index for rebuilding aggregated messages without scanning all files.
|
|
719
|
+
* Contains the same `SourceMessage` arrays as `sourceMessagesByFile` and is
|
|
720
|
+
* kept in sync with it.
|
|
721
|
+
*/
|
|
722
|
+
sourceMessagesById = new Map();
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Fast lookup for messages by ID, aggregated across all files. This combines
|
|
726
|
+
* metadata from `sourceMessagesById`, e.g. references and descriptions.
|
|
727
|
+
*/
|
|
728
|
+
messagesById = new Map();
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* This potentially also includes outdated ones that were initially available,
|
|
732
|
+
* but are not used anymore. This allows to restore them if they are used again.
|
|
733
|
+
**/
|
|
734
|
+
translationsByTargetLocale = new Map();
|
|
735
|
+
lastWriteByLocale = new Map();
|
|
736
|
+
|
|
737
|
+
// Cached instances
|
|
738
|
+
|
|
739
|
+
// Resolves when all catalogs are loaded
|
|
740
|
+
|
|
741
|
+
// Resolves when the initial project scan and processing is complete
|
|
742
|
+
|
|
743
|
+
constructor(config, opts) {
|
|
744
|
+
this.config = config;
|
|
745
|
+
this.saveScheduler = new SaveScheduler(opts.saveDebounceMs ?? 50);
|
|
746
|
+
this.projectRoot = opts.projectRoot ?? getDefaultProjectRoot();
|
|
747
|
+
this.isDevelopment = opts.isDevelopment ?? false;
|
|
748
|
+
this.extractor = opts.extractor;
|
|
749
|
+
if (this.isDevelopment) {
|
|
750
|
+
// We kick this off as early as possible, so we get notified about changes
|
|
751
|
+
// that happen during the initial project scan (while awaiting it to
|
|
752
|
+
// complete though)
|
|
753
|
+
this.sourceWatcher = new SourceFileWatcher(this.getSrcPaths(), this.handleFileEvents.bind(this));
|
|
754
|
+
void this.sourceWatcher.start();
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
async getCodec() {
|
|
758
|
+
if (!this.codec) {
|
|
759
|
+
this.codec = await resolveCodec(this.config.messages.format, this.projectRoot);
|
|
760
|
+
}
|
|
761
|
+
return this.codec;
|
|
762
|
+
}
|
|
763
|
+
async getPersister() {
|
|
764
|
+
if (this.persister) {
|
|
765
|
+
return this.persister;
|
|
766
|
+
} else {
|
|
767
|
+
this.persister = new CatalogPersister({
|
|
768
|
+
messagesPath: this.config.extract.path,
|
|
769
|
+
codec: await this.getCodec(),
|
|
770
|
+
extension: getFormatExtension(this.config.messages.format)
|
|
771
|
+
});
|
|
772
|
+
return this.persister;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
getCatalogLocales() {
|
|
776
|
+
if (this.catalogLocales) {
|
|
777
|
+
return this.catalogLocales;
|
|
778
|
+
} else {
|
|
779
|
+
const messagesDir = path__default.default.join(this.projectRoot, this.config.extract.path);
|
|
780
|
+
this.catalogLocales = new CatalogLocales({
|
|
781
|
+
messagesDir,
|
|
782
|
+
extension: getFormatExtension(this.config.messages.format),
|
|
783
|
+
locales: this.config.extract.locales,
|
|
784
|
+
sourceLocale: this.config.extract.sourceLocale
|
|
785
|
+
});
|
|
786
|
+
return this.catalogLocales;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
async getTargetLocales() {
|
|
790
|
+
return this.getCatalogLocales().getTargetLocales();
|
|
791
|
+
}
|
|
792
|
+
getSrcPaths() {
|
|
793
|
+
return (Array.isArray(this.config.extract.srcPath) ? this.config.extract.srcPath : [this.config.extract.srcPath]).map(srcPath => path__default.default.join(this.projectRoot, srcPath));
|
|
794
|
+
}
|
|
795
|
+
async loadMessages() {
|
|
796
|
+
const sourceDiskMessages = await this.loadSourceMessages();
|
|
797
|
+
this.loadCatalogsPromise = this.loadTargetMessages();
|
|
798
|
+
await this.loadCatalogsPromise;
|
|
799
|
+
this.scanCompletePromise = (async () => {
|
|
800
|
+
const sourceFiles = Array.from(await SourceFileScanner.getSourceFiles(this.getSrcPaths()))
|
|
801
|
+
// Stable file order keeps catalog ties independent of processing timing
|
|
802
|
+
.toSorted(localeCompare);
|
|
803
|
+
const extractedFiles = await Promise.all(sourceFiles.map(async filePath => ({
|
|
804
|
+
filePath,
|
|
805
|
+
messages: await this.extractFile(filePath)
|
|
806
|
+
})));
|
|
807
|
+
for (const {
|
|
808
|
+
filePath,
|
|
809
|
+
messages
|
|
810
|
+
} of extractedFiles) {
|
|
811
|
+
if (messages) {
|
|
812
|
+
this.applyFileMessages(filePath, messages);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
this.mergeSourceDiskMetadata(sourceDiskMessages);
|
|
816
|
+
})();
|
|
817
|
+
await this.scanCompletePromise;
|
|
818
|
+
if (this.isDevelopment) {
|
|
819
|
+
const catalogLocales = this.getCatalogLocales();
|
|
820
|
+
catalogLocales.subscribeLocalesChange(this.onLocalesChange);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
async loadSourceMessages() {
|
|
824
|
+
// Load source catalog to hydrate metadata (e.g. flags) later without
|
|
825
|
+
// treating catalog entries as source of truth.
|
|
826
|
+
const diskMessages = await this.loadLocaleMessages(this.config.extract.sourceLocale);
|
|
827
|
+
const byId = new Map();
|
|
828
|
+
for (const diskMessage of diskMessages) {
|
|
829
|
+
byId.set(diskMessage.id, diskMessage);
|
|
830
|
+
}
|
|
831
|
+
return byId;
|
|
832
|
+
}
|
|
833
|
+
async loadLocaleMessages(locale) {
|
|
834
|
+
const persister = await this.getPersister();
|
|
835
|
+
const messages = await persister.read(locale);
|
|
836
|
+
const fileTime = await persister.getLastModified(locale);
|
|
837
|
+
this.lastWriteByLocale.set(locale, fileTime);
|
|
838
|
+
return messages;
|
|
839
|
+
}
|
|
840
|
+
async loadTargetMessages() {
|
|
841
|
+
const targetLocales = await this.getTargetLocales();
|
|
842
|
+
await Promise.all(targetLocales.map(locale => this.reloadLocaleCatalog(locale)));
|
|
843
|
+
}
|
|
844
|
+
async reloadLocaleCatalog(locale) {
|
|
845
|
+
const diskMessages = await this.loadLocaleMessages(locale);
|
|
846
|
+
if (locale === this.config.extract.sourceLocale) {
|
|
847
|
+
// For source: Merge additional properties like flags
|
|
848
|
+
for (const diskMessage of diskMessages) {
|
|
849
|
+
const prev = this.messagesById.get(diskMessage.id);
|
|
850
|
+
if (prev) {
|
|
851
|
+
for (const key of Object.keys(diskMessage)) {
|
|
852
|
+
if (!CatalogManager.extractorOwnedAggregatorKeys.has(key)) {
|
|
853
|
+
// For unknown properties (like flags), disk wins
|
|
854
|
+
prev[key] = diskMessage[key];
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
} else {
|
|
860
|
+
// For target: disk wins completely, BUT preserve existing translations
|
|
861
|
+
// if we read empty (likely a write in progress by an external tool
|
|
862
|
+
// that causes the file to temporarily be empty)
|
|
863
|
+
const existingTranslations = this.translationsByTargetLocale.get(locale);
|
|
864
|
+
const hasExistingTranslations = existingTranslations && existingTranslations.size > 0;
|
|
865
|
+
if (diskMessages.length > 0) {
|
|
866
|
+
// We got content from disk, replace with it
|
|
867
|
+
const translations = new Map();
|
|
868
|
+
for (const message of diskMessages) {
|
|
869
|
+
translations.set(message.id, message);
|
|
870
|
+
}
|
|
871
|
+
this.translationsByTargetLocale.set(locale, translations);
|
|
872
|
+
} else if (hasExistingTranslations) ; else {
|
|
873
|
+
// We read empty and have no existing translations
|
|
874
|
+
const translations = new Map();
|
|
875
|
+
this.translationsByTargetLocale.set(locale, translations);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
mergeSourceDiskMetadata(diskMessages) {
|
|
880
|
+
for (const [id, diskMessage] of diskMessages) {
|
|
881
|
+
const existing = this.messagesById.get(id);
|
|
882
|
+
if (!existing) continue;
|
|
883
|
+
|
|
884
|
+
// Fill unknown metadata from disk without replacing extraction-owned fields.
|
|
885
|
+
for (const key of Object.keys(diskMessage)) {
|
|
886
|
+
if (!CatalogManager.extractorOwnedAggregatorKeys.has(key) && existing[key] == null) {
|
|
887
|
+
existing[key] = diskMessage[key];
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
async processFile(absoluteFilePath) {
|
|
893
|
+
const messages = await this.extractFile(absoluteFilePath);
|
|
894
|
+
|
|
895
|
+
// `undefined` only when `extractFile()` throws. An empty array is success
|
|
896
|
+
// and must still run `applyFileMessages` to clear stale ids for this file.
|
|
897
|
+
if (!messages) return false;
|
|
898
|
+
return this.applyFileMessages(absoluteFilePath, messages);
|
|
899
|
+
}
|
|
900
|
+
async extractFile(absoluteFilePath) {
|
|
901
|
+
let messages = [];
|
|
902
|
+
try {
|
|
903
|
+
const content = await fs__default$1.default.readFile(absoluteFilePath, 'utf8');
|
|
904
|
+
let extraction;
|
|
905
|
+
try {
|
|
906
|
+
extraction = await this.extractor.extract(absoluteFilePath, content);
|
|
907
|
+
} catch {
|
|
908
|
+
return undefined;
|
|
909
|
+
}
|
|
910
|
+
messages = extraction.messages;
|
|
911
|
+
} catch (err) {
|
|
912
|
+
if (err.code !== 'ENOENT') {
|
|
913
|
+
throw err;
|
|
914
|
+
}
|
|
915
|
+
// ENOENT -> treat as no messages
|
|
916
|
+
}
|
|
917
|
+
return messages;
|
|
918
|
+
}
|
|
919
|
+
applyFileMessages(absoluteFilePath, messages) {
|
|
920
|
+
const prevFileMessages = this.sourceMessagesByFile.get(absoluteFilePath);
|
|
921
|
+
const nextFileMessages = this.groupSourceMessagesById(messages);
|
|
922
|
+
const affectedIds = new Set([...(prevFileMessages?.keys() ?? []), ...nextFileMessages.keys()]);
|
|
923
|
+
if (nextFileMessages.size > 0) {
|
|
924
|
+
this.sourceMessagesByFile.set(absoluteFilePath, nextFileMessages);
|
|
925
|
+
} else {
|
|
926
|
+
this.sourceMessagesByFile.delete(absoluteFilePath);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Clear this file's contribution from the reverse index, then re-insert
|
|
930
|
+
// fresh rows and rebuild aggregates (messagesById) per touched id.
|
|
931
|
+
for (const id of affectedIds) {
|
|
932
|
+
const sourceMessagesForId = this.sourceMessagesById.get(id);
|
|
933
|
+
if (sourceMessagesForId) {
|
|
934
|
+
sourceMessagesForId.delete(absoluteFilePath);
|
|
935
|
+
// No files left for this id: drop the reverse-index entry.
|
|
936
|
+
if (sourceMessagesForId.size === 0) {
|
|
937
|
+
this.sourceMessagesById.delete(id);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
const nextSourceMessagesForId = nextFileMessages.get(id);
|
|
941
|
+
if (nextSourceMessagesForId) {
|
|
942
|
+
let sourceMessagesByFile = this.sourceMessagesById.get(id);
|
|
943
|
+
if (!sourceMessagesByFile) {
|
|
944
|
+
sourceMessagesByFile = new Map();
|
|
945
|
+
this.sourceMessagesById.set(id, sourceMessagesByFile);
|
|
946
|
+
}
|
|
947
|
+
sourceMessagesByFile.set(absoluteFilePath, nextSourceMessagesForId);
|
|
948
|
+
}
|
|
949
|
+
this.rebuildMessageById(id);
|
|
950
|
+
}
|
|
951
|
+
const changed = this.haveMessagesChangedForFile(prevFileMessages, nextFileMessages);
|
|
952
|
+
return changed;
|
|
953
|
+
}
|
|
954
|
+
groupSourceMessagesById(messages) {
|
|
955
|
+
const result = new Map();
|
|
956
|
+
for (const message of messages) {
|
|
957
|
+
const messagesById = result.get(message.id);
|
|
958
|
+
if (messagesById) {
|
|
959
|
+
messagesById.push(message);
|
|
960
|
+
} else {
|
|
961
|
+
result.set(message.id, [message]);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return result;
|
|
965
|
+
}
|
|
966
|
+
rebuildMessageById(id) {
|
|
967
|
+
const sourceMessages = Array.from(this.sourceMessagesById.get(id)?.values() ?? []).flat();
|
|
968
|
+
if (sourceMessages.length === 0) {
|
|
969
|
+
this.messagesById.delete(id);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const previousMessage = this.messagesById.get(id);
|
|
973
|
+
const aggregate = {
|
|
974
|
+
description: this.mergeDescriptions(sourceMessages),
|
|
975
|
+
id,
|
|
976
|
+
message: sourceMessages[0].message,
|
|
977
|
+
references: sourceMessages.map(message => message.reference).sort(compareReferences)
|
|
978
|
+
};
|
|
979
|
+
if (previousMessage) {
|
|
980
|
+
for (const key of Object.keys(previousMessage)) {
|
|
981
|
+
// Preserve extra fields (e.g. from disk/codec) across rebuilds; the
|
|
982
|
+
// four core fields above are always recomputed from source messages.
|
|
983
|
+
if (!CatalogManager.extractorOwnedAggregatorKeys.has(key) && aggregate[key] == null) {
|
|
984
|
+
aggregate[key] = previousMessage[key];
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
this.messagesById.set(id, aggregate);
|
|
989
|
+
}
|
|
990
|
+
mergeDescriptions(messages) {
|
|
991
|
+
const sortedByReference = messages.toSorted((a, b) => compareReferences(a.reference, b.reference));
|
|
992
|
+
const merged = [];
|
|
993
|
+
for (const message of sortedByReference) {
|
|
994
|
+
const {
|
|
995
|
+
description
|
|
996
|
+
} = message;
|
|
997
|
+
if (description != null && !merged.includes(description)) {
|
|
998
|
+
merged.push(description);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return merged;
|
|
1002
|
+
}
|
|
1003
|
+
haveMessagesChangedForFile(beforeMessages, afterMessages) {
|
|
1004
|
+
// If one exists and the other doesn't, there's a change
|
|
1005
|
+
if (!beforeMessages) {
|
|
1006
|
+
return afterMessages.size > 0;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Different sizes means changes
|
|
1010
|
+
if (beforeMessages.size !== afterMessages.size) {
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Check differences in beforeMessages vs afterMessages
|
|
1015
|
+
for (const [id, prevSourceMessages] of beforeMessages) {
|
|
1016
|
+
const nextSourceMessages = afterMessages.get(id);
|
|
1017
|
+
if (!nextSourceMessages) {
|
|
1018
|
+
return true;
|
|
1019
|
+
}
|
|
1020
|
+
if (!this.areSourceMessageArraysEqual(prevSourceMessages, nextSourceMessages)) {
|
|
1021
|
+
return true; // Early exit on first difference
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
areSourceMessageArraysEqual(messages1, messages2) {
|
|
1027
|
+
return messages1.length === messages2.length && messages1.every((message, index) => this.areSourceMessagesEqual(message, messages2[index]));
|
|
1028
|
+
}
|
|
1029
|
+
areSourceMessagesEqual(msg1, msg2) {
|
|
1030
|
+
return msg1.id === msg2.id && msg1.message === msg2.message && msg1.description === msg2.description && msg1.reference.path === msg2.reference.path && msg1.reference.line === msg2.reference.line;
|
|
1031
|
+
}
|
|
1032
|
+
async save() {
|
|
1033
|
+
return this.saveScheduler.schedule(() => this.saveImpl());
|
|
1034
|
+
}
|
|
1035
|
+
async saveImpl() {
|
|
1036
|
+
await this.saveLocale(this.config.extract.sourceLocale);
|
|
1037
|
+
const targetLocales = await this.getTargetLocales();
|
|
1038
|
+
await Promise.all(targetLocales.map(locale => this.saveLocale(locale)));
|
|
1039
|
+
}
|
|
1040
|
+
async saveLocale(locale) {
|
|
1041
|
+
await this.loadCatalogsPromise;
|
|
1042
|
+
const messages = Array.from(this.messagesById.values());
|
|
1043
|
+
const persister = await this.getPersister();
|
|
1044
|
+
const isSourceLocale = locale === this.config.extract.sourceLocale;
|
|
1045
|
+
|
|
1046
|
+
// Check if file was modified externally (poll-at-save is cheaper than
|
|
1047
|
+
// watchers here since stat() is fast and avoids continuous overhead)
|
|
1048
|
+
const lastWriteTime = this.lastWriteByLocale.get(locale);
|
|
1049
|
+
const currentFileTime = await persister.getLastModified(locale);
|
|
1050
|
+
if (currentFileTime && lastWriteTime && currentFileTime > lastWriteTime) {
|
|
1051
|
+
await this.reloadLocaleCatalog(locale);
|
|
1052
|
+
}
|
|
1053
|
+
const localeMessages = isSourceLocale ? this.messagesById : this.translationsByTargetLocale.get(locale);
|
|
1054
|
+
const messagesToPersist = messages.map(message => {
|
|
1055
|
+
const localeMessage = localeMessages?.get(message.id);
|
|
1056
|
+
return {
|
|
1057
|
+
...localeMessage,
|
|
1058
|
+
id: message.id,
|
|
1059
|
+
description: message.description,
|
|
1060
|
+
references: message.references,
|
|
1061
|
+
message: isSourceLocale ? message.message : localeMessage?.message ?? ''
|
|
1062
|
+
};
|
|
1063
|
+
});
|
|
1064
|
+
await persister.write(messagesToPersist, {
|
|
1065
|
+
locale,
|
|
1066
|
+
sourceMessagesById: this.messagesById
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
// Update timestamps
|
|
1070
|
+
const newTime = await persister.getLastModified(locale);
|
|
1071
|
+
this.lastWriteByLocale.set(locale, newTime);
|
|
1072
|
+
}
|
|
1073
|
+
onLocalesChange = async params => {
|
|
1074
|
+
// Chain to existing promise
|
|
1075
|
+
this.loadCatalogsPromise = Promise.all([this.loadCatalogsPromise, ...params.added.map(locale => this.reloadLocaleCatalog(locale))]);
|
|
1076
|
+
for (const locale of params.added) {
|
|
1077
|
+
await this.saveLocale(locale);
|
|
1078
|
+
}
|
|
1079
|
+
for (const locale of params.removed) {
|
|
1080
|
+
this.translationsByTargetLocale.delete(locale);
|
|
1081
|
+
this.lastWriteByLocale.delete(locale);
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
async handleFileEvents(events) {
|
|
1085
|
+
if (this.loadCatalogsPromise) {
|
|
1086
|
+
await this.loadCatalogsPromise;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Wait for initial scan to complete to avoid race conditions
|
|
1090
|
+
if (this.scanCompletePromise) {
|
|
1091
|
+
await this.scanCompletePromise;
|
|
1092
|
+
}
|
|
1093
|
+
let changed = false;
|
|
1094
|
+
const expandedEvents = await this.sourceWatcher.expandDirectoryDeleteEvents(events, Array.from(this.sourceMessagesByFile.keys()));
|
|
1095
|
+
// Stable file order keeps catalog ties independent of event timing.
|
|
1096
|
+
for (const event of expandedEvents.toSorted((a, b) => localeCompare(a.path, b.path))) {
|
|
1097
|
+
const hasChanged = await this.processFile(event.path);
|
|
1098
|
+
changed ||= hasChanged;
|
|
1099
|
+
}
|
|
1100
|
+
if (changed) {
|
|
1101
|
+
await this.save();
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
[Symbol.dispose]() {
|
|
1105
|
+
this.sourceWatcher?.stop();
|
|
1106
|
+
this.sourceWatcher = undefined;
|
|
1107
|
+
this.saveScheduler[Symbol.dispose]();
|
|
1108
|
+
if (this.catalogLocales && this.isDevelopment) {
|
|
1109
|
+
this.catalogLocales.unsubscribeLocalesChange(this.onLocalesChange);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
class LRUCache {
|
|
1115
|
+
constructor(maxSize) {
|
|
1116
|
+
this.maxSize = maxSize;
|
|
1117
|
+
this.cache = new Map();
|
|
1118
|
+
}
|
|
1119
|
+
set(key, value) {
|
|
1120
|
+
const isNewKey = !this.cache.has(key);
|
|
1121
|
+
if (isNewKey && this.cache.size >= this.maxSize) {
|
|
1122
|
+
const lruKey = this.cache.keys().next().value;
|
|
1123
|
+
if (lruKey !== undefined) {
|
|
1124
|
+
this.cache.delete(lruKey);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
this.cache.set(key, {
|
|
1128
|
+
key,
|
|
1129
|
+
value
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
get(key) {
|
|
1133
|
+
const item = this.cache.get(key);
|
|
1134
|
+
if (item) {
|
|
1135
|
+
this.cache.delete(key);
|
|
1136
|
+
this.cache.set(key, item);
|
|
1137
|
+
return item.value;
|
|
1138
|
+
}
|
|
1139
|
+
return undefined;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const require$2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('plugin-DlFYUFWh.cjs', document.baseURI).href)));
|
|
1144
|
+
class MessageExtractor {
|
|
1145
|
+
compileCache = new LRUCache(750);
|
|
1146
|
+
constructor(opts) {
|
|
1147
|
+
this.isDevelopment = opts.isDevelopment ?? false;
|
|
1148
|
+
this.projectRoot = opts.projectRoot ?? getDefaultProjectRoot();
|
|
1149
|
+
this.sourceMap = opts.sourceMap ?? false;
|
|
1150
|
+
}
|
|
1151
|
+
async extract(absoluteFilePath, source) {
|
|
1152
|
+
const cacheKey = [source, absoluteFilePath].join('!');
|
|
1153
|
+
const cached = this.compileCache.get(cacheKey);
|
|
1154
|
+
if (cached) return cached;
|
|
1155
|
+
|
|
1156
|
+
// Shortcut parsing if hook is not used. The Turbopack integration already
|
|
1157
|
+
// pre-filters this, but for webpack this feature doesn't exist, so we need
|
|
1158
|
+
// to do it here.
|
|
1159
|
+
if (!source.includes('useExtracted') && !source.includes('getExtracted')) {
|
|
1160
|
+
return {
|
|
1161
|
+
messages: [],
|
|
1162
|
+
code: source
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
const filePath = normalizePathToPosix(path__default.default.relative(this.projectRoot, absoluteFilePath));
|
|
1166
|
+
const result = await core.transform(source, {
|
|
1167
|
+
jsc: {
|
|
1168
|
+
target: 'esnext',
|
|
1169
|
+
parser: {
|
|
1170
|
+
syntax: 'typescript',
|
|
1171
|
+
tsx: true,
|
|
1172
|
+
decorators: true
|
|
1173
|
+
},
|
|
1174
|
+
experimental: {
|
|
1175
|
+
cacheRoot: 'node_modules/.cache/swc',
|
|
1176
|
+
disableBuiltinTransformsForInternalTesting: true,
|
|
1177
|
+
disableAllLints: true,
|
|
1178
|
+
plugins: [[require$2.resolve('next-intl-swc-plugin-extractor'), {
|
|
1179
|
+
isDevelopment: this.isDevelopment,
|
|
1180
|
+
filePath
|
|
1181
|
+
}]]
|
|
1182
|
+
}
|
|
1183
|
+
},
|
|
1184
|
+
sourceMaps: this.sourceMap,
|
|
1185
|
+
sourceFileName: filePath,
|
|
1186
|
+
filename: filePath
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
// TODO: Improve the typing of @swc/core
|
|
1190
|
+
const output = result.output;
|
|
1191
|
+
const messages = JSON.parse(JSON.parse(output).results);
|
|
1192
|
+
const extractionResult = {
|
|
1193
|
+
code: result.code,
|
|
1194
|
+
map: result.map,
|
|
1195
|
+
messages
|
|
1196
|
+
};
|
|
1197
|
+
this.compileCache.set(cacheKey, extractionResult);
|
|
1198
|
+
return extractionResult;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
class ExtractionCompiler {
|
|
1203
|
+
constructor(config, opts = {}) {
|
|
1204
|
+
const extractor = opts.extractor ?? new MessageExtractor(opts);
|
|
1205
|
+
this.manager = new CatalogManager(config, {
|
|
1206
|
+
...opts,
|
|
1207
|
+
extractor
|
|
1208
|
+
});
|
|
1209
|
+
this[Symbol.dispose] = this[Symbol.dispose].bind(this);
|
|
1210
|
+
this.installExitHandlers();
|
|
1211
|
+
}
|
|
1212
|
+
async extractAll() {
|
|
1213
|
+
// We can't rely on all files being compiled (e.g. due to persistent
|
|
1214
|
+
// caching), so loading the messages initially is necessary.
|
|
1215
|
+
await this.manager.loadMessages();
|
|
1216
|
+
await this.manager.save();
|
|
1217
|
+
}
|
|
1218
|
+
[Symbol.dispose]() {
|
|
1219
|
+
this.uninstallExitHandlers();
|
|
1220
|
+
this.manager[Symbol.dispose]();
|
|
1221
|
+
}
|
|
1222
|
+
installExitHandlers() {
|
|
1223
|
+
const cleanup = this[Symbol.dispose];
|
|
1224
|
+
process.on('exit', cleanup);
|
|
1225
|
+
process.on('SIGINT', cleanup);
|
|
1226
|
+
process.on('SIGTERM', cleanup);
|
|
1227
|
+
}
|
|
1228
|
+
uninstallExitHandlers() {
|
|
1229
|
+
const cleanup = this[Symbol.dispose];
|
|
1230
|
+
process.off('exit', cleanup);
|
|
1231
|
+
process.off('SIGINT', cleanup);
|
|
1232
|
+
process.off('SIGTERM', cleanup);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// Avoid rollup's `replace` plugin to compile this away
|
|
1237
|
+
const nodeEnvKey = 'NODE_ENV'.trim();
|
|
1238
|
+
|
|
1239
|
+
// We avoid reading `argv.includes('dev')` related to
|
|
1240
|
+
// https://github.com/amannn/next-intl/issues/2006
|
|
1241
|
+
const isDevelopment = process.env[nodeEnvKey] === 'development';
|
|
1242
|
+
const isNextBuild = process.argv.includes('build');
|
|
1243
|
+
const isDevelopmentOrNextBuild = isDevelopment || isNextBuild;
|
|
1244
|
+
|
|
1245
|
+
// Single compiler instance, initialized once per process
|
|
1246
|
+
let compiler;
|
|
1247
|
+
const runOnce = once('_NEXT_INTL_EXTRACT');
|
|
1248
|
+
function initExtractionCompiler(extractorConfig) {
|
|
1249
|
+
if (!extractorConfig || !hasLocalesToExtract(extractorConfig)) {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Avoid running for:
|
|
1254
|
+
// - info
|
|
1255
|
+
// - start
|
|
1256
|
+
// - typegen
|
|
1257
|
+
//
|
|
1258
|
+
// Doesn't consult Next.js config anyway:
|
|
1259
|
+
// - lint
|
|
1260
|
+
// - telemetry (however, the `detached-flush` DOES - see `createNextIntlPlugin`)
|
|
1261
|
+
//
|
|
1262
|
+
// What remains are:
|
|
1263
|
+
// - dev (NODE_ENV=development)
|
|
1264
|
+
// - build (NODE_ENV=production)
|
|
1265
|
+
const shouldRun = isDevelopment || isNextBuild;
|
|
1266
|
+
if (!shouldRun) return;
|
|
1267
|
+
runOnce(() => {
|
|
1268
|
+
compiler = new ExtractionCompiler(extractorConfig, {
|
|
1269
|
+
isDevelopment,
|
|
1270
|
+
projectRoot: process.cwd()
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
// Fire-and-forget: Start extraction, don't block config return.
|
|
1274
|
+
// In dev mode, this also starts the file watcher.
|
|
1275
|
+
// In prod, ideally we would wait until the extraction is complete,
|
|
1276
|
+
// but we can't `await` anywhere (at least for Turbopack).
|
|
1277
|
+
// The result is ok though, as if we encounter untranslated messages,
|
|
1278
|
+
// we'll simply add empty messages to the catalog. So for actually
|
|
1279
|
+
// running the app, there is no difference.
|
|
1280
|
+
compiler.extractAll();
|
|
1281
|
+
function cleanup() {
|
|
1282
|
+
if (compiler) {
|
|
1283
|
+
compiler[Symbol.dispose]();
|
|
1284
|
+
compiler = undefined;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
process.on('exit', cleanup);
|
|
1288
|
+
process.on('SIGINT', cleanup);
|
|
1289
|
+
process.on('SIGTERM', cleanup);
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function getCurrentVersion() {
|
|
1294
|
+
try {
|
|
1295
|
+
const require$1 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('plugin-DlFYUFWh.cjs', document.baseURI).href)));
|
|
1296
|
+
const pkg = require$1('next/package.json');
|
|
1297
|
+
return pkg.version;
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
throw new Error('Failed to get current Next.js version. This can happen if next-intl/plugin is imported into your app code outside of your next.config.js.', {
|
|
1300
|
+
cause: error
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
function compareVersions(version1, version2) {
|
|
1305
|
+
const v1Parts = version1.split('.').map(Number);
|
|
1306
|
+
const v2Parts = version2.split('.').map(Number);
|
|
1307
|
+
for (let i = 0; i < 3; i++) {
|
|
1308
|
+
const v1 = v1Parts[i] || 0;
|
|
1309
|
+
const v2 = v2Parts[i] || 0;
|
|
1310
|
+
if (v1 > v2) return 1;
|
|
1311
|
+
if (v1 < v2) return -1;
|
|
1312
|
+
}
|
|
1313
|
+
return 0;
|
|
1314
|
+
}
|
|
1315
|
+
function hasStableTurboConfig() {
|
|
1316
|
+
return compareVersions(getCurrentVersion(), '15.3.0') >= 0;
|
|
1317
|
+
}
|
|
1318
|
+
function isNextJs16OrHigher() {
|
|
1319
|
+
return compareVersions(getCurrentVersion(), '16.0.0') >= 0;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const require$1 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('plugin-DlFYUFWh.cjs', document.baseURI).href)));
|
|
1323
|
+
function withExtensions(localPath) {
|
|
1324
|
+
return [`${localPath}.ts`, `${localPath}.tsx`, `${localPath}.js`, `${localPath}.jsx`];
|
|
1325
|
+
}
|
|
1326
|
+
function normalizeTurbopackAliasPath(pathname) {
|
|
1327
|
+
// Turbopack alias targets should use forward slashes; Windows backslashes can
|
|
1328
|
+
// break resolution in dev (see `next-intl/config` alias path style).
|
|
1329
|
+
return pathname.replace(/\\/g, '/');
|
|
1330
|
+
}
|
|
1331
|
+
function resolveI18nPath(providedPath, cwd) {
|
|
1332
|
+
function resolvePath(pathname) {
|
|
1333
|
+
const parts = [];
|
|
1334
|
+
if (cwd) parts.push(cwd);
|
|
1335
|
+
parts.push(pathname);
|
|
1336
|
+
return path__default.default.resolve(...parts);
|
|
1337
|
+
}
|
|
1338
|
+
function pathExists(pathname) {
|
|
1339
|
+
return fs__default.default.existsSync(resolvePath(pathname));
|
|
1340
|
+
}
|
|
1341
|
+
if (providedPath) {
|
|
1342
|
+
// We use the `isNextDevOrBuild` condition to avoid throwing errors
|
|
1343
|
+
// if `next.config.ts` is read by a non-Next.js process.
|
|
1344
|
+
// https://github.com/amannn/next-intl/discussions/2209#discussioncomment-15650927
|
|
1345
|
+
if (isDevelopmentOrNextBuild && !pathExists(providedPath)) {
|
|
1346
|
+
throwError(`Could not find i18n config at ${providedPath}, please provide a valid path.`);
|
|
1347
|
+
}
|
|
1348
|
+
return providedPath;
|
|
1349
|
+
} else {
|
|
1350
|
+
for (const candidate of [...withExtensions('./i18n/request'), ...withExtensions('./src/i18n/request')]) {
|
|
1351
|
+
if (pathExists(candidate)) {
|
|
1352
|
+
return candidate;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
if (isDevelopmentOrNextBuild) {
|
|
1356
|
+
throwError(`Could not locate request configuration module.\n\nThis path is supported by default: ./(src/)i18n/request.{js,jsx,ts,tsx}\n\nAlternatively, you can specify a custom location in your Next.js config:\n\nconst withNextIntl = createNextIntlPlugin(\n './path/to/i18n/request.tsx'\n);`);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Default as fallback
|
|
1360
|
+
if (pathExists('./src')) {
|
|
1361
|
+
return './src/i18n/request.ts';
|
|
1362
|
+
} else {
|
|
1363
|
+
return './i18n/request.ts';
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function getNextConfig(pluginConfig, nextConfig, extractorConfig) {
|
|
1368
|
+
const useTurbo = process.env.TURBOPACK != null;
|
|
1369
|
+
|
|
1370
|
+
// `experimental-analyze` doesn’t set the TURBOPACK env param. Since Next.js
|
|
1371
|
+
// 16 doesn't print a warning when we configure both Turbo- and Webpack, just
|
|
1372
|
+
// always configure Turbopack just in case.
|
|
1373
|
+
const shouldConfigureTurbo = useTurbo || isNextJs16OrHigher();
|
|
1374
|
+
const nextIntlConfig = {};
|
|
1375
|
+
let messageLoadPaths = [];
|
|
1376
|
+
if (pluginConfig.experimental?.messages) {
|
|
1377
|
+
messageLoadPaths = normalizeMessagesCatalogPaths(pluginConfig.experimental.messages.path);
|
|
1378
|
+
}
|
|
1379
|
+
function getExtractMessagesLoaderConfig(config) {
|
|
1380
|
+
return {
|
|
1381
|
+
loader: 'next-intl/extractor/extractionLoader',
|
|
1382
|
+
options: config
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
function getCatalogLoaderConfig() {
|
|
1386
|
+
const messages = pluginConfig.experimental.messages;
|
|
1387
|
+
return {
|
|
1388
|
+
loader: 'next-intl/extractor/catalogLoader',
|
|
1389
|
+
options: {
|
|
1390
|
+
messages: {
|
|
1391
|
+
format: messages.format,
|
|
1392
|
+
...(messages.precompile !== undefined && {
|
|
1393
|
+
precompile: messages.precompile
|
|
1394
|
+
})
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
function getTurboRules() {
|
|
1400
|
+
return nextConfig?.turbopack?.rules ||
|
|
1401
|
+
// @ts-expect-error -- For Next.js <16
|
|
1402
|
+
nextConfig?.experimental?.turbo?.rules || {};
|
|
1403
|
+
}
|
|
1404
|
+
function addTurboRule(rules, glob, rule) {
|
|
1405
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1406
|
+
if (rules[glob]) {
|
|
1407
|
+
if (Array.isArray(rules[glob])) {
|
|
1408
|
+
rules[glob].push(rule);
|
|
1409
|
+
} else {
|
|
1410
|
+
rules[glob] = [rules[glob], rule];
|
|
1411
|
+
}
|
|
1412
|
+
} else {
|
|
1413
|
+
rules[glob] = rule;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if (shouldConfigureTurbo) {
|
|
1417
|
+
if (pluginConfig.requestConfig && path__default.default.isAbsolute(pluginConfig.requestConfig)) {
|
|
1418
|
+
throwError("Turbopack support for next-intl currently does not support absolute paths, please provide a relative one (e.g. './src/i18n/config.ts').\n\nFound: " + pluginConfig.requestConfig);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// Assign alias for `next-intl/config`
|
|
1422
|
+
const resolveAlias = {
|
|
1423
|
+
// Turbo aliases don't work with absolute
|
|
1424
|
+
// paths (see error handling above)
|
|
1425
|
+
'next-intl/config': resolveI18nPath(pluginConfig.requestConfig)
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
// Add alias for precompiled message formatting
|
|
1429
|
+
if (pluginConfig.experimental?.messages?.precompile) {
|
|
1430
|
+
// Workaround for https://github.com/vercel/next.js/issues/88540
|
|
1431
|
+
let formatOnlyPath = path__default.default.relative(process.cwd(), require$1.resolve('use-intl/format-message/format-only'));
|
|
1432
|
+
|
|
1433
|
+
// Turbopack seems to require this, otherwise `use-intl/format-message` is
|
|
1434
|
+
// still bundled (despite the code correctly calling into `format-only`).
|
|
1435
|
+
// Note that in this monorepo this is not necessary, because we'll end
|
|
1436
|
+
// up with a path like `../…` — but for actual consumers this is required.
|
|
1437
|
+
if (!formatOnlyPath.startsWith('.')) {
|
|
1438
|
+
formatOnlyPath = `./${formatOnlyPath}`;
|
|
1439
|
+
}
|
|
1440
|
+
resolveAlias['use-intl/format-message'] = normalizeTurbopackAliasPath(formatOnlyPath);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Add loaders
|
|
1444
|
+
let rules;
|
|
1445
|
+
|
|
1446
|
+
// Add loader for extractor
|
|
1447
|
+
if (pluginConfig.experimental?.extract) {
|
|
1448
|
+
if (!isNextJs16OrHigher()) {
|
|
1449
|
+
throwError('Message extraction requires Next.js 16 or higher.');
|
|
1450
|
+
}
|
|
1451
|
+
rules ??= getTurboRules();
|
|
1452
|
+
addTurboRule(rules, `*.{${SourceFileFilter.EXTENSIONS.join(',')}}`, {
|
|
1453
|
+
loaders: [getExtractMessagesLoaderConfig(extractorConfig)],
|
|
1454
|
+
condition: {
|
|
1455
|
+
// We don't filter for `path` here to allow transformation
|
|
1456
|
+
// of `useExtracted` calls in external packages (e.g. monorepos)
|
|
1457
|
+
content: /(useExtracted|getExtracted)/
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Add loader for catalog
|
|
1463
|
+
if (pluginConfig.experimental?.messages) {
|
|
1464
|
+
if (!isNextJs16OrHigher()) {
|
|
1465
|
+
throwError('Message catalog loading requires Next.js 16 or higher.');
|
|
1466
|
+
}
|
|
1467
|
+
rules ??= getTurboRules();
|
|
1468
|
+
const extension = getFormatExtension(pluginConfig.experimental.messages.format);
|
|
1469
|
+
addTurboRule(rules, `*${extension}`, {
|
|
1470
|
+
loaders: [getCatalogLoaderConfig()],
|
|
1471
|
+
condition: {
|
|
1472
|
+
path: `{${messageLoadPaths.join(',')}}/**/*`
|
|
1473
|
+
},
|
|
1474
|
+
as: '*.js'
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
if (hasStableTurboConfig() &&
|
|
1478
|
+
// @ts-expect-error -- For Next.js <16
|
|
1479
|
+
!nextConfig?.experimental?.turbo) {
|
|
1480
|
+
nextIntlConfig.turbopack = {
|
|
1481
|
+
...nextConfig?.turbopack,
|
|
1482
|
+
...(rules && {
|
|
1483
|
+
rules
|
|
1484
|
+
}),
|
|
1485
|
+
resolveAlias: {
|
|
1486
|
+
...nextConfig?.turbopack?.resolveAlias,
|
|
1487
|
+
...resolveAlias
|
|
1488
|
+
}
|
|
1489
|
+
};
|
|
1490
|
+
} else {
|
|
1491
|
+
nextIntlConfig.experimental = {
|
|
1492
|
+
...nextConfig?.experimental,
|
|
1493
|
+
// @ts-expect-error -- For Next.js <16
|
|
1494
|
+
turbo: {
|
|
1495
|
+
// @ts-expect-error -- For Next.js <16
|
|
1496
|
+
...nextConfig?.experimental?.turbo,
|
|
1497
|
+
...(rules && {
|
|
1498
|
+
rules
|
|
1499
|
+
}),
|
|
1500
|
+
resolveAlias: {
|
|
1501
|
+
// @ts-expect-error -- For Next.js <16
|
|
1502
|
+
...nextConfig?.experimental?.turbo?.resolveAlias,
|
|
1503
|
+
...resolveAlias
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (!useTurbo) {
|
|
1510
|
+
nextIntlConfig.webpack = function webpack(config, context) {
|
|
1511
|
+
if (!config.resolve) config.resolve = {};
|
|
1512
|
+
if (!config.resolve.alias) config.resolve.alias = {};
|
|
1513
|
+
|
|
1514
|
+
// Assign alias for `next-intl/config`
|
|
1515
|
+
// (Webpack requires absolute paths)
|
|
1516
|
+
config.resolve.alias['next-intl/config'] = path__default.default.resolve(config.context, resolveI18nPath(pluginConfig.requestConfig, config.context));
|
|
1517
|
+
|
|
1518
|
+
// Add alias for precompiled message formatting
|
|
1519
|
+
if (pluginConfig.experimental?.messages?.precompile) {
|
|
1520
|
+
// Use require.resolve to get the actual file path, since
|
|
1521
|
+
// bundlers don't properly resolve package subpath exports
|
|
1522
|
+
// when used as alias targets
|
|
1523
|
+
config.resolve.alias['use-intl/format-message'] = require$1.resolve('use-intl/format-message/format-only');
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Add loader for extractor
|
|
1527
|
+
if (pluginConfig.experimental?.extract) {
|
|
1528
|
+
if (!config.module) config.module = {};
|
|
1529
|
+
if (!config.module.rules) config.module.rules = [];
|
|
1530
|
+
config.module.rules.push({
|
|
1531
|
+
test: new RegExp(`\\.(${SourceFileFilter.EXTENSIONS.join('|')})$`),
|
|
1532
|
+
use: [getExtractMessagesLoaderConfig(extractorConfig)]
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Add loader for catalog
|
|
1537
|
+
if (pluginConfig.experimental?.messages) {
|
|
1538
|
+
if (!config.module) config.module = {};
|
|
1539
|
+
if (!config.module.rules) config.module.rules = [];
|
|
1540
|
+
const extension = getFormatExtension(pluginConfig.experimental.messages.format);
|
|
1541
|
+
config.module.rules.push({
|
|
1542
|
+
test: new RegExp(`${extension.replace(/\./g, '\\.')}$`),
|
|
1543
|
+
include: messageLoadPaths.map(dirPath => path__default.default.resolve(config.context, dirPath)),
|
|
1544
|
+
use: [getCatalogLoaderConfig()],
|
|
1545
|
+
type: 'javascript/auto'
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
if (typeof nextConfig?.webpack === 'function') {
|
|
1549
|
+
return nextConfig.webpack(config, context);
|
|
1550
|
+
}
|
|
1551
|
+
return config;
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// Forward config
|
|
1556
|
+
if (nextConfig?.trailingSlash) {
|
|
1557
|
+
nextIntlConfig.env = {
|
|
1558
|
+
...nextConfig.env,
|
|
1559
|
+
_next_intl_trailing_slash: 'true'
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
return Object.assign({}, nextConfig, nextIntlConfig);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function initPlugin(pluginConfig, nextConfig) {
|
|
1566
|
+
if (nextConfig?.i18n != null) {
|
|
1567
|
+
warn("An `i18n` property was found in your Next.js config. This likely causes conflicts and should therefore be removed if you use the App Router.\n\nIf you're in progress of migrating from the Pages Router, you can refer to this example: https://next-intl.dev/examples#app-router-migration\n");
|
|
1568
|
+
}
|
|
1569
|
+
const skipWatchers = isNextTelemetryDetachedFlushProcess();
|
|
1570
|
+
const messagesPathOrPaths = pluginConfig.experimental?.createMessagesDeclaration;
|
|
1571
|
+
if (messagesPathOrPaths && !skipWatchers) {
|
|
1572
|
+
createMessagesDeclaration(typeof messagesPathOrPaths === 'string' ? [messagesPathOrPaths] : messagesPathOrPaths);
|
|
1573
|
+
}
|
|
1574
|
+
let extractorConfig;
|
|
1575
|
+
const experimental = pluginConfig.experimental;
|
|
1576
|
+
const extract = experimental?.extract;
|
|
1577
|
+
if (extract) {
|
|
1578
|
+
extractorConfig = normalizeExtractorConfig({
|
|
1579
|
+
extract,
|
|
1580
|
+
messages: experimental.messages,
|
|
1581
|
+
srcPath: experimental.srcPath
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
if (!skipWatchers) {
|
|
1585
|
+
initExtractionCompiler(extractorConfig);
|
|
1586
|
+
}
|
|
1587
|
+
return getNextConfig(pluginConfig, nextConfig, extractorConfig);
|
|
1588
|
+
}
|
|
1589
|
+
function createNextIntlPlugin(i18nPathOrConfig = {}) {
|
|
1590
|
+
const config = typeof i18nPathOrConfig === 'string' ? {
|
|
1591
|
+
requestConfig: i18nPathOrConfig
|
|
1592
|
+
} : i18nPathOrConfig;
|
|
1593
|
+
return function withNextIntl(nextConfig) {
|
|
1594
|
+
return initPlugin(config, nextConfig);
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* Next runs `telemetry/detached-flush.js` in a detached process to flush telemetry
|
|
1600
|
+
* (often when `next dev` exits). That loads dev `next.config` with inherited
|
|
1601
|
+
* `NODE_ENV=development`, which would otherwise start orphan plugin watchers.
|
|
1602
|
+
*/
|
|
1603
|
+
function isNextTelemetryDetachedFlushProcess() {
|
|
1604
|
+
const scriptPath = process.argv[1];
|
|
1605
|
+
if (!scriptPath) return false;
|
|
1606
|
+
const normalized = scriptPath.replace(/\\/g, '/');
|
|
1607
|
+
return normalized.includes('/telemetry/detached-flush');
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
exports.createNextIntlPlugin = createNextIntlPlugin;
|
|
1611
|
+
exports.getSortedMessages = getSortedMessages;
|
|
1612
|
+
exports.isForbiddenObjectKey = isForbiddenObjectKey;
|
|
1613
|
+
exports.setNestedProperty = setNestedProperty;
|