next-intl 4.6.0 → 4.7.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.
Files changed (36) hide show
  1. package/dist/cjs/development/ExtractorCodec-D9Tw618d.cjs +7 -0
  2. package/dist/cjs/development/{JSONCodec-Dlcx71xz.cjs → JSONCodec-CusgMbzf.cjs} +10 -3
  3. package/dist/cjs/development/{POCodec-BW-UDNcq.cjs → POCodec-Bns8JvnL.cjs} +16 -5
  4. package/dist/cjs/development/plugin-B0KIcFlc.cjs +1392 -0
  5. package/dist/cjs/development/plugin.cjs +8 -402
  6. package/dist/esm/development/extractor/ExtractionCompiler.js +1 -1
  7. package/dist/esm/development/extractor/catalog/CatalogManager.js +53 -27
  8. package/dist/esm/development/extractor/catalog/SaveScheduler.js +1 -1
  9. package/dist/esm/development/extractor/extractionLoader.js +5 -25
  10. package/dist/esm/development/extractor/source/SourceFileWatcher.js +99 -5
  11. package/dist/esm/development/extractor/utils.js +14 -8
  12. package/dist/esm/development/plugin/createNextIntlPlugin.js +2 -0
  13. package/dist/esm/development/plugin/declaration/createMessagesDeclaration.js +2 -11
  14. package/dist/esm/development/plugin/extractor/initExtractionCompiler.js +61 -0
  15. package/dist/esm/development/plugin/utils.js +16 -1
  16. package/dist/esm/production/extractor/ExtractionCompiler.js +1 -1
  17. package/dist/esm/production/extractor/catalog/CatalogManager.js +1 -1
  18. package/dist/esm/production/extractor/catalog/SaveScheduler.js +1 -1
  19. package/dist/esm/production/extractor/extractionLoader.js +1 -1
  20. package/dist/esm/production/extractor/source/SourceFileWatcher.js +1 -1
  21. package/dist/esm/production/extractor/utils.js +1 -1
  22. package/dist/esm/production/plugin/createNextIntlPlugin.js +1 -1
  23. package/dist/esm/production/plugin/declaration/createMessagesDeclaration.js +1 -1
  24. package/dist/esm/production/plugin/extractor/initExtractionCompiler.js +1 -0
  25. package/dist/esm/production/plugin/utils.js +1 -1
  26. package/dist/types/extractor/catalog/CatalogManager.d.ts +6 -5
  27. package/dist/types/extractor/catalog/SaveScheduler.d.ts +2 -2
  28. package/dist/types/extractor/source/SourceFileFilter.d.ts +1 -1
  29. package/dist/types/extractor/source/SourceFileWatcher.d.ts +3 -0
  30. package/dist/types/extractor/types.d.ts +5 -3
  31. package/dist/types/extractor/utils.d.ts +2 -1
  32. package/dist/types/plugin/extractor/initExtractionCompiler.d.ts +2 -0
  33. package/dist/types/plugin/utils.d.ts +6 -0
  34. package/package.json +5 -5
  35. package/dist/cjs/development/ExtractorCodec-DZKNn0Zq.cjs +0 -37
  36. package/dist/types/extractor/extractor/ASTScope.d.ts +0 -12
@@ -1,5 +1,8 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
1
3
  import { subscribe } from '@parcel/watcher';
2
4
  import SourceFileFilter from './SourceFileFilter.js';
5
+ import SourceFileScanner from './SourceFileScanner.js';
3
6
 
4
7
  class SourceFileWatcher {
5
8
  subscriptions = [];
@@ -8,17 +11,19 @@ class SourceFileWatcher {
8
11
  this.onChange = onChange;
9
12
  }
10
13
  async start() {
11
- if (this.subscriptions.length > 0) return;
14
+ if (this.subscriptions.length > 0) {
15
+ return;
16
+ }
12
17
  const ignore = SourceFileFilter.IGNORED_DIRECTORIES.map(dir => `**/${dir}/**`);
13
18
  for (const root of this.roots) {
14
- const sub = await subscribe(root, (err, events) => {
19
+ const sub = await subscribe(root, async (err, events) => {
15
20
  if (err) {
16
21
  console.error(err);
17
22
  return;
18
23
  }
19
- const filteredEvents = events.filter(event => SourceFileFilter.isSourceFile(event.path));
20
- if (filteredEvents.length > 0) {
21
- void this.onChange(filteredEvents);
24
+ const filtered = await this.normalizeEvents(events);
25
+ if (filtered.length > 0) {
26
+ void this.onChange(filtered);
22
27
  }
23
28
  }, {
24
29
  ignore
@@ -26,6 +31,95 @@ class SourceFileWatcher {
26
31
  this.subscriptions.push(sub);
27
32
  }
28
33
  }
34
+ async normalizeEvents(events) {
35
+ const directoryCreatePaths = [];
36
+ const otherEvents = [];
37
+
38
+ // We need to expand directory creates because during rename operations,
39
+ // @parcel/watcher emits a directory create event but may not emit individual
40
+ // file events for the moved files
41
+ await Promise.all(events.map(async event => {
42
+ if (event.type === 'create') {
43
+ try {
44
+ const stats = await fs.stat(event.path);
45
+ if (stats.isDirectory()) {
46
+ directoryCreatePaths.push(event.path);
47
+ return;
48
+ }
49
+ } catch {
50
+ // Path doesn't exist or is inaccessible, treat as file
51
+ }
52
+ }
53
+ otherEvents.push(event);
54
+ }));
55
+
56
+ // Expand directory create events to find source files inside
57
+ let expandedCreateEvents = [];
58
+ if (directoryCreatePaths.length > 0) {
59
+ try {
60
+ const sourceFiles = await SourceFileScanner.getSourceFiles(directoryCreatePaths);
61
+ expandedCreateEvents = Array.from(sourceFiles).map(filePath => ({
62
+ type: 'create',
63
+ path: filePath
64
+ }));
65
+ } catch {
66
+ // Directories might have been deleted or are inaccessible
67
+ }
68
+ }
69
+
70
+ // Combine original events with expanded directory creates.
71
+ // Deduplicate by path to avoid processing the same file twice
72
+ // in case @parcel/watcher also emitted individual file events.
73
+ const allEvents = [...otherEvents, ...expandedCreateEvents];
74
+ const seenPaths = new Set();
75
+ const deduplicated = [];
76
+ for (const event of allEvents) {
77
+ const key = `${event.type}:${event.path}`;
78
+ if (!seenPaths.has(key)) {
79
+ seenPaths.add(key);
80
+ deduplicated.push(event);
81
+ }
82
+ }
83
+ return deduplicated.filter(event => {
84
+ // Keep all delete events (might be deleted directories that no longer exist)
85
+ if (event.type === 'delete') {
86
+ return true;
87
+ }
88
+ // Keep source files
89
+ return SourceFileFilter.isSourceFile(event.path);
90
+ });
91
+ }
92
+ async expandDirectoryDeleteEvents(events, prevKnownFiles) {
93
+ const expanded = [];
94
+ for (const event of events) {
95
+ if (event.type === 'delete' && !SourceFileFilter.isSourceFile(event.path)) {
96
+ const dirPath = path.resolve(event.path);
97
+ const filesInDirectory = [];
98
+ for (const filePath of prevKnownFiles) {
99
+ if (SourceFileFilter.isWithinPath(filePath, dirPath)) {
100
+ filesInDirectory.push(filePath);
101
+ }
102
+ }
103
+
104
+ // If we found files within this path, it was a directory
105
+ if (filesInDirectory.length > 0) {
106
+ for (const filePath of filesInDirectory) {
107
+ expanded.push({
108
+ type: 'delete',
109
+ path: filePath
110
+ });
111
+ }
112
+ } else {
113
+ // Not a directory or no files in it, pass through as-is
114
+ expanded.push(event);
115
+ }
116
+ } else {
117
+ // Pass through as-is
118
+ expanded.push(event);
119
+ }
120
+ }
121
+ return expanded;
122
+ }
29
123
  async stop() {
30
124
  await Promise.all(this.subscriptions.map(sub => sub.unsubscribe()));
31
125
  this.subscriptions = [];
@@ -13,20 +13,26 @@ function setNestedProperty(obj, keyPath, value) {
13
13
  }
14
14
  function getSortedMessages(messages) {
15
15
  return messages.toSorted((messageA, messageB) => {
16
- const pathA = messageA.references?.[0]?.path ?? '';
17
- const pathB = messageB.references?.[0]?.path ?? '';
18
- if (pathA === pathB) {
19
- return localeCompare(messageA.id, messageB.id);
20
- } else {
21
- return localeCompare(pathA, pathB);
22
- }
16
+ const refA = messageA.references?.[0];
17
+ const refB = messageB.references?.[0];
18
+
19
+ // No references: preserve original (extraction) order
20
+ if (!refA || !refB) return 0;
21
+
22
+ // Sort by path, then line. Same path+line: preserve original order
23
+ return compareReferences(refA, refB);
23
24
  });
24
25
  }
25
26
  function localeCompare(a, b) {
26
27
  return a.localeCompare(b, 'en');
27
28
  }
29
+ function compareReferences(refA, refB) {
30
+ const pathCompare = localeCompare(refA.path, refB.path);
31
+ if (pathCompare !== 0) return pathCompare;
32
+ return (refA.line ?? 0) - (refB.line ?? 0);
33
+ }
28
34
  function getDefaultProjectRoot() {
29
35
  return process.cwd();
30
36
  }
31
37
 
32
- export { getDefaultProjectRoot, getSortedMessages, localeCompare, setNestedProperty };
38
+ export { compareReferences, getDefaultProjectRoot, getSortedMessages, localeCompare, setNestedProperty };
@@ -1,3 +1,4 @@
1
+ import initExtractionCompiler from './extractor/initExtractionCompiler.js';
1
2
  import getNextConfig from './getNextConfig.js';
2
3
  import { warn } from './utils.js';
3
4
  import createMessagesDeclaration from './declaration/createMessagesDeclaration.js';
@@ -10,6 +11,7 @@ function initPlugin(pluginConfig, nextConfig) {
10
11
  if (messagesPathOrPaths) {
11
12
  createMessagesDeclaration(typeof messagesPathOrPaths === 'string' ? [messagesPathOrPaths] : messagesPathOrPaths);
12
13
  }
14
+ initExtractionCompiler(pluginConfig);
13
15
  return getNextConfig(pluginConfig, nextConfig);
14
16
  }
15
17
  function createNextIntlPlugin(i18nPathOrConfig = {}) {
@@ -1,15 +1,9 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { throwError } from '../utils.js';
3
+ import { once, throwError } from '../utils.js';
4
4
  import watchFile from '../watchFile.js';
5
5
 
6
- function runOnce(fn) {
7
- if (process.env._NEXT_INTL_COMPILE_MESSAGES === '1') {
8
- return;
9
- }
10
- process.env._NEXT_INTL_COMPILE_MESSAGES = '1';
11
- fn();
12
- }
6
+ const runOnce = once('_NEXT_INTL_COMPILE_MESSAGES');
13
7
  function createMessagesDeclaration(messagesPaths) {
14
8
  // Instead of running _only_ in certain cases, it's
15
9
  // safer to _avoid_ running for certain known cases.
@@ -29,9 +23,6 @@ function createMessagesDeclaration(messagesPaths) {
29
23
  if (shouldBailOut) {
30
24
  return;
31
25
  }
32
-
33
- // Next.js can call the Next.js config multiple
34
- // times - ensure we only run once.
35
26
  runOnce(() => {
36
27
  for (const messagesPath of messagesPaths) {
37
28
  const fullPath = path.resolve(messagesPath);
@@ -0,0 +1,61 @@
1
+ import ExtractionCompiler from '../../extractor/ExtractionCompiler.js';
2
+ import { once } from '../utils.js';
3
+
4
+ // Single compiler instance, initialized once per process
5
+ let compiler;
6
+ const runOnce = once('_NEXT_INTL_EXTRACT');
7
+ function initExtractionCompiler(pluginConfig) {
8
+ const experimental = pluginConfig.experimental;
9
+ if (!experimental?.extract) {
10
+ return;
11
+ }
12
+
13
+ // Avoid rollup's `replace` plugin to compile this away
14
+ const isDevelopment = process.env['NODE_ENV'.trim()] === 'development';
15
+
16
+ // Avoid running for:
17
+ // - info
18
+ // - start
19
+ // - typegen
20
+ //
21
+ // Doesn't consult Next.js config anyway:
22
+ // - telemetry
23
+ // - lint
24
+ //
25
+ // What remains are:
26
+ // - dev (NODE_ENV=development)
27
+ // - build (NODE_ENV=production)
28
+ const shouldRun = isDevelopment || process.argv.includes('build');
29
+ if (!shouldRun) return;
30
+ runOnce(() => {
31
+ const extractorConfig = {
32
+ srcPath: experimental.srcPath,
33
+ sourceLocale: experimental.extract.sourceLocale,
34
+ messages: experimental.messages
35
+ };
36
+ compiler = new ExtractionCompiler(extractorConfig, {
37
+ isDevelopment,
38
+ projectRoot: process.cwd()
39
+ });
40
+
41
+ // Fire-and-forget: Start extraction, don't block config return.
42
+ // In dev mode, this also starts the file watcher.
43
+ // In prod, ideally we would wait until the extraction is complete,
44
+ // but we can't `await` anywhere (at least for Turbopack).
45
+ // The result is ok though, as if we encounter untranslated messages,
46
+ // we'll simply add empty messages to the catalog. So for actually
47
+ // running the app, there is no difference.
48
+ compiler.extractAll();
49
+ function cleanup() {
50
+ if (compiler) {
51
+ compiler[Symbol.dispose]();
52
+ compiler = undefined;
53
+ }
54
+ }
55
+ process.on('exit', cleanup);
56
+ process.on('SIGINT', cleanup);
57
+ process.on('SIGTERM', cleanup);
58
+ });
59
+ }
60
+
61
+ export { initExtractionCompiler as default };
@@ -8,4 +8,19 @@ function warn(message) {
8
8
  console.warn(formatMessage(message));
9
9
  }
10
10
 
11
- export { throwError, warn };
11
+ /**
12
+ * Returns a function that runs the provided callback only once per process.
13
+ * Next.js can call the config multiple times - this ensures we only run once.
14
+ * Uses an environment variable to track execution across config loads.
15
+ */
16
+ function once(namespace) {
17
+ return function runOnce(fn) {
18
+ if (process.env[namespace] === '1') {
19
+ return;
20
+ }
21
+ process.env[namespace] = '1';
22
+ fn();
23
+ };
24
+ }
25
+
26
+ export { once, throwError, warn };
@@ -1 +1 @@
1
- import s from"./catalog/CatalogManager.js";import t from"./extractor/MessageExtractor.js";class o{constructor(o,a={}){const e=a.extractor??new t(a);this.manager=new s(o,{...a,extractor:e}),this[Symbol.dispose]=this[Symbol.dispose].bind(this),this.installExitHandlers()}async extractAll(){await this.manager.loadMessages(),await this.manager.save()}[Symbol.dispose](){this.uninstallExitHandlers(),this.manager.destroy()}installExitHandlers(){const s=this[Symbol.dispose];process.on("exit",s),process.on("SIGINT",s),process.on("SIGTERM",s)}uninstallExitHandlers(){const s=this[Symbol.dispose];process.off("exit",s),process.off("SIGINT",s),process.off("SIGTERM",s)}}export{o as default};
1
+ import s from"./catalog/CatalogManager.js";import t from"./extractor/MessageExtractor.js";class o{constructor(o,a={}){const e=a.extractor??new t(a);this.manager=new s(o,{...a,extractor:e}),this[Symbol.dispose]=this[Symbol.dispose].bind(this),this.installExitHandlers()}async extractAll(){await this.manager.loadMessages(),await this.manager.save()}[Symbol.dispose](){this.uninstallExitHandlers(),this.manager[Symbol.dispose]()}installExitHandlers(){const s=this[Symbol.dispose];process.on("exit",s),process.on("SIGINT",s),process.on("SIGTERM",s)}uninstallExitHandlers(){const s=this[Symbol.dispose];process.off("exit",s),process.off("SIGINT",s),process.off("SIGTERM",s)}}export{o as default};
@@ -1 +1 @@
1
- import e from"fs/promises";import s from"path";import{resolveCodec as t,getFormatExtension as a}from"../format/index.js";import o from"../source/SourceFileScanner.js";import i from"../source/SourceFileWatcher.js";import{getDefaultProjectRoot as r,localeCompare as c}from"../utils.js";import l from"./CatalogLocales.js";import n from"./CatalogPersister.js";import h from"./SaveScheduler.js";class g{messagesByFile=(()=>new Map)();messagesById=(()=>new Map)();translationsByTargetLocale=(()=>new Map)();lastWriteByLocale=(()=>new Map)();constructor(e,s){this.config=e,this.saveScheduler=new h(50),this.projectRoot=s.projectRoot??r(),this.isDevelopment=s.isDevelopment??!1,this.extractor=s.extractor,this.isDevelopment&&(this.sourceWatcher=new i(this.getSrcPaths(),this.handleFileEvents.bind(this)),this.sourceWatcher.start())}async getCodec(){return this.codec||(this.codec=await t(this.config.messages.format,this.projectRoot)),this.codec}async getPersister(){return this.persister||(this.persister=new n({messagesPath:this.config.messages.path,codec:await this.getCodec(),extension:a(this.config.messages.format)})),this.persister}getCatalogLocales(){if(this.catalogLocales)return this.catalogLocales;{const e=s.join(this.projectRoot,this.config.messages.path);return this.catalogLocales=new l({messagesDir:e,sourceLocale:this.config.sourceLocale,extension:a(this.config.messages.format),locales:this.config.messages.locales}),this.catalogLocales}}async getTargetLocales(){return this.getCatalogLocales().getTargetLocales()}getSrcPaths(){return(Array.isArray(this.config.srcPath)?this.config.srcPath:[this.config.srcPath]).map((e=>s.join(this.projectRoot,e)))}async loadMessages(){const e=await this.loadSourceMessages();this.loadCatalogsPromise=this.loadTargetMessages(),await this.loadCatalogsPromise;const s=await o.getSourceFiles(this.getSrcPaths());if(await Promise.all(Array.from(s).map((async e=>this.processFile(e)))),this.mergeSourceDiskMetadata(e),this.isDevelopment){this.getCatalogLocales().subscribeLocalesChange(this.onLocalesChange)}}async loadSourceMessages(){const e=await this.loadLocaleMessages(this.config.sourceLocale),s=new Map;for(const t of e)s.set(t.id,t);return s}async loadLocaleMessages(e){const s=await this.getPersister(),t=await s.read(e),a=await s.getLastModified(e);return this.lastWriteByLocale.set(e,a),t}async loadTargetMessages(){const e=await this.getTargetLocales();await Promise.all(e.map((e=>this.reloadLocaleCatalog(e))))}async reloadLocaleCatalog(e){const s=await this.loadLocaleMessages(e);if(e===this.config.sourceLocale)for(const e of s){const s=this.messagesById.get(e.id);if(s)for(const t of Object.keys(e))["id","message","description","references"].includes(t)||(s[t]=e[t])}else{const t=new Map;for(const e of s)t.set(e.id,e);this.translationsByTargetLocale.set(e,t)}}mergeSourceDiskMetadata(e){for(const[s,t]of e){const e=this.messagesById.get(s);if(e)for(const s of Object.keys(t))null==e[s]&&(e[s]=t[s])}}async processFile(t){let a=[];try{const s=await e.readFile(t,"utf8");a=(await this.extractor.extract(t,s)).messages}catch(e){if("ENOENT"!==e.code)throw e}const o=this.messagesByFile.get(t),i=Array.from(o?.keys()??[]),r=new Map;for(let e of a){const a=this.messagesById.get(e.id);if(a){const o=a.references??[];e={...e,references:this.mergeReferences(o,{path:s.relative(this.projectRoot,t)})};for(const s of Object.keys(a))null==e[s]&&(e[s]=a[s])}this.messagesById.set(e.id,e),r.set(e.id,e);const o=i.indexOf(e.id);-1!==o&&i.splice(o,1)}const c=s.relative(this.projectRoot,t);i.forEach((e=>{const s=this.messagesById.get(e);if(!s)return;const t=s.references?.some((e=>e.path!==c));t?s.references=s.references?.filter((e=>e.path!==c)):this.messagesById.delete(e)})),a.length>0?this.messagesByFile.set(t,r):this.messagesByFile.delete(t);return this.haveMessagesChangedForFile(o,r)}mergeReferences(e,s){const t=new Map;for(const s of e)t.set(s.path,s);return t.set(s.path,s),Array.from(t.values()).sort(((e,s)=>c(e.path,s.path)))}haveMessagesChangedForFile(e,s){if(!e)return s.size>0;if(e.size!==s.size)return!0;for(const[t,a]of e){const e=s.get(t);if(!e||!this.areMessagesEqual(a,e))return!0}return!1}areMessagesEqual(e,s){return e.id===s.id&&e.message===s.message&&e.description===s.description}async save(){return this.saveScheduler.schedule((()=>this.saveImpl()))}async saveImpl(){await this.saveLocale(this.config.sourceLocale);const e=await this.getTargetLocales();await Promise.all(e.map((e=>this.saveLocale(e))))}async saveLocale(e){await this.loadCatalogsPromise;const s=Array.from(this.messagesById.values()),t=await this.getPersister(),a=e===this.config.sourceLocale,o=this.lastWriteByLocale.get(e),i=await t.getLastModified(e);i&&o&&i>o&&await this.reloadLocaleCatalog(e);const r=a?this.messagesById:this.translationsByTargetLocale.get(e),c=s.map((e=>{const s=r?.get(e.id);return{...s,id:e.id,description:e.description,references:e.references,message:a?e.message:s?.message??""}}));await t.write(c,{locale:e,sourceMessagesById:this.messagesById});const l=await t.getLastModified(e);this.lastWriteByLocale.set(e,l)}onLocalesChange=async e=>{this.loadCatalogsPromise=Promise.all([this.loadCatalogsPromise,...e.added.map((e=>this.reloadLocaleCatalog(e)))]);for(const s of e.added)await this.saveLocale(s);for(const s of e.removed)this.translationsByTargetLocale.delete(s),this.lastWriteByLocale.delete(s)};async handleFileEvents(e){this.loadCatalogsPromise&&await this.loadCatalogsPromise;let s=!1;for(const t of e){const e=await this.processFile(t.path);s||=e}s&&await this.save()}destroy(){this.sourceWatcher?.stop(),this.sourceWatcher=void 0,this.saveScheduler.destroy(),this.catalogLocales&&this.isDevelopment&&this.catalogLocales.unsubscribeLocalesChange(this.onLocalesChange)}}export{g as default};
1
+ import e from"fs/promises";import s from"path";import{resolveCodec as t,getFormatExtension as a}from"../format/index.js";import o from"../source/SourceFileScanner.js";import i from"../source/SourceFileWatcher.js";import{getDefaultProjectRoot as r,compareReferences as c}from"../utils.js";import l from"./CatalogLocales.js";import n from"./CatalogPersister.js";import h from"./SaveScheduler.js";class g{messagesByFile=(()=>new Map)();messagesById=(()=>new Map)();translationsByTargetLocale=(()=>new Map)();lastWriteByLocale=(()=>new Map)();constructor(e,s){this.config=e,this.saveScheduler=new h(50),this.projectRoot=s.projectRoot??r(),this.isDevelopment=s.isDevelopment??!1,this.extractor=s.extractor,this.isDevelopment&&(this.sourceWatcher=new i(this.getSrcPaths(),this.handleFileEvents.bind(this)),this.sourceWatcher.start())}async getCodec(){return this.codec||(this.codec=await t(this.config.messages.format,this.projectRoot)),this.codec}async getPersister(){return this.persister||(this.persister=new n({messagesPath:this.config.messages.path,codec:await this.getCodec(),extension:a(this.config.messages.format)})),this.persister}getCatalogLocales(){if(this.catalogLocales)return this.catalogLocales;{const e=s.join(this.projectRoot,this.config.messages.path);return this.catalogLocales=new l({messagesDir:e,sourceLocale:this.config.sourceLocale,extension:a(this.config.messages.format),locales:this.config.messages.locales}),this.catalogLocales}}async getTargetLocales(){return this.getCatalogLocales().getTargetLocales()}getSrcPaths(){return(Array.isArray(this.config.srcPath)?this.config.srcPath:[this.config.srcPath]).map((e=>s.join(this.projectRoot,e)))}async loadMessages(){const e=await this.loadSourceMessages();if(this.loadCatalogsPromise=this.loadTargetMessages(),await this.loadCatalogsPromise,this.scanCompletePromise=(async()=>{const s=await o.getSourceFiles(this.getSrcPaths());await Promise.all(Array.from(s).map((async e=>this.processFile(e)))),this.mergeSourceDiskMetadata(e)})(),await this.scanCompletePromise,this.isDevelopment){this.getCatalogLocales().subscribeLocalesChange(this.onLocalesChange)}}async loadSourceMessages(){const e=await this.loadLocaleMessages(this.config.sourceLocale),s=new Map;for(const t of e)s.set(t.id,t);return s}async loadLocaleMessages(e){const s=await this.getPersister(),t=await s.read(e),a=await s.getLastModified(e);return this.lastWriteByLocale.set(e,a),t}async loadTargetMessages(){const e=await this.getTargetLocales();await Promise.all(e.map((e=>this.reloadLocaleCatalog(e))))}async reloadLocaleCatalog(e){const s=await this.loadLocaleMessages(e);if(e===this.config.sourceLocale)for(const e of s){const s=this.messagesById.get(e.id);if(s)for(const t of Object.keys(e))["id","message","description","references"].includes(t)||(s[t]=e[t])}else{const t=this.translationsByTargetLocale.get(e),a=t&&t.size>0;if(s.length>0){const t=new Map;for(const e of s)t.set(e.id,e);this.translationsByTargetLocale.set(e,t)}else if(a);else{const s=new Map;this.translationsByTargetLocale.set(e,s)}}}mergeSourceDiskMetadata(e){for(const[s,t]of e){const e=this.messagesById.get(s);if(e)for(const s of Object.keys(t))null==e[s]&&(e[s]=t[s])}}async processFile(t){let a=[];try{const s=await e.readFile(t,"utf8");let o;try{o=await this.extractor.extract(t,s)}catch{return!1}a=o.messages}catch(e){if("ENOENT"!==e.code)throw e}const o=this.messagesByFile.get(t),i=s.relative(this.projectRoot,t),r=Array.from(o?.keys()??[]),c=new Map;for(let e of a){const s=this.messagesById.get(e.id);if(s){e={...e},e.references&&(e.references=this.mergeReferences(s.references??[],i,e.references));for(const t of Object.keys(s))null==e[t]&&(e[t]=s[t])}this.messagesById.set(e.id,e),c.set(e.id,e);const t=r.indexOf(e.id);-1!==t&&r.splice(t,1)}r.forEach((e=>{const s=this.messagesById.get(e);if(!s)return;const t=s.references?.some((e=>e.path!==i));t?s.references=s.references?.filter((e=>e.path!==i)):this.messagesById.delete(e)})),a.length>0?this.messagesByFile.set(t,c):this.messagesByFile.delete(t);return this.haveMessagesChangedForFile(o,c)}mergeReferences(e,s,t){return[...e.filter((e=>e.path!==s)),...t].sort(c)}haveMessagesChangedForFile(e,s){if(!e)return s.size>0;if(e.size!==s.size)return!0;for(const[t,a]of e){const e=s.get(t);if(!e||!this.areMessagesEqual(a,e))return!0}return!1}areMessagesEqual(e,s){return e.id===s.id&&e.message===s.message&&e.description===s.description}async save(){return this.saveScheduler.schedule((()=>this.saveImpl()))}async saveImpl(){await this.saveLocale(this.config.sourceLocale);const e=await this.getTargetLocales();await Promise.all(e.map((e=>this.saveLocale(e))))}async saveLocale(e){await this.loadCatalogsPromise;const s=Array.from(this.messagesById.values()),t=await this.getPersister(),a=e===this.config.sourceLocale,o=this.lastWriteByLocale.get(e),i=await t.getLastModified(e);i&&o&&i>o&&await this.reloadLocaleCatalog(e);const r=a?this.messagesById:this.translationsByTargetLocale.get(e),c=s.map((e=>{const s=r?.get(e.id);return{...s,id:e.id,description:e.description,references:e.references,message:a?e.message:s?.message??""}}));await t.write(c,{locale:e,sourceMessagesById:this.messagesById});const l=await t.getLastModified(e);this.lastWriteByLocale.set(e,l)}onLocalesChange=async e=>{this.loadCatalogsPromise=Promise.all([this.loadCatalogsPromise,...e.added.map((e=>this.reloadLocaleCatalog(e)))]);for(const s of e.added)await this.saveLocale(s);for(const s of e.removed)this.translationsByTargetLocale.delete(s),this.lastWriteByLocale.delete(s)};async handleFileEvents(e){this.loadCatalogsPromise&&await this.loadCatalogsPromise,this.scanCompletePromise&&await this.scanCompletePromise;let s=!1;const t=await this.sourceWatcher.expandDirectoryDeleteEvents(e,Array.from(this.messagesByFile.keys()));for(const e of t){const t=await this.processFile(e.path);s||=t}s&&await this.save()}[Symbol.dispose](){this.sourceWatcher?.stop(),this.sourceWatcher=void 0,this.saveScheduler[Symbol.dispose](),this.catalogLocales&&this.isDevelopment&&this.catalogLocales.unsubscribeLocalesChange(this.onLocalesChange)}}export{g as default};
@@ -1 +1 @@
1
- class e{isSaving=!1;pendingResolvers=[];constructor(e=50){this.delayMs=e}async schedule(e){return new Promise(((s,i)=>{this.pendingResolvers.push({resolve:s,reject:i}),this.nextSaveTask=e,this.isSaving||this.saveTimeout?this.saveTimeout&&this.scheduleSave():this.executeSave()}))}scheduleSave(){this.saveTimeout&&clearTimeout(this.saveTimeout),this.saveTimeout=setTimeout((()=>{this.saveTimeout=void 0,this.executeSave()}),this.delayMs)}async executeSave(){if(this.isSaving)return;const e=this.nextSaveTask;if(!e)return;const s=this.pendingResolvers;this.pendingResolvers=[],this.nextSaveTask=void 0,this.isSaving=!0;try{const i=await e();s.forEach((({resolve:e})=>e(i)))}catch(e){s.forEach((({reject:s})=>s(e)))}finally{this.isSaving=!1,this.pendingResolvers.length>0&&this.scheduleSave()}}destroy(){this.saveTimeout&&(clearTimeout(this.saveTimeout),this.saveTimeout=void 0),this.pendingResolvers=[],this.nextSaveTask=void 0,this.isSaving=!1}}export{e as default};
1
+ class e{isSaving=!1;pendingResolvers=[];constructor(e=50){this.delayMs=e}async schedule(e){return new Promise(((s,i)=>{this.pendingResolvers.push({resolve:s,reject:i}),this.nextSaveTask=e,this.isSaving||this.saveTimeout?this.saveTimeout&&this.scheduleSave():this.executeSave()}))}scheduleSave(){this.saveTimeout&&clearTimeout(this.saveTimeout),this.saveTimeout=setTimeout((()=>{this.saveTimeout=void 0,this.executeSave()}),this.delayMs)}async executeSave(){if(this.isSaving)return;const e=this.nextSaveTask;if(!e)return;const s=this.pendingResolvers;this.pendingResolvers=[],this.nextSaveTask=void 0,this.isSaving=!0;try{const i=await e();s.forEach((({resolve:e})=>e(i)))}catch(e){s.forEach((({reject:s})=>s(e)))}finally{this.isSaving=!1,this.pendingResolvers.length>0&&this.scheduleSave()}}[Symbol.dispose](){this.saveTimeout&&(clearTimeout(this.saveTimeout),this.saveTimeout=void 0),this.pendingResolvers=[],this.nextSaveTask=void 0,this.isSaving=!1}}export{e as default};
@@ -1 +1 @@
1
- import t from"./ExtractionCompiler.js";import e from"./extractor/MessageExtractor.js";let o,r,s;function c(c){const a=this.getOptions(),i=this.async(),n=this.rootContext,p="development"===process.env["NODE_ENV".trim()];r||(r=new e({isDevelopment:p,projectRoot:n,sourceMap:this.sourceMap})),o||(o=new t(a,{isDevelopment:p,projectRoot:n,sourceMap:this.sourceMap,extractor:r})),s||(s=o.extractAll()),r.extract(this.resourcePath,c).then((async t=>{p||await s,i(null,t.code,t.map)})).catch(i)}export{c as default};
1
+ import t from"./extractor/MessageExtractor.js";let e;function o(o){const r=this.async(),s=this.rootContext,c="development"===process.env["NODE_ENV".trim()];e||(e=new t({isDevelopment:c,projectRoot:s,sourceMap:this.sourceMap})),e.extract(this.resourcePath,o).then((t=>{r(null,t.code,t.map)})).catch(r)}export{o as default};
@@ -1 +1 @@
1
- import{subscribe as s}from"@parcel/watcher";import t from"./SourceFileFilter.js";class o{subscriptions=[];constructor(s,t){this.roots=s,this.onChange=t}async start(){if(this.subscriptions.length>0)return;const o=t.IGNORED_DIRECTORIES.map((s=>`**/${s}/**`));for(const i of this.roots){const r=await s(i,((s,o)=>{if(s)return void console.error(s);const i=o.filter((s=>t.isSourceFile(s.path)));i.length>0&&this.onChange(i)}),{ignore:o});this.subscriptions.push(r)}}async stop(){await Promise.all(this.subscriptions.map((s=>s.unsubscribe()))),this.subscriptions=[]}[Symbol.dispose](){this.stop()}}export{o as default};
1
+ import t from"fs/promises";import s from"path";import{subscribe as e}from"@parcel/watcher";import o from"./SourceFileFilter.js";import r from"./SourceFileScanner.js";class i{subscriptions=[];constructor(t,s){this.roots=t,this.onChange=s}async start(){if(this.subscriptions.length>0)return;const t=o.IGNORED_DIRECTORIES.map((t=>`**/${t}/**`));for(const s of this.roots){const o=await e(s,(async(t,s)=>{if(t)return void console.error(t);const e=await this.normalizeEvents(s);e.length>0&&this.onChange(e)}),{ignore:t});this.subscriptions.push(o)}}async normalizeEvents(s){const e=[],i=[];await Promise.all(s.map((async s=>{if("create"===s.type)try{if((await t.stat(s.path)).isDirectory())return void e.push(s.path)}catch{}i.push(s)})));let a=[];if(e.length>0)try{const t=await r.getSourceFiles(e);a=Array.from(t).map((t=>({type:"create",path:t})))}catch{}const n=[...i,...a],c=new Set,p=[];for(const t of n){const s=`${t.type}:${t.path}`;c.has(s)||(c.add(s),p.push(t))}return p.filter((t=>"delete"===t.type||o.isSourceFile(t.path)))}async expandDirectoryDeleteEvents(t,e){const r=[];for(const i of t)if("delete"!==i.type||o.isSourceFile(i.path))r.push(i);else{const t=s.resolve(i.path),a=[];for(const s of e)o.isWithinPath(s,t)&&a.push(s);if(a.length>0)for(const t of a)r.push({type:"delete",path:t});else r.push(i)}return r}async stop(){await Promise.all(this.subscriptions.map((t=>t.unsubscribe()))),this.subscriptions=[]}[Symbol.dispose](){this.stop()}}export{i as default};
@@ -1 +1 @@
1
- function e(e,t,n){const r=t.split(".");let o=e;for(let e=0;e<r.length-1;e++){const t=r[e];t in o&&"object"==typeof o[t]&&null!==o[t]||(o[t]={}),o=o[t]}o[r[r.length-1]]=n}function t(e){return e.toSorted(((e,t)=>{const r=e.references?.[0]?.path??"",o=t.references?.[0]?.path??"";return r===o?n(e.id,t.id):n(r,o)}))}function n(e,t){return e.localeCompare(t,"en")}function r(){return process.cwd()}export{r as getDefaultProjectRoot,t as getSortedMessages,n as localeCompare,e as setNestedProperty};
1
+ function n(n,e,t){const r=e.split(".");let o=n;for(let n=0;n<r.length-1;n++){const e=r[n];e in o&&"object"==typeof o[e]&&null!==o[e]||(o[e]={}),o=o[e]}o[r[r.length-1]]=t}function e(n){return n.toSorted(((n,e)=>{const t=n.references?.[0],o=e.references?.[0];return t&&o?r(t,o):0}))}function t(n,e){return n.localeCompare(e,"en")}function r(n,e){const r=t(n.path,e.path);return 0!==r?r:(n.line??0)-(e.line??0)}function o(){return process.cwd()}export{r as compareReferences,o as getDefaultProjectRoot,e as getSortedMessages,t as localeCompare,n as setNestedProperty};
@@ -1 +1 @@
1
- import e from"./getNextConfig.js";import{warn as t}from"./utils.js";import r from"./declaration/createMessagesDeclaration.js";function o(o={}){const n="string"==typeof o?{requestConfig:o}:o;return function(o){return function(o,n){null!=n?.i18n&&t("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");const i=o.experimental?.createMessagesDeclaration;return i&&r("string"==typeof i?[i]:i),e(o,n)}(n,o)}}export{o as default};
1
+ import e from"./extractor/initExtractionCompiler.js";import t from"./getNextConfig.js";import{warn as r}from"./utils.js";import o from"./declaration/createMessagesDeclaration.js";function n(n={}){const i="string"==typeof n?{requestConfig:n}:n;return function(n){return function(n,i){null!=i?.i18n&&r("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");const s=n.experimental?.createMessagesDeclaration;return s&&o("string"==typeof s?[s]:s),e(n),t(n,i)}(i,n)}}export{n as default};
@@ -1 +1 @@
1
- import e from"fs";import s from"path";import{throwError as t}from"../utils.js";import o from"../watchFile.js";function n(o){var n;["info","start"].some((e=>process.argv.includes(e)))||(n=()=>{for(const n of o){const o=s.resolve(n);e.existsSync(o)||t(`\`createMessagesDeclaration\` points to a non-existent file: ${o}`),o.endsWith(".json")||t(`\`createMessagesDeclaration\` needs to point to a JSON file. Received: ${o}`);const c=process.env["NODE_ENV".trim()];i(n),"development"===c&&r(n)}},"1"!==process.env._NEXT_INTL_COMPILE_MESSAGES&&(process.env._NEXT_INTL_COMPILE_MESSAGES="1",n()))}function r(e){const s=o(e,(()=>{i(e,!0)}));process.on("exit",(()=>{s.close()}))}function i(s,t=!1){const o=s.replace(/\.json$/,".d.json.ts");function n(e){return`// This file is auto-generated by next-intl, do not edit directly.\n// See: https://next-intl.dev/docs/workflows/typescript#messages-arguments\n\ndeclare const messages: ${e.trim()};\nexport default messages;`}if(t)return e.promises.readFile(s,"utf-8").then((s=>e.promises.writeFile(o,n(s))));const r=e.readFileSync(s,"utf-8");e.writeFileSync(o,n(r))}export{n as default};
1
+ import e from"fs";import t from"path";import{once as s,throwError as o}from"../utils.js";import n from"../watchFile.js";const i=s("_NEXT_INTL_COMPILE_MESSAGES");function r(s){["info","start"].some((e=>process.argv.includes(e)))||i((()=>{for(const n of s){const s=t.resolve(n);e.existsSync(s)||o(`\`createMessagesDeclaration\` points to a non-existent file: ${s}`),s.endsWith(".json")||o(`\`createMessagesDeclaration\` needs to point to a JSON file. Received: ${s}`);const i=process.env["NODE_ENV".trim()];a(n),"development"===i&&c(n)}}))}function c(e){const t=n(e,(()=>{a(e,!0)}));process.on("exit",(()=>{t.close()}))}function a(t,s=!1){const o=t.replace(/\.json$/,".d.json.ts");function n(e){return`// This file is auto-generated by next-intl, do not edit directly.\n// See: https://next-intl.dev/docs/workflows/typescript#messages-arguments\n\ndeclare const messages: ${e.trim()};\nexport default messages;`}if(s)return e.promises.readFile(t,"utf-8").then((t=>e.promises.writeFile(o,n(t))));const i=e.readFileSync(t,"utf-8");e.writeFileSync(o,n(i))}export{r as default};
@@ -0,0 +1 @@
1
+ import e from"../../extractor/ExtractionCompiler.js";import{once as o}from"../utils.js";let s;const t=o("_NEXT_INTL_EXTRACT");function r(o){const r=o.experimental;if(!r?.extract)return;const c="development"===process.env["NODE_ENV".trim()];(c||process.argv.includes("build"))&&t((()=>{const o={srcPath:r.srcPath,sourceLocale:r.extract.sourceLocale,messages:r.messages};function t(){s&&(s[Symbol.dispose](),s=void 0)}s=new e(o,{isDevelopment:c,projectRoot:process.cwd()}),s.extractAll(),process.on("exit",t),process.on("SIGINT",t),process.on("SIGTERM",t)}))}export{r as default};
@@ -1 +1 @@
1
- function n(n){return`\n[next-intl] ${n}\n`}function o(o){throw new Error(n(o))}function r(o){console.warn(n(o))}export{o as throwError,r as warn};
1
+ function n(n){return`\n[next-intl] ${n}\n`}function o(o){throw new Error(n(o))}function r(o){console.warn(n(o))}function t(n){return function(o){"1"!==process.env[n]&&(process.env[n]="1",o())}}export{t as once,o as throwError,r as warn};
@@ -1,6 +1,6 @@
1
1
  import type MessageExtractor from '../extractor/MessageExtractor.js';
2
2
  import type { ExtractorConfig } from '../types.js';
3
- export default class CatalogManager {
3
+ export default class CatalogManager implements Disposable {
4
4
  private config;
5
5
  /**
6
6
  * The source of truth for which messages are used.
@@ -27,7 +27,8 @@ export default class CatalogManager {
27
27
  private catalogLocales?;
28
28
  private extractor;
29
29
  private sourceWatcher?;
30
- loadCatalogsPromise?: Promise<unknown>;
30
+ private loadCatalogsPromise?;
31
+ private scanCompletePromise?;
31
32
  constructor(config: ExtractorConfig, opts: {
32
33
  projectRoot?: string;
33
34
  isDevelopment?: boolean;
@@ -38,14 +39,14 @@ export default class CatalogManager {
38
39
  private getPersister;
39
40
  private getCatalogLocales;
40
41
  private getTargetLocales;
41
- getSrcPaths(): Array<string>;
42
+ private getSrcPaths;
42
43
  loadMessages(): Promise<void>;
43
44
  private loadSourceMessages;
44
45
  private loadLocaleMessages;
45
46
  private loadTargetMessages;
46
47
  private reloadLocaleCatalog;
47
48
  private mergeSourceDiskMetadata;
48
- processFile(absoluteFilePath: string): Promise<boolean>;
49
+ private processFile;
49
50
  private mergeReferences;
50
51
  private haveMessagesChangedForFile;
51
52
  private areMessagesEqual;
@@ -54,5 +55,5 @@ export default class CatalogManager {
54
55
  private saveLocale;
55
56
  private onLocalesChange;
56
57
  private handleFileEvents;
57
- destroy(): void;
58
+ [Symbol.dispose](): void;
58
59
  }
@@ -3,7 +3,7 @@ type SaveTask<T> = () => Promise<T>;
3
3
  * De-duplicates excessive save invocations,
4
4
  * while keeping a single one instant.
5
5
  */
6
- export default class SaveScheduler<Value> {
6
+ export default class SaveScheduler<Value> implements Disposable {
7
7
  private saveTimeout?;
8
8
  private isSaving;
9
9
  private delayMs;
@@ -13,6 +13,6 @@ export default class SaveScheduler<Value> {
13
13
  schedule(saveTask: SaveTask<Value>): Promise<Value>;
14
14
  private scheduleSave;
15
15
  private executeSave;
16
- destroy(): void;
16
+ [Symbol.dispose](): void;
17
17
  }
18
18
  export {};
@@ -4,5 +4,5 @@ export default class SourceFileFilter {
4
4
  static isSourceFile(filePath: string): boolean;
5
5
  static shouldEnterDirectory(dirPath: string, srcPaths: Array<string>): boolean;
6
6
  private static isIgnoredDirectoryExplicitlyIncluded;
7
- private static isWithinPath;
7
+ static isWithinPath(targetPath: string, basePath: string): boolean;
8
8
  }
@@ -1,11 +1,14 @@
1
1
  import { type Event } from '@parcel/watcher';
2
2
  type OnChange = (events: Array<Event>) => Promise<void>;
3
+ export type SourceFileWatcherEvent = Event;
3
4
  export default class SourceFileWatcher implements Disposable {
4
5
  private subscriptions;
5
6
  private roots;
6
7
  private onChange;
7
8
  constructor(roots: Array<string>, onChange: OnChange);
8
9
  start(): Promise<void>;
10
+ private normalizeEvents;
11
+ expandDirectoryDeleteEvents(events: Array<Event>, prevKnownFiles: Array<string>): Promise<Array<Event>>;
9
12
  stop(): Promise<void>;
10
13
  [Symbol.dispose](): void;
11
14
  }
@@ -1,12 +1,14 @@
1
1
  import type { MessagesFormat } from './format/types.js';
2
2
  export type Locale = string;
3
+ export type ExtractorMessageReference = {
4
+ path: string;
5
+ line?: number;
6
+ };
3
7
  export type ExtractorMessage = {
4
8
  id: string;
5
9
  message: string;
6
10
  description?: string;
7
- references?: Array<{
8
- path: string;
9
- }>;
11
+ references?: Array<ExtractorMessageReference>;
10
12
  /** Allows for additional properties like .po flags to be read and later written. */
11
13
  [key: string]: unknown;
12
14
  };
@@ -1,5 +1,6 @@
1
- import type { ExtractorMessage } from './types.js';
1
+ import type { ExtractorMessage, ExtractorMessageReference } from './types.js';
2
2
  export declare function setNestedProperty(obj: Record<string, any>, keyPath: string, value: any): void;
3
3
  export declare function getSortedMessages(messages: Array<ExtractorMessage>): Array<ExtractorMessage>;
4
4
  export declare function localeCompare(a: string, b: string): number;
5
+ export declare function compareReferences(refA: ExtractorMessageReference, refB: ExtractorMessageReference): number;
5
6
  export declare function getDefaultProjectRoot(): string;
@@ -0,0 +1,2 @@
1
+ import type { PluginConfig } from '../types.js';
2
+ export default function initExtractionCompiler(pluginConfig: PluginConfig): void;
@@ -1,2 +1,8 @@
1
1
  export declare function throwError(message: string): never;
2
2
  export declare function warn(message: string): void;
3
+ /**
4
+ * Returns a function that runs the provided callback only once per process.
5
+ * Next.js can call the config multiple times - this ensures we only run once.
6
+ * Uses an environment variable to track execution across config loads.
7
+ */
8
+ export declare function once(namespace: string): (fn: () => void) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-intl",
3
- "version": "4.6.0",
3
+ "version": "4.7.0",
4
4
  "sideEffects": false,
5
5
  "author": "Jan Amann <jan@amann.work>",
6
6
  "funding": [
@@ -128,9 +128,9 @@
128
128
  "@parcel/watcher": "^2.4.1",
129
129
  "@swc/core": "^1.15.2",
130
130
  "negotiator": "^1.0.0",
131
- "next-intl-swc-plugin-extractor": "^4.6.0",
132
- "po-parser": "^2.0.0",
133
- "use-intl": "^4.6.0"
131
+ "next-intl-swc-plugin-extractor": "^4.7.0",
132
+ "po-parser": "^2.1.1",
133
+ "use-intl": "^4.7.0"
134
134
  },
135
135
  "peerDependencies": {
136
136
  "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
@@ -142,5 +142,5 @@
142
142
  "optional": true
143
143
  }
144
144
  },
145
- "gitHead": "70a047daa5cb235fdcadcf6ebf1daed32ad30c85"
145
+ "gitHead": "f7da9b11eb59dcc83525500ff9af14110ea9bea9"
146
146
  }
@@ -1,37 +0,0 @@
1
- 'use strict';
2
-
3
- // Essentialls lodash/set, but we avoid this dependency
4
- function setNestedProperty(obj, keyPath, value) {
5
- const keys = keyPath.split('.');
6
- let current = obj;
7
- for (let i = 0; i < keys.length - 1; i++) {
8
- const key = keys[i];
9
- if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
10
- current[key] = {};
11
- }
12
- current = current[key];
13
- }
14
- current[keys[keys.length - 1]] = value;
15
- }
16
- function getSortedMessages(messages) {
17
- return messages.toSorted((messageA, messageB) => {
18
- const pathA = messageA.references?.[0]?.path ?? '';
19
- const pathB = messageB.references?.[0]?.path ?? '';
20
- if (pathA === pathB) {
21
- return localeCompare(messageA.id, messageB.id);
22
- } else {
23
- return localeCompare(pathA, pathB);
24
- }
25
- });
26
- }
27
- function localeCompare(a, b) {
28
- return a.localeCompare(b, 'en');
29
- }
30
-
31
- function defineCodec(factory) {
32
- return factory;
33
- }
34
-
35
- exports.defineCodec = defineCodec;
36
- exports.getSortedMessages = getSortedMessages;
37
- exports.setNestedProperty = setNestedProperty;
@@ -1,12 +0,0 @@
1
- type VarInfo = {
2
- kind: string;
3
- namespace?: string;
4
- };
5
- export default class ASTScope {
6
- parent?: ASTScope;
7
- vars: Map<string, VarInfo>;
8
- constructor(parent?: ASTScope);
9
- define(name: string, kind: string, namespace?: string): void;
10
- lookup(name: string): VarInfo | undefined;
11
- }
12
- export {};