@depup/next-intl 4.8.3-depup.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 +21 -0
- package/README.md +33 -0
- package/changes.json +18 -0
- package/config.d.ts +4 -0
- package/dist/cjs/development/ExtractorCodec-D9Tw618d.cjs +7 -0
- package/dist/cjs/development/JSONCodec-rhejs716.cjs +50 -0
- package/dist/cjs/development/POCodec-Cv3VdXQG.cjs +105 -0
- package/dist/cjs/development/plugin-YzBhQjAo.cjs +1457 -0
- package/dist/cjs/development/plugin.cjs +13 -0
- package/dist/esm/development/config.js +5 -0
- package/dist/esm/development/extractor/ExtractionCompiler.js +38 -0
- package/dist/esm/development/extractor/catalog/CatalogLocales.js +84 -0
- package/dist/esm/development/extractor/catalog/CatalogManager.js +370 -0
- package/dist/esm/development/extractor/catalog/CatalogPersister.js +63 -0
- package/dist/esm/development/extractor/catalog/SaveScheduler.js +85 -0
- package/dist/esm/development/extractor/catalogLoader.js +96 -0
- package/dist/esm/development/extractor/extractMessages.js +15 -0
- package/dist/esm/development/extractor/extractionLoader.js +26 -0
- package/dist/esm/development/extractor/extractor/LRUCache.js +30 -0
- package/dist/esm/development/extractor/extractor/MessageExtractor.js +66 -0
- package/dist/esm/development/extractor/format/ExtractorCodec.js +5 -0
- package/dist/esm/development/extractor/format/codecs/JSONCodec.js +42 -0
- package/dist/esm/development/extractor/format/codecs/POCodec.js +93 -0
- package/dist/esm/development/extractor/format/index.js +44 -0
- package/dist/esm/development/extractor/source/SourceFileFilter.js +29 -0
- package/dist/esm/development/extractor/source/SourceFileScanner.js +31 -0
- package/dist/esm/development/extractor/source/SourceFileWatcher.js +132 -0
- package/dist/esm/development/extractor/utils.js +46 -0
- package/dist/esm/development/extractor.js +2 -0
- package/dist/esm/development/index.react-client.js +4 -0
- package/dist/esm/development/index.react-server.js +9 -0
- package/dist/esm/development/middleware/getAlternateLinksHeaderValue.js +91 -0
- package/dist/esm/development/middleware/middleware.js +168 -0
- package/dist/esm/development/middleware/resolveLocale.js +129 -0
- package/dist/esm/development/middleware/syncCookie.js +27 -0
- package/dist/esm/development/middleware/utils.js +194 -0
- package/dist/esm/development/middleware.js +1 -0
- package/dist/esm/development/navigation/react-client/createNavigation.js +80 -0
- package/dist/esm/development/navigation/react-client/useBasePathname.js +36 -0
- package/dist/esm/development/navigation/react-server/createNavigation.js +22 -0
- package/dist/esm/development/navigation/react-server/getServerLocale.js +12 -0
- package/dist/esm/development/navigation/shared/BaseLink.js +51 -0
- package/dist/esm/development/navigation/shared/createSharedNavigationFns.js +105 -0
- package/dist/esm/development/navigation/shared/syncLocaleCookie.js +47 -0
- package/dist/esm/development/navigation/shared/utils.js +165 -0
- package/dist/esm/development/navigation.react-client.js +1 -0
- package/dist/esm/development/navigation.react-server.js +1 -0
- package/dist/esm/development/plugin/config.js +10 -0
- package/dist/esm/development/plugin/createNextIntlPlugin.js +26 -0
- package/dist/esm/development/plugin/declaration/createMessagesDeclaration.js +70 -0
- package/dist/esm/development/plugin/extractor/initExtractionCompiler.js +59 -0
- package/dist/esm/development/plugin/getNextConfig.js +267 -0
- package/dist/esm/development/plugin/nextFlags.js +32 -0
- package/dist/esm/development/plugin/utils.js +26 -0
- package/dist/esm/development/plugin/watchFile.js +21 -0
- package/dist/esm/development/plugin.js +1 -0
- package/dist/esm/development/react-client/index.js +34 -0
- package/dist/esm/development/react-server/NextIntlClientProviderServer.js +34 -0
- package/dist/esm/development/react-server/useConfig.js +21 -0
- package/dist/esm/development/react-server/useExtracted.js +9 -0
- package/dist/esm/development/react-server/useFormatter.js +9 -0
- package/dist/esm/development/react-server/useLocale.js +8 -0
- package/dist/esm/development/react-server/useMessages.js +9 -0
- package/dist/esm/development/react-server/useNow.js +12 -0
- package/dist/esm/development/react-server/useTimeZone.js +8 -0
- package/dist/esm/development/react-server/useTranslations.js +9 -0
- package/dist/esm/development/routing/config.js +26 -0
- package/dist/esm/development/routing/defineRouting.js +25 -0
- package/dist/esm/development/routing.js +1 -0
- package/dist/esm/development/server/react-client/index.js +32 -0
- package/dist/esm/development/server/react-server/RequestLocale.js +36 -0
- package/dist/esm/development/server/react-server/RequestLocaleCache.js +18 -0
- package/dist/esm/development/server/react-server/createRequestConfig.js +2 -0
- package/dist/esm/development/server/react-server/getConfig.js +59 -0
- package/dist/esm/development/server/react-server/getConfigNow.js +10 -0
- package/dist/esm/development/server/react-server/getDefaultNow.js +9 -0
- package/dist/esm/development/server/react-server/getExtracted.js +24 -0
- package/dist/esm/development/server/react-server/getFormats.js +10 -0
- package/dist/esm/development/server/react-server/getFormatter.js +21 -0
- package/dist/esm/development/server/react-server/getLocale.js +10 -0
- package/dist/esm/development/server/react-server/getMessages.js +19 -0
- package/dist/esm/development/server/react-server/getNow.js +8 -0
- package/dist/esm/development/server/react-server/getRequestConfig.js +8 -0
- package/dist/esm/development/server/react-server/getServerExtractor.js +38 -0
- package/dist/esm/development/server/react-server/getServerFormatter.js +17 -0
- package/dist/esm/development/server/react-server/getServerTranslator.js +12 -0
- package/dist/esm/development/server/react-server/getTimeZone.js +13 -0
- package/dist/esm/development/server/react-server/getTranslations.js +28 -0
- package/dist/esm/development/server/react-server/validateLocale.js +12 -0
- package/dist/esm/development/server.react-client.js +1 -0
- package/dist/esm/development/server.react-server.js +9 -0
- package/dist/esm/development/shared/NextIntlClientProvider.js +18 -0
- package/dist/esm/development/shared/constants.js +4 -0
- package/dist/esm/development/shared/use.js +13 -0
- package/dist/esm/development/shared/utils.js +140 -0
- package/dist/esm/production/config.js +1 -0
- package/dist/esm/production/extractor/ExtractionCompiler.js +1 -0
- package/dist/esm/production/extractor/catalog/CatalogLocales.js +1 -0
- package/dist/esm/production/extractor/catalog/CatalogManager.js +1 -0
- package/dist/esm/production/extractor/catalog/CatalogPersister.js +1 -0
- package/dist/esm/production/extractor/catalog/SaveScheduler.js +1 -0
- package/dist/esm/production/extractor/catalogLoader.js +1 -0
- package/dist/esm/production/extractor/extractMessages.js +1 -0
- package/dist/esm/production/extractor/extractionLoader.js +1 -0
- package/dist/esm/production/extractor/extractor/LRUCache.js +1 -0
- package/dist/esm/production/extractor/extractor/MessageExtractor.js +1 -0
- package/dist/esm/production/extractor/format/ExtractorCodec.js +1 -0
- package/dist/esm/production/extractor/format/codecs/JSONCodec.js +1 -0
- package/dist/esm/production/extractor/format/codecs/POCodec.js +1 -0
- package/dist/esm/production/extractor/format/index.js +1 -0
- package/dist/esm/production/extractor/source/SourceFileFilter.js +1 -0
- package/dist/esm/production/extractor/source/SourceFileScanner.js +1 -0
- package/dist/esm/production/extractor/source/SourceFileWatcher.js +1 -0
- package/dist/esm/production/extractor/utils.js +1 -0
- package/dist/esm/production/extractor.js +1 -0
- package/dist/esm/production/index.react-client.js +1 -0
- package/dist/esm/production/index.react-server.js +1 -0
- package/dist/esm/production/middleware/getAlternateLinksHeaderValue.js +1 -0
- package/dist/esm/production/middleware/middleware.js +1 -0
- package/dist/esm/production/middleware/resolveLocale.js +1 -0
- package/dist/esm/production/middleware/syncCookie.js +1 -0
- package/dist/esm/production/middleware/utils.js +1 -0
- package/dist/esm/production/middleware.js +1 -0
- package/dist/esm/production/navigation/react-client/createNavigation.js +1 -0
- package/dist/esm/production/navigation/react-client/useBasePathname.js +1 -0
- package/dist/esm/production/navigation/react-server/createNavigation.js +1 -0
- package/dist/esm/production/navigation/react-server/getServerLocale.js +1 -0
- package/dist/esm/production/navigation/shared/BaseLink.js +2 -0
- package/dist/esm/production/navigation/shared/createSharedNavigationFns.js +1 -0
- package/dist/esm/production/navigation/shared/syncLocaleCookie.js +1 -0
- package/dist/esm/production/navigation/shared/utils.js +1 -0
- package/dist/esm/production/navigation.react-client.js +1 -0
- package/dist/esm/production/navigation.react-server.js +1 -0
- package/dist/esm/production/plugin/config.js +1 -0
- package/dist/esm/production/plugin/createNextIntlPlugin.js +1 -0
- package/dist/esm/production/plugin/declaration/createMessagesDeclaration.js +1 -0
- package/dist/esm/production/plugin/extractor/initExtractionCompiler.js +1 -0
- package/dist/esm/production/plugin/getNextConfig.js +1 -0
- package/dist/esm/production/plugin/nextFlags.js +1 -0
- package/dist/esm/production/plugin/utils.js +1 -0
- package/dist/esm/production/plugin/watchFile.js +1 -0
- package/dist/esm/production/plugin.js +1 -0
- package/dist/esm/production/react-client/index.js +1 -0
- package/dist/esm/production/react-server/NextIntlClientProviderServer.js +1 -0
- package/dist/esm/production/react-server/useConfig.js +1 -0
- package/dist/esm/production/react-server/useExtracted.js +1 -0
- package/dist/esm/production/react-server/useFormatter.js +1 -0
- package/dist/esm/production/react-server/useLocale.js +1 -0
- package/dist/esm/production/react-server/useMessages.js +1 -0
- package/dist/esm/production/react-server/useNow.js +1 -0
- package/dist/esm/production/react-server/useTimeZone.js +1 -0
- package/dist/esm/production/react-server/useTranslations.js +1 -0
- package/dist/esm/production/routing/config.js +1 -0
- package/dist/esm/production/routing/defineRouting.js +1 -0
- package/dist/esm/production/routing.js +1 -0
- package/dist/esm/production/server/react-client/index.js +1 -0
- package/dist/esm/production/server/react-server/RequestLocale.js +1 -0
- package/dist/esm/production/server/react-server/RequestLocaleCache.js +1 -0
- package/dist/esm/production/server/react-server/createRequestConfig.js +1 -0
- package/dist/esm/production/server/react-server/getConfig.js +1 -0
- package/dist/esm/production/server/react-server/getConfigNow.js +1 -0
- package/dist/esm/production/server/react-server/getDefaultNow.js +1 -0
- package/dist/esm/production/server/react-server/getExtracted.js +1 -0
- package/dist/esm/production/server/react-server/getFormats.js +1 -0
- package/dist/esm/production/server/react-server/getFormatter.js +1 -0
- package/dist/esm/production/server/react-server/getLocale.js +1 -0
- package/dist/esm/production/server/react-server/getMessages.js +1 -0
- package/dist/esm/production/server/react-server/getNow.js +1 -0
- package/dist/esm/production/server/react-server/getRequestConfig.js +1 -0
- package/dist/esm/production/server/react-server/getServerExtractor.js +1 -0
- package/dist/esm/production/server/react-server/getServerFormatter.js +1 -0
- package/dist/esm/production/server/react-server/getServerTranslator.js +1 -0
- package/dist/esm/production/server/react-server/getTimeZone.js +1 -0
- package/dist/esm/production/server/react-server/getTranslations.js +1 -0
- package/dist/esm/production/server.react-client.js +1 -0
- package/dist/esm/production/server.react-server.js +1 -0
- package/dist/esm/production/shared/NextIntlClientProvider.js +2 -0
- package/dist/esm/production/shared/constants.js +1 -0
- package/dist/esm/production/shared/use.js +1 -0
- package/dist/esm/production/shared/utils.js +1 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/extractor/ExtractionCompiler.d.ts +15 -0
- package/dist/types/extractor/catalog/CatalogLocales.d.ts +29 -0
- package/dist/types/extractor/catalog/CatalogManager.d.ts +59 -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/KeyGenerator.d.ts +3 -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 +21 -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/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 +28 -0
- package/dist/types/extractor/utils.d.ts +7 -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 +464 -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 +3 -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 +29 -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 +24 -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 +25 -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 +175 -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,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var plugin = require('./plugin-YzBhQjAo.cjs');
|
|
4
|
+
require('fs/promises');
|
|
5
|
+
require('path');
|
|
6
|
+
require('@parcel/watcher');
|
|
7
|
+
require('fs');
|
|
8
|
+
require('module');
|
|
9
|
+
require('@swc/core');
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
module.exports = plugin.createNextIntlPlugin;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import CatalogManager from './catalog/CatalogManager.js';
|
|
2
|
+
import MessageExtractor from './extractor/MessageExtractor.js';
|
|
3
|
+
|
|
4
|
+
class ExtractionCompiler {
|
|
5
|
+
constructor(config, opts = {}) {
|
|
6
|
+
const extractor = opts.extractor ?? new MessageExtractor(opts);
|
|
7
|
+
this.manager = new CatalogManager(config, {
|
|
8
|
+
...opts,
|
|
9
|
+
extractor
|
|
10
|
+
});
|
|
11
|
+
this[Symbol.dispose] = this[Symbol.dispose].bind(this);
|
|
12
|
+
this.installExitHandlers();
|
|
13
|
+
}
|
|
14
|
+
async extractAll() {
|
|
15
|
+
// We can't rely on all files being compiled (e.g. due to persistent
|
|
16
|
+
// caching), so loading the messages initially is necessary.
|
|
17
|
+
await this.manager.loadMessages();
|
|
18
|
+
await this.manager.save();
|
|
19
|
+
}
|
|
20
|
+
[Symbol.dispose]() {
|
|
21
|
+
this.uninstallExitHandlers();
|
|
22
|
+
this.manager[Symbol.dispose]();
|
|
23
|
+
}
|
|
24
|
+
installExitHandlers() {
|
|
25
|
+
const cleanup = this[Symbol.dispose];
|
|
26
|
+
process.on('exit', cleanup);
|
|
27
|
+
process.on('SIGINT', cleanup);
|
|
28
|
+
process.on('SIGTERM', cleanup);
|
|
29
|
+
}
|
|
30
|
+
uninstallExitHandlers() {
|
|
31
|
+
const cleanup = this[Symbol.dispose];
|
|
32
|
+
process.off('exit', cleanup);
|
|
33
|
+
process.off('SIGINT', cleanup);
|
|
34
|
+
process.off('SIGTERM', cleanup);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { ExtractionCompiler as default };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs$1 from 'fs';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
class CatalogLocales {
|
|
6
|
+
onChangeCallbacks = new Set();
|
|
7
|
+
constructor(params) {
|
|
8
|
+
this.messagesDir = params.messagesDir;
|
|
9
|
+
this.sourceLocale = params.sourceLocale;
|
|
10
|
+
this.extension = params.extension;
|
|
11
|
+
this.locales = params.locales;
|
|
12
|
+
}
|
|
13
|
+
async getTargetLocales() {
|
|
14
|
+
if (this.targetLocales) {
|
|
15
|
+
return this.targetLocales;
|
|
16
|
+
}
|
|
17
|
+
if (this.locales === 'infer') {
|
|
18
|
+
this.targetLocales = await this.readTargetLocales();
|
|
19
|
+
} else {
|
|
20
|
+
this.targetLocales = this.locales.filter(locale => locale !== this.sourceLocale);
|
|
21
|
+
}
|
|
22
|
+
return this.targetLocales;
|
|
23
|
+
}
|
|
24
|
+
async readTargetLocales() {
|
|
25
|
+
try {
|
|
26
|
+
const files = await fs.readdir(this.messagesDir);
|
|
27
|
+
return files.filter(file => file.endsWith(this.extension)).map(file => path.basename(file, this.extension)).filter(locale => locale !== this.sourceLocale);
|
|
28
|
+
} catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
subscribeLocalesChange(callback) {
|
|
33
|
+
this.onChangeCallbacks.add(callback);
|
|
34
|
+
if (this.locales === 'infer' && !this.watcher) {
|
|
35
|
+
void this.startWatcher();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
unsubscribeLocalesChange(callback) {
|
|
39
|
+
this.onChangeCallbacks.delete(callback);
|
|
40
|
+
if (this.onChangeCallbacks.size === 0) {
|
|
41
|
+
this.stopWatcher();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async startWatcher() {
|
|
45
|
+
if (this.watcher) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
await fs.mkdir(this.messagesDir, {
|
|
49
|
+
recursive: true
|
|
50
|
+
});
|
|
51
|
+
this.watcher = fs$1.watch(this.messagesDir, {
|
|
52
|
+
persistent: false,
|
|
53
|
+
recursive: false
|
|
54
|
+
}, (event, filename) => {
|
|
55
|
+
const isCatalogFile = filename != null && filename.endsWith(this.extension) && !filename.includes(path.sep);
|
|
56
|
+
if (isCatalogFile) {
|
|
57
|
+
void this.onChange();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
stopWatcher() {
|
|
62
|
+
if (this.watcher) {
|
|
63
|
+
this.watcher.close();
|
|
64
|
+
this.watcher = undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async onChange() {
|
|
68
|
+
const oldLocales = new Set(this.targetLocales || []);
|
|
69
|
+
this.targetLocales = await this.readTargetLocales();
|
|
70
|
+
const newLocalesSet = new Set(this.targetLocales);
|
|
71
|
+
const added = this.targetLocales.filter(locale => !oldLocales.has(locale));
|
|
72
|
+
const removed = Array.from(oldLocales).filter(locale => !newLocalesSet.has(locale));
|
|
73
|
+
if (added.length > 0 || removed.length > 0) {
|
|
74
|
+
for (const callback of this.onChangeCallbacks) {
|
|
75
|
+
callback({
|
|
76
|
+
added,
|
|
77
|
+
removed
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { CatalogLocales as default };
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { resolveCodec, getFormatExtension } from '../format/index.js';
|
|
4
|
+
import SourceFileScanner from '../source/SourceFileScanner.js';
|
|
5
|
+
import SourceFileWatcher from '../source/SourceFileWatcher.js';
|
|
6
|
+
import { getDefaultProjectRoot, normalizePathToPosix, compareReferences } from '../utils.js';
|
|
7
|
+
import CatalogLocales from './CatalogLocales.js';
|
|
8
|
+
import CatalogPersister from './CatalogPersister.js';
|
|
9
|
+
import SaveScheduler from './SaveScheduler.js';
|
|
10
|
+
|
|
11
|
+
class CatalogManager {
|
|
12
|
+
/**
|
|
13
|
+
* The source of truth for which messages are used.
|
|
14
|
+
* NOTE: Should be mutated in place to keep `messagesById` and `messagesByFile` in sync.
|
|
15
|
+
*/
|
|
16
|
+
messagesByFile = new Map();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fast lookup for messages by ID across all files,
|
|
20
|
+
* contains the same messages as `messagesByFile`.
|
|
21
|
+
* NOTE: Should be mutated in place to keep `messagesById` and `messagesByFile` in sync.
|
|
22
|
+
*/
|
|
23
|
+
messagesById = new Map();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* This potentially also includes outdated ones that were initially available,
|
|
27
|
+
* but are not used anymore. This allows to restore them if they are used again.
|
|
28
|
+
**/
|
|
29
|
+
translationsByTargetLocale = new Map();
|
|
30
|
+
lastWriteByLocale = new Map();
|
|
31
|
+
|
|
32
|
+
// Cached instances
|
|
33
|
+
|
|
34
|
+
// Resolves when all catalogs are loaded
|
|
35
|
+
|
|
36
|
+
// Resolves when the initial project scan and processing is complete
|
|
37
|
+
|
|
38
|
+
constructor(config, opts) {
|
|
39
|
+
this.config = config;
|
|
40
|
+
this.saveScheduler = new SaveScheduler(50);
|
|
41
|
+
this.projectRoot = opts.projectRoot ?? getDefaultProjectRoot();
|
|
42
|
+
this.isDevelopment = opts.isDevelopment ?? false;
|
|
43
|
+
this.extractor = opts.extractor;
|
|
44
|
+
if (this.isDevelopment) {
|
|
45
|
+
// We kick this off as early as possible, so we get notified about changes
|
|
46
|
+
// that happen during the initial project scan (while awaiting it to
|
|
47
|
+
// complete though)
|
|
48
|
+
this.sourceWatcher = new SourceFileWatcher(this.getSrcPaths(), this.handleFileEvents.bind(this));
|
|
49
|
+
void this.sourceWatcher.start();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async getCodec() {
|
|
53
|
+
if (!this.codec) {
|
|
54
|
+
this.codec = await resolveCodec(this.config.messages.format, this.projectRoot);
|
|
55
|
+
}
|
|
56
|
+
return this.codec;
|
|
57
|
+
}
|
|
58
|
+
async getPersister() {
|
|
59
|
+
if (this.persister) {
|
|
60
|
+
return this.persister;
|
|
61
|
+
} else {
|
|
62
|
+
this.persister = new CatalogPersister({
|
|
63
|
+
messagesPath: this.config.messages.path,
|
|
64
|
+
codec: await this.getCodec(),
|
|
65
|
+
extension: getFormatExtension(this.config.messages.format)
|
|
66
|
+
});
|
|
67
|
+
return this.persister;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
getCatalogLocales() {
|
|
71
|
+
if (this.catalogLocales) {
|
|
72
|
+
return this.catalogLocales;
|
|
73
|
+
} else {
|
|
74
|
+
const messagesDir = path.join(this.projectRoot, this.config.messages.path);
|
|
75
|
+
this.catalogLocales = new CatalogLocales({
|
|
76
|
+
messagesDir,
|
|
77
|
+
sourceLocale: this.config.sourceLocale,
|
|
78
|
+
extension: getFormatExtension(this.config.messages.format),
|
|
79
|
+
locales: this.config.messages.locales
|
|
80
|
+
});
|
|
81
|
+
return this.catalogLocales;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async getTargetLocales() {
|
|
85
|
+
return this.getCatalogLocales().getTargetLocales();
|
|
86
|
+
}
|
|
87
|
+
getSrcPaths() {
|
|
88
|
+
return (Array.isArray(this.config.srcPath) ? this.config.srcPath : [this.config.srcPath]).map(srcPath => path.join(this.projectRoot, srcPath));
|
|
89
|
+
}
|
|
90
|
+
async loadMessages() {
|
|
91
|
+
const sourceDiskMessages = await this.loadSourceMessages();
|
|
92
|
+
this.loadCatalogsPromise = this.loadTargetMessages();
|
|
93
|
+
await this.loadCatalogsPromise;
|
|
94
|
+
this.scanCompletePromise = (async () => {
|
|
95
|
+
const sourceFiles = await SourceFileScanner.getSourceFiles(this.getSrcPaths());
|
|
96
|
+
await Promise.all(Array.from(sourceFiles).map(async filePath => this.processFile(filePath)));
|
|
97
|
+
this.mergeSourceDiskMetadata(sourceDiskMessages);
|
|
98
|
+
})();
|
|
99
|
+
await this.scanCompletePromise;
|
|
100
|
+
if (this.isDevelopment) {
|
|
101
|
+
const catalogLocales = this.getCatalogLocales();
|
|
102
|
+
catalogLocales.subscribeLocalesChange(this.onLocalesChange);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async loadSourceMessages() {
|
|
106
|
+
// Load source catalog to hydrate metadata (e.g. flags) later without
|
|
107
|
+
// treating catalog entries as source of truth.
|
|
108
|
+
const diskMessages = await this.loadLocaleMessages(this.config.sourceLocale);
|
|
109
|
+
const byId = new Map();
|
|
110
|
+
for (const diskMessage of diskMessages) {
|
|
111
|
+
byId.set(diskMessage.id, diskMessage);
|
|
112
|
+
}
|
|
113
|
+
return byId;
|
|
114
|
+
}
|
|
115
|
+
async loadLocaleMessages(locale) {
|
|
116
|
+
const persister = await this.getPersister();
|
|
117
|
+
const messages = await persister.read(locale);
|
|
118
|
+
const fileTime = await persister.getLastModified(locale);
|
|
119
|
+
this.lastWriteByLocale.set(locale, fileTime);
|
|
120
|
+
return messages;
|
|
121
|
+
}
|
|
122
|
+
async loadTargetMessages() {
|
|
123
|
+
const targetLocales = await this.getTargetLocales();
|
|
124
|
+
await Promise.all(targetLocales.map(locale => this.reloadLocaleCatalog(locale)));
|
|
125
|
+
}
|
|
126
|
+
async reloadLocaleCatalog(locale) {
|
|
127
|
+
const diskMessages = await this.loadLocaleMessages(locale);
|
|
128
|
+
if (locale === this.config.sourceLocale) {
|
|
129
|
+
// For source: Merge additional properties like flags
|
|
130
|
+
for (const diskMessage of diskMessages) {
|
|
131
|
+
const prev = this.messagesById.get(diskMessage.id);
|
|
132
|
+
if (prev) {
|
|
133
|
+
// Mutate the existing object instead of creating a copy
|
|
134
|
+
// to keep messagesById and messagesByFile in sync.
|
|
135
|
+
// Unknown properties (like flags): disk wins
|
|
136
|
+
// Known properties: existing (from extraction) wins
|
|
137
|
+
for (const key of Object.keys(diskMessage)) {
|
|
138
|
+
if (!['id', 'message', 'description', 'references'].includes(key)) {
|
|
139
|
+
// For unknown properties (like flags), disk wins
|
|
140
|
+
prev[key] = diskMessage[key];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// For target: disk wins completely, BUT preserve existing translations
|
|
147
|
+
// if we read empty (likely a write in progress by an external tool
|
|
148
|
+
// that causes the file to temporarily be empty)
|
|
149
|
+
const existingTranslations = this.translationsByTargetLocale.get(locale);
|
|
150
|
+
const hasExistingTranslations = existingTranslations && existingTranslations.size > 0;
|
|
151
|
+
if (diskMessages.length > 0) {
|
|
152
|
+
// We got content from disk, replace with it
|
|
153
|
+
const translations = new Map();
|
|
154
|
+
for (const message of diskMessages) {
|
|
155
|
+
translations.set(message.id, message);
|
|
156
|
+
}
|
|
157
|
+
this.translationsByTargetLocale.set(locale, translations);
|
|
158
|
+
} else if (hasExistingTranslations) ; else {
|
|
159
|
+
// We read empty and have no existing translations
|
|
160
|
+
const translations = new Map();
|
|
161
|
+
this.translationsByTargetLocale.set(locale, translations);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
mergeSourceDiskMetadata(diskMessages) {
|
|
166
|
+
for (const [id, diskMessage] of diskMessages) {
|
|
167
|
+
const existing = this.messagesById.get(id);
|
|
168
|
+
if (!existing) continue;
|
|
169
|
+
|
|
170
|
+
// Mutate the existing object instead of creating a copy.
|
|
171
|
+
// This keeps `messagesById` and `messagesByFile` in sync since
|
|
172
|
+
// they reference the same object instance.
|
|
173
|
+
for (const key of Object.keys(diskMessage)) {
|
|
174
|
+
if (existing[key] == null) {
|
|
175
|
+
existing[key] = diskMessage[key];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async processFile(absoluteFilePath) {
|
|
181
|
+
let messages = [];
|
|
182
|
+
try {
|
|
183
|
+
const content = await fs.readFile(absoluteFilePath, 'utf8');
|
|
184
|
+
let extraction;
|
|
185
|
+
try {
|
|
186
|
+
extraction = await this.extractor.extract(absoluteFilePath, content);
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
messages = extraction.messages;
|
|
191
|
+
} catch (err) {
|
|
192
|
+
if (err.code !== 'ENOENT') {
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
// ENOENT -> treat as no messages
|
|
196
|
+
}
|
|
197
|
+
const prevFileMessages = this.messagesByFile.get(absoluteFilePath);
|
|
198
|
+
const relativeFilePath = normalizePathToPosix(path.relative(this.projectRoot, absoluteFilePath));
|
|
199
|
+
|
|
200
|
+
// Init with all previous ones
|
|
201
|
+
const idsToRemove = Array.from(prevFileMessages?.keys() ?? []);
|
|
202
|
+
|
|
203
|
+
// Replace existing messages with new ones
|
|
204
|
+
const fileMessages = new Map();
|
|
205
|
+
for (let message of messages) {
|
|
206
|
+
const prevMessage = this.messagesById.get(message.id);
|
|
207
|
+
|
|
208
|
+
// Merge with previous message if it exists
|
|
209
|
+
if (prevMessage) {
|
|
210
|
+
message = {
|
|
211
|
+
...message
|
|
212
|
+
};
|
|
213
|
+
if (message.references) {
|
|
214
|
+
message.references = this.mergeReferences(prevMessage.references ?? [], relativeFilePath, message.references);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Merge other properties like description, or unknown
|
|
218
|
+
// attributes like flags that are opaque to us
|
|
219
|
+
for (const key of Object.keys(prevMessage)) {
|
|
220
|
+
if (message[key] == null) {
|
|
221
|
+
message[key] = prevMessage[key];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
this.messagesById.set(message.id, message);
|
|
226
|
+
fileMessages.set(message.id, message);
|
|
227
|
+
|
|
228
|
+
// This message continues to exist in this file
|
|
229
|
+
const index = idsToRemove.indexOf(message.id);
|
|
230
|
+
if (index !== -1) idsToRemove.splice(index, 1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Clean up removed messages from `messagesById`
|
|
234
|
+
idsToRemove.forEach(id => {
|
|
235
|
+
const message = this.messagesById.get(id);
|
|
236
|
+
if (!message) return;
|
|
237
|
+
const hasOtherReferences = message.references?.some(ref => ref.path !== relativeFilePath);
|
|
238
|
+
if (!hasOtherReferences) {
|
|
239
|
+
// No other references, delete the message entirely
|
|
240
|
+
this.messagesById.delete(id);
|
|
241
|
+
} else {
|
|
242
|
+
// Message is used elsewhere, remove this file from references
|
|
243
|
+
// Mutate the existing object to keep `messagesById` and `messagesByFile` in sync
|
|
244
|
+
message.references = message.references?.filter(ref => ref.path !== relativeFilePath);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Update the stored messages
|
|
249
|
+
if (messages.length > 0) {
|
|
250
|
+
this.messagesByFile.set(absoluteFilePath, fileMessages);
|
|
251
|
+
} else {
|
|
252
|
+
this.messagesByFile.delete(absoluteFilePath);
|
|
253
|
+
}
|
|
254
|
+
const changed = this.haveMessagesChangedForFile(prevFileMessages, fileMessages);
|
|
255
|
+
return changed;
|
|
256
|
+
}
|
|
257
|
+
mergeReferences(existing, currentFilePath, currentFileRefs) {
|
|
258
|
+
// Keep refs from other files, replace all refs from the current file
|
|
259
|
+
const otherFileRefs = existing.filter(ref => ref.path !== currentFilePath);
|
|
260
|
+
const merged = [...otherFileRefs, ...currentFileRefs];
|
|
261
|
+
return merged.sort(compareReferences);
|
|
262
|
+
}
|
|
263
|
+
haveMessagesChangedForFile(beforeMessages, afterMessages) {
|
|
264
|
+
// If one exists and the other doesn't, there's a change
|
|
265
|
+
if (!beforeMessages) {
|
|
266
|
+
return afterMessages.size > 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Different sizes means changes
|
|
270
|
+
if (beforeMessages.size !== afterMessages.size) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check differences in beforeMessages vs afterMessages
|
|
275
|
+
for (const [id, msg1] of beforeMessages) {
|
|
276
|
+
const msg2 = afterMessages.get(id);
|
|
277
|
+
if (!msg2 || !this.areMessagesEqual(msg1, msg2)) {
|
|
278
|
+
return true; // Early exit on first difference
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
areMessagesEqual(msg1, msg2) {
|
|
284
|
+
// Note: We intentionally don't compare references here.
|
|
285
|
+
// References are aggregated metadata from multiple files and comparing
|
|
286
|
+
// them would cause false positives due to parallel extraction order.
|
|
287
|
+
return msg1.id === msg2.id && msg1.message === msg2.message && msg1.description === msg2.description;
|
|
288
|
+
}
|
|
289
|
+
async save() {
|
|
290
|
+
return this.saveScheduler.schedule(() => this.saveImpl());
|
|
291
|
+
}
|
|
292
|
+
async saveImpl() {
|
|
293
|
+
await this.saveLocale(this.config.sourceLocale);
|
|
294
|
+
const targetLocales = await this.getTargetLocales();
|
|
295
|
+
await Promise.all(targetLocales.map(locale => this.saveLocale(locale)));
|
|
296
|
+
}
|
|
297
|
+
async saveLocale(locale) {
|
|
298
|
+
await this.loadCatalogsPromise;
|
|
299
|
+
const messages = Array.from(this.messagesById.values());
|
|
300
|
+
const persister = await this.getPersister();
|
|
301
|
+
const isSourceLocale = locale === this.config.sourceLocale;
|
|
302
|
+
|
|
303
|
+
// Check if file was modified externally (poll-at-save is cheaper than
|
|
304
|
+
// watchers here since stat() is fast and avoids continuous overhead)
|
|
305
|
+
const lastWriteTime = this.lastWriteByLocale.get(locale);
|
|
306
|
+
const currentFileTime = await persister.getLastModified(locale);
|
|
307
|
+
if (currentFileTime && lastWriteTime && currentFileTime > lastWriteTime) {
|
|
308
|
+
await this.reloadLocaleCatalog(locale);
|
|
309
|
+
}
|
|
310
|
+
const localeMessages = isSourceLocale ? this.messagesById : this.translationsByTargetLocale.get(locale);
|
|
311
|
+
const messagesToPersist = messages.map(message => {
|
|
312
|
+
const localeMessage = localeMessages?.get(message.id);
|
|
313
|
+
return {
|
|
314
|
+
...localeMessage,
|
|
315
|
+
id: message.id,
|
|
316
|
+
description: message.description,
|
|
317
|
+
references: message.references,
|
|
318
|
+
message: isSourceLocale ? message.message : localeMessage?.message ?? ''
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
await persister.write(messagesToPersist, {
|
|
322
|
+
locale,
|
|
323
|
+
sourceMessagesById: this.messagesById
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Update timestamps
|
|
327
|
+
const newTime = await persister.getLastModified(locale);
|
|
328
|
+
this.lastWriteByLocale.set(locale, newTime);
|
|
329
|
+
}
|
|
330
|
+
onLocalesChange = async params => {
|
|
331
|
+
// Chain to existing promise
|
|
332
|
+
this.loadCatalogsPromise = Promise.all([this.loadCatalogsPromise, ...params.added.map(locale => this.reloadLocaleCatalog(locale))]);
|
|
333
|
+
for (const locale of params.added) {
|
|
334
|
+
await this.saveLocale(locale);
|
|
335
|
+
}
|
|
336
|
+
for (const locale of params.removed) {
|
|
337
|
+
this.translationsByTargetLocale.delete(locale);
|
|
338
|
+
this.lastWriteByLocale.delete(locale);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
async handleFileEvents(events) {
|
|
342
|
+
if (this.loadCatalogsPromise) {
|
|
343
|
+
await this.loadCatalogsPromise;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Wait for initial scan to complete to avoid race conditions
|
|
347
|
+
if (this.scanCompletePromise) {
|
|
348
|
+
await this.scanCompletePromise;
|
|
349
|
+
}
|
|
350
|
+
let changed = false;
|
|
351
|
+
const expandedEvents = await this.sourceWatcher.expandDirectoryDeleteEvents(events, Array.from(this.messagesByFile.keys()));
|
|
352
|
+
for (const event of expandedEvents) {
|
|
353
|
+
const hasChanged = await this.processFile(event.path);
|
|
354
|
+
changed ||= hasChanged;
|
|
355
|
+
}
|
|
356
|
+
if (changed) {
|
|
357
|
+
await this.save();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
[Symbol.dispose]() {
|
|
361
|
+
this.sourceWatcher?.stop();
|
|
362
|
+
this.sourceWatcher = undefined;
|
|
363
|
+
this.saveScheduler[Symbol.dispose]();
|
|
364
|
+
if (this.catalogLocales && this.isDevelopment) {
|
|
365
|
+
this.catalogLocales.unsubscribeLocalesChange(this.onLocalesChange);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export { CatalogManager as default };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
class CatalogPersister {
|
|
5
|
+
constructor(params) {
|
|
6
|
+
this.messagesPath = params.messagesPath;
|
|
7
|
+
this.codec = params.codec;
|
|
8
|
+
this.extension = params.extension;
|
|
9
|
+
}
|
|
10
|
+
getFileName(locale) {
|
|
11
|
+
return locale + this.extension;
|
|
12
|
+
}
|
|
13
|
+
getFilePath(locale) {
|
|
14
|
+
return path.join(this.messagesPath, this.getFileName(locale));
|
|
15
|
+
}
|
|
16
|
+
async read(locale) {
|
|
17
|
+
const filePath = this.getFilePath(locale);
|
|
18
|
+
let content;
|
|
19
|
+
try {
|
|
20
|
+
content = await fs.readFile(filePath, 'utf8');
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`Error while reading ${this.getFileName(locale)}:\n> ${error}`, {
|
|
26
|
+
cause: error
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return this.codec.decode(content, {
|
|
31
|
+
locale
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new Error(`Error while decoding ${this.getFileName(locale)}:\n> ${error}`, {
|
|
35
|
+
cause: error
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async write(messages, context) {
|
|
40
|
+
const filePath = this.getFilePath(context.locale);
|
|
41
|
+
const content = this.codec.encode(messages, context);
|
|
42
|
+
try {
|
|
43
|
+
const outputDir = path.dirname(filePath);
|
|
44
|
+
await fs.mkdir(outputDir, {
|
|
45
|
+
recursive: true
|
|
46
|
+
});
|
|
47
|
+
await fs.writeFile(filePath, content);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(`❌ Failed to write catalog: ${error}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async getLastModified(locale) {
|
|
53
|
+
const filePath = this.getFilePath(locale);
|
|
54
|
+
try {
|
|
55
|
+
const stats = await fs.stat(filePath);
|
|
56
|
+
return stats.mtime;
|
|
57
|
+
} catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { CatalogPersister as default };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* De-duplicates excessive save invocations,
|
|
3
|
+
* while keeping a single one instant.
|
|
4
|
+
*/
|
|
5
|
+
class SaveScheduler {
|
|
6
|
+
isSaving = false;
|
|
7
|
+
pendingResolvers = [];
|
|
8
|
+
constructor(delayMs = 50) {
|
|
9
|
+
this.delayMs = delayMs;
|
|
10
|
+
}
|
|
11
|
+
async schedule(saveTask) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
this.pendingResolvers.push({
|
|
14
|
+
resolve,
|
|
15
|
+
reject
|
|
16
|
+
});
|
|
17
|
+
this.nextSaveTask = saveTask;
|
|
18
|
+
if (!this.isSaving && !this.saveTimeout) {
|
|
19
|
+
// Not currently saving and no scheduled save, save immediately
|
|
20
|
+
this.executeSave();
|
|
21
|
+
} else if (this.saveTimeout) {
|
|
22
|
+
// A save is already scheduled, reschedule to debounce
|
|
23
|
+
this.scheduleSave();
|
|
24
|
+
}
|
|
25
|
+
// If isSaving is true and no timeout is scheduled, the current save
|
|
26
|
+
// will check for pending resolvers when it completes and schedule
|
|
27
|
+
// another save if needed (see finally block in executeSave)
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
scheduleSave() {
|
|
31
|
+
if (this.saveTimeout) {
|
|
32
|
+
clearTimeout(this.saveTimeout);
|
|
33
|
+
}
|
|
34
|
+
this.saveTimeout = setTimeout(() => {
|
|
35
|
+
this.saveTimeout = undefined;
|
|
36
|
+
this.executeSave();
|
|
37
|
+
}, this.delayMs);
|
|
38
|
+
}
|
|
39
|
+
async executeSave() {
|
|
40
|
+
if (this.isSaving) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const saveTask = this.nextSaveTask;
|
|
44
|
+
if (!saveTask) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Capture current pending resolvers for this save
|
|
49
|
+
const resolversForThisSave = this.pendingResolvers;
|
|
50
|
+
this.pendingResolvers = [];
|
|
51
|
+
this.nextSaveTask = undefined;
|
|
52
|
+
this.isSaving = true;
|
|
53
|
+
try {
|
|
54
|
+
const result = await saveTask();
|
|
55
|
+
|
|
56
|
+
// Resolve only the promises that were pending when this save started
|
|
57
|
+
resolversForThisSave.forEach(({
|
|
58
|
+
resolve
|
|
59
|
+
}) => resolve(result));
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Reject only the promises that were pending when this save started
|
|
62
|
+
resolversForThisSave.forEach(({
|
|
63
|
+
reject
|
|
64
|
+
}) => reject(error));
|
|
65
|
+
} finally {
|
|
66
|
+
this.isSaving = false;
|
|
67
|
+
|
|
68
|
+
// If new saves were requested during this save, schedule another
|
|
69
|
+
if (this.pendingResolvers.length > 0) {
|
|
70
|
+
this.scheduleSave();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
[Symbol.dispose]() {
|
|
75
|
+
if (this.saveTimeout) {
|
|
76
|
+
clearTimeout(this.saveTimeout);
|
|
77
|
+
this.saveTimeout = undefined;
|
|
78
|
+
}
|
|
79
|
+
this.pendingResolvers = [];
|
|
80
|
+
this.nextSaveTask = undefined;
|
|
81
|
+
this.isSaving = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { SaveScheduler as default };
|