next-intl 4.5.4 → 4.5.6

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.
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import MessageExtractor from '../extractor/MessageExtractor.js';
4
4
  import formatters from '../formatters/index.js';
5
5
  import SourceFileScanner from '../source/SourceFileScanner.js';
6
+ import { localeCompare } from '../utils.js';
6
7
  import CatalogLocales from './CatalogLocales.js';
7
8
  import CatalogPersister from './CatalogPersister.js';
8
9
  import SaveScheduler from './SaveScheduler.js';
@@ -24,6 +25,9 @@ class CatalogManager {
24
25
 
25
26
  // Cached instances
26
27
 
28
+ // Resolves when all catalogs are loaded
29
+ // (but doesn't indicate that project scan is done)
30
+
27
31
  constructor(config, opts = {}) {
28
32
  this.config = config;
29
33
  this.saveScheduler = new SaveScheduler(50);
@@ -74,24 +78,41 @@ class CatalogManager {
74
78
  getSrcPaths() {
75
79
  return (Array.isArray(this.config.srcPath) ? this.config.srcPath : [this.config.srcPath]).map(srcPath => path.join(this.projectRoot, srcPath));
76
80
  }
77
- getFileMessages(absoluteFilePath) {
78
- return this.messagesByFile.get(absoluteFilePath);
79
- }
80
81
  async loadMessages() {
81
- await this.loadSourceMessages();
82
- await this.loadTargetMessages();
82
+ this.loadCatalogsPromise = Promise.all([this.loadSourceMessages(), this.loadTargetMessages()]);
83
+
84
+ // Ensure catalogs are loaded before scanning source files.
85
+ // Otherwise, `loadSourceMessages` might overwrite extracted
86
+ // messages if it finishes after source file extraction.
87
+ await this.loadCatalogsPromise;
83
88
  if (this.isDevelopment) {
84
89
  const catalogLocales = await this.getCatalogLocales();
85
90
  catalogLocales.subscribeLocalesChange(this.onLocalesChange);
86
91
  }
92
+ const sourceFiles = await SourceFileScanner.getSourceFiles(this.getSrcPaths());
93
+ await Promise.all(sourceFiles.map(async filePath => this.extractFileMessages(filePath, await fs.readFile(filePath, 'utf8'))));
87
94
  }
88
95
  async loadSourceMessages() {
89
96
  // First hydrate from source locale file to potentially init metadata
90
- await this.loadLocaleMessages(this.config.sourceLocale);
91
-
92
- // Then extract from all source files
93
- const sourceFiles = await SourceFileScanner.getSourceFiles(this.getSrcPaths());
94
- await Promise.all(sourceFiles.map(async filePath => this.extractFileMessages(filePath, await fs.readFile(filePath, 'utf8'))));
97
+ const messages = await this.loadLocaleMessages(this.config.sourceLocale);
98
+ const messagesById = new Map();
99
+ const messagesByFile = new Map();
100
+ for (const message of messages) {
101
+ messagesById.set(message.id, message);
102
+ if (message.references) {
103
+ for (const ref of message.references) {
104
+ const absoluteFilePath = path.join(this.projectRoot, ref.path);
105
+ let fileMessages = messagesByFile.get(absoluteFilePath);
106
+ if (!fileMessages) {
107
+ fileMessages = new Map();
108
+ messagesByFile.set(absoluteFilePath, fileMessages);
109
+ }
110
+ fileMessages.set(message.id, message);
111
+ }
112
+ }
113
+ }
114
+ this.messagesById = messagesById;
115
+ this.messagesByFile = messagesByFile;
95
116
  }
96
117
  async loadLocaleMessages(locale) {
97
118
  const persister = await this.getPersister();
@@ -106,14 +127,34 @@ class CatalogManager {
106
127
  }
107
128
  async loadTargetMessages() {
108
129
  const targetLocales = await this.getTargetLocales();
109
- await Promise.all(targetLocales.map(async locale => {
110
- this.translationsByTargetLocale.set(locale, new Map());
111
- const messages = await this.loadLocaleMessages(locale);
112
- for (const message of messages) {
113
- const translations = this.translationsByTargetLocale.get(locale);
114
- translations.set(message.id, message.message);
130
+ await Promise.all(targetLocales.map(locale => this.reloadLocaleCatalog(locale)));
131
+ }
132
+ async reloadLocaleCatalog(locale) {
133
+ const diskMessages = await this.loadLocaleMessages(locale);
134
+ if (locale === this.config.sourceLocale) {
135
+ // For source: Merge additional properties like flags
136
+ for (const diskMessage of diskMessages) {
137
+ const prev = this.messagesById.get(diskMessage.id);
138
+ if (prev) {
139
+ // Unknown properties (like flags): disk wins
140
+ // Known properties: existing (from extraction) wins
141
+ this.messagesById.set(diskMessage.id, {
142
+ ...diskMessage,
143
+ id: prev.id,
144
+ message: prev.message,
145
+ description: prev.description,
146
+ references: prev.references
147
+ });
148
+ }
115
149
  }
116
- }));
150
+ } else {
151
+ // For target: disk wins completely
152
+ const translations = new Map();
153
+ for (const message of diskMessages) {
154
+ translations.set(message.id, message);
155
+ }
156
+ this.translationsByTargetLocale.set(locale, translations);
157
+ }
117
158
  }
118
159
  async extractFileMessages(absoluteFilePath, source) {
119
160
  const result = await this.messageExtractor.processFileContent(absoluteFilePath, source);
@@ -138,18 +179,18 @@ class CatalogManager {
138
179
  references.push(ref);
139
180
  }
140
181
  });
141
- references.sort((referenceA, referenceB) => referenceA.path.localeCompare(referenceB.path));
182
+ references.sort((referenceA, referenceB) => localeCompare(referenceA.path, referenceB.path));
142
183
  message = {
143
184
  ...message,
144
185
  references
145
186
  };
146
187
 
147
- // Description: In case we have conflicting descriptions, the new one wins.
148
- if (prevMessage.description && !message.description) {
149
- message = {
150
- ...message,
151
- description: prevMessage.description
152
- };
188
+ // Merge other properties like description, or unknown
189
+ // attributes like flags that are opaque to us
190
+ for (const key of Object.keys(prevMessage)) {
191
+ if (message[key] == null) {
192
+ message[key] = prevMessage[key];
193
+ }
153
194
  }
154
195
  }
155
196
  this.messagesById.set(message.id, message);
@@ -179,13 +220,13 @@ class CatalogManager {
179
220
  } else {
180
221
  this.messagesByFile.delete(absoluteFilePath);
181
222
  }
182
- const changed = this.haveMessagesChanged(prevFileMessages, fileMessages);
223
+ const changed = this.haveMessagesChangedForFile(prevFileMessages, fileMessages);
183
224
  return {
184
225
  ...result,
185
226
  changed
186
227
  };
187
228
  }
188
- haveMessagesChanged(beforeMessages, afterMessages) {
229
+ haveMessagesChangedForFile(beforeMessages, afterMessages) {
189
230
  // If one exists and the other doesn't, there's a change
190
231
  if (!beforeMessages) {
191
232
  return afterMessages.size > 0;
@@ -206,56 +247,43 @@ class CatalogManager {
206
247
  return false;
207
248
  }
208
249
  areMessagesEqual(msg1, msg2) {
209
- return msg1.id === msg2.id && msg1.message === msg2.message && msg1.description === msg2.description && this.areReferencesEqual(msg1.references, msg2.references);
210
- }
211
- areReferencesEqual(refs1, refs2) {
212
- // Both undefined or both empty
213
- if (!refs1 && !refs2) return true;
214
- if (!refs1 || !refs2) return false;
215
- if (refs1.length !== refs2.length) return false;
216
-
217
- // Compare each reference
218
- for (let i = 0; i < refs1.length; i++) {
219
- if (refs1[i].path !== refs2[i].path) {
220
- return false;
221
- }
222
- }
223
- return true;
250
+ // Note: We intentionally don't compare references here.
251
+ // References are aggregated metadata from multiple files and comparing
252
+ // them would cause false positives due to parallel extraction order.
253
+ return msg1.id === msg2.id && msg1.message === msg2.message && msg1.description === msg2.description;
224
254
  }
225
255
  async save() {
226
256
  return this.saveScheduler.schedule(() => this.saveImpl());
227
257
  }
228
258
  async saveImpl() {
229
- const messages = Array.from(this.messagesById.values());
230
- const persister = await this.getPersister();
231
- await persister.write(this.config.sourceLocale, messages);
232
- for (const locale of await this.getTargetLocales()) {
233
- await this.saveLocale(locale);
234
- }
235
- return messages.length;
259
+ await this.saveLocale(this.config.sourceLocale);
260
+ const targetLocales = await this.getTargetLocales();
261
+ await Promise.all(targetLocales.map(locale => this.saveLocale(locale)));
236
262
  }
237
263
  async saveLocale(locale) {
264
+ await this.loadCatalogsPromise;
238
265
  const messages = Array.from(this.messagesById.values());
239
266
  const persister = await this.getPersister();
267
+ const isSourceLocale = locale === this.config.sourceLocale;
240
268
 
241
- // Check if file was modified externally
269
+ // Check if file was modified externally (poll-at-save is cheaper than
270
+ // watchers here since stat() is fast and avoids continuous overhead)
242
271
  const lastWriteTime = this.lastWriteByLocale.get(locale);
243
272
  const currentFileTime = await persister.getLastModified(locale);
244
-
245
- // If file was modified externally, read and merge
246
273
  if (currentFileTime && lastWriteTime && currentFileTime > lastWriteTime) {
247
- const diskMessages = await persister.read(locale);
248
- const translations = this.translationsByTargetLocale.get(locale);
249
- for (const diskMessage of diskMessages) {
250
- // Disk wins: preserve manual edits
251
- translations.set(diskMessage.id, diskMessage.message);
252
- }
274
+ await this.reloadLocaleCatalog(locale);
253
275
  }
254
- const translations = this.translationsByTargetLocale.get(locale);
255
- const localeMessages = messages.map(message => ({
256
- ...message,
257
- message: translations.get(message.id) || ''
258
- }));
276
+ const prevMessages = isSourceLocale ? this.messagesById : this.translationsByTargetLocale.get(locale);
277
+ const localeMessages = messages.map(message => {
278
+ const prev = prevMessages?.get(message.id);
279
+ return {
280
+ ...prev,
281
+ id: message.id,
282
+ description: message.description,
283
+ references: message.references,
284
+ message: isSourceLocale ? message.message : prev?.message ?? ''
285
+ };
286
+ });
259
287
  await persister.write(locale, localeMessages);
260
288
 
261
289
  // Update timestamps
@@ -263,13 +291,9 @@ class CatalogManager {
263
291
  this.lastWriteByLocale.set(locale, newTime);
264
292
  }
265
293
  onLocalesChange = async params => {
294
+ // Chain to existing promise
295
+ this.loadCatalogsPromise = Promise.all([this.loadCatalogsPromise, ...params.added.map(locale => this.reloadLocaleCatalog(locale))]);
266
296
  for (const locale of params.added) {
267
- const translations = new Map();
268
- this.translationsByTargetLocale.set(locale, translations);
269
- const messages = await this.loadLocaleMessages(locale);
270
- for (const message of messages) {
271
- translations.set(message.id, message.message);
272
- }
273
297
  await this.saveLocale(locale);
274
298
  }
275
299
  for (const locale of params.removed) {
@@ -35,6 +35,7 @@ class MessageExtractor {
35
35
  decorators: true
36
36
  },
37
37
  experimental: {
38
+ cacheRoot: 'node_modules/.cache/swc',
38
39
  disableBuiltinTransformsForInternalTesting: true,
39
40
  disableAllLints: true,
40
41
  plugins: [[require.resolve('next-intl-swc-plugin-extractor'), {
@@ -1,4 +1,4 @@
1
- import { setNestedProperty } from '../utils/ObjectUtils.js';
1
+ import { setNestedProperty } from '../utils.js';
2
2
  import Formatter from './Formatter.js';
3
3
  import { getSortedMessages } from './utils.js';
4
4
 
@@ -1,5 +1,5 @@
1
1
  import POParser from 'po-parser';
2
- import { setNestedProperty } from '../utils/ObjectUtils.js';
2
+ import { setNestedProperty } from '../utils.js';
3
3
  import Formatter from './Formatter.js';
4
4
  import { getSortedMessages } from './utils.js';
5
5
 
@@ -1,11 +1,13 @@
1
+ import { localeCompare } from '../utils.js';
2
+
1
3
  function getSortedMessages(messages) {
2
4
  return messages.toSorted((messageA, messageB) => {
3
5
  const pathA = messageA.references?.[0]?.path ?? '';
4
6
  const pathB = messageB.references?.[0]?.path ?? '';
5
7
  if (pathA === pathB) {
6
- return messageA.id.localeCompare(messageB.id);
8
+ return localeCompare(messageA.id, messageB.id);
7
9
  } else {
8
- return pathA.localeCompare(pathB);
10
+ return localeCompare(pathA, pathB);
9
11
  }
10
12
  });
11
13
  }
@@ -10,5 +10,8 @@ function setNestedProperty(obj, keyPath, value) {
10
10
  }
11
11
  current[keys[keys.length - 1]] = value;
12
12
  }
13
+ function localeCompare(a, b) {
14
+ return a.localeCompare(b, 'en');
15
+ }
13
16
 
14
- export { setNestedProperty };
17
+ export { localeCompare, setNestedProperty };
@@ -1 +1 @@
1
- import e from"fs/promises";import s from"path";import t from"../extractor/MessageExtractor.js";import a from"../formatters/index.js";import o from"../source/SourceFileScanner.js";import r from"./CatalogLocales.js";import i from"./CatalogPersister.js";import c from"./SaveScheduler.js";class n{messagesByFile=(()=>new Map)();messagesById=(()=>new Map)();translationsByTargetLocale=(()=>new Map)();lastWriteByLocale=(()=>new Map)();constructor(e,s={}){this.config=e,this.saveScheduler=new c(50),this.projectRoot=s.projectRoot||process.cwd(),this.isDevelopment=s.isDevelopment??!1,this.messageExtractor=new t({isDevelopment:this.isDevelopment,projectRoot:this.projectRoot,sourceMap:s.sourceMap})}async getFormatter(){if(this.formatter)return this.formatter;{const e=(await a[this.config.messages.format]()).default;return this.formatter=new e,this.formatter}}async getPersister(){return this.persister||(this.persister=new i(this.config.messages.path,await this.getFormatter())),this.persister}async getCatalogLocales(){if(this.catalogLocales)return this.catalogLocales;{const e=s.join(this.projectRoot,this.config.messages.path),t=await this.getFormatter();return this.catalogLocales=new r({messagesDir:e,sourceLocale:this.config.sourceLocale,extension:t.EXTENSION,locales:this.config.messages.locales}),this.catalogLocales}}async getTargetLocales(){return(await this.getCatalogLocales()).getTargetLocales()}getSrcPaths(){return(Array.isArray(this.config.srcPath)?this.config.srcPath:[this.config.srcPath]).map((e=>s.join(this.projectRoot,e)))}getFileMessages(e){return this.messagesByFile.get(e)}async loadMessages(){if(await this.loadSourceMessages(),await this.loadTargetMessages(),this.isDevelopment){(await this.getCatalogLocales()).subscribeLocalesChange(this.onLocalesChange)}}async loadSourceMessages(){await this.loadLocaleMessages(this.config.sourceLocale);const s=await o.getSourceFiles(this.getSrcPaths());await Promise.all(s.map((async s=>this.extractFileMessages(s,await e.readFile(s,"utf8")))))}async loadLocaleMessages(e){const s=await this.getPersister();try{const t=await s.read(e),a=await s.getLastModified(e);return this.lastWriteByLocale.set(e,a),t}catch{return[]}}async loadTargetMessages(){const e=await this.getTargetLocales();await Promise.all(e.map((async e=>{this.translationsByTargetLocale.set(e,new Map);const s=await this.loadLocaleMessages(e);for(const t of s){this.translationsByTargetLocale.get(e).set(t.id,t.message)}})))}async extractFileMessages(e,t){const a=await this.messageExtractor.processFileContent(e,t),o=this.messagesByFile.get(e),r=Array.from(o?.keys()??[]),i=new Map;for(let e of a.messages){const s=this.messagesById.get(e.id);if(s){const t=[...s.references??[]];e.references.forEach((e=>{t.some((s=>s.path===e.path))||t.push(e)})),t.sort(((e,s)=>e.path.localeCompare(s.path))),e={...e,references:t},s.description&&!e.description&&(e={...e,description:s.description})}this.messagesById.set(e.id,e),i.set(e.id,e);const t=r.indexOf(e.id);-1!==t&&r.splice(t,1)}const c=s.relative(this.projectRoot,e);r.filter((e=>{const s=this.messagesById.get(e);return!s?.references?.some((e=>e.path!==c))})).forEach((e=>{this.messagesById.delete(e)}));a.messages.length>0?this.messagesByFile.set(e,i):this.messagesByFile.delete(e);const n=this.haveMessagesChanged(o,i);return{...a,changed:n}}haveMessagesChanged(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&&this.areReferencesEqual(e.references,s.references)}areReferencesEqual(e,s){if(!e&&!s)return!0;if(!e||!s)return!1;if(e.length!==s.length)return!1;for(let t=0;t<e.length;t++)if(e[t].path!==s[t].path)return!1;return!0}async save(){return this.saveScheduler.schedule((()=>this.saveImpl()))}async saveImpl(){const e=Array.from(this.messagesById.values()),s=await this.getPersister();await s.write(this.config.sourceLocale,e);for(const e of await this.getTargetLocales())await this.saveLocale(e);return e.length}async saveLocale(e){const s=Array.from(this.messagesById.values()),t=await this.getPersister(),a=this.lastWriteByLocale.get(e),o=await t.getLastModified(e);if(o&&a&&o>a){const s=await t.read(e),a=this.translationsByTargetLocale.get(e);for(const e of s)a.set(e.id,e.message)}const r=this.translationsByTargetLocale.get(e),i=s.map((e=>({...e,message:r.get(e.id)||""})));await t.write(e,i);const c=await t.getLastModified(e);this.lastWriteByLocale.set(e,c)}onLocalesChange=async e=>{for(const s of e.added){const e=new Map;this.translationsByTargetLocale.set(s,e);const t=await this.loadLocaleMessages(s);for(const s of t)e.set(s.id,s.message);await this.saveLocale(s)}for(const s of e.removed)this.translationsByTargetLocale.delete(s),this.lastWriteByLocale.delete(s)};destroy(){this.saveScheduler.destroy(),this.catalogLocales&&this.isDevelopment&&this.catalogLocales.unsubscribeLocalesChange(this.onLocalesChange)}}export{n as default};
1
+ import e from"fs/promises";import s from"path";import t from"../extractor/MessageExtractor.js";import a from"../formatters/index.js";import o from"../source/SourceFileScanner.js";import{localeCompare as i}from"../utils.js";import r from"./CatalogLocales.js";import c from"./CatalogPersister.js";import l from"./SaveScheduler.js";class n{messagesByFile=(()=>new Map)();messagesById=(()=>new Map)();translationsByTargetLocale=(()=>new Map)();lastWriteByLocale=(()=>new Map)();constructor(e,s={}){this.config=e,this.saveScheduler=new l(50),this.projectRoot=s.projectRoot||process.cwd(),this.isDevelopment=s.isDevelopment??!1,this.messageExtractor=new t({isDevelopment:this.isDevelopment,projectRoot:this.projectRoot,sourceMap:s.sourceMap})}async getFormatter(){if(this.formatter)return this.formatter;{const e=(await a[this.config.messages.format]()).default;return this.formatter=new e,this.formatter}}async getPersister(){return this.persister||(this.persister=new c(this.config.messages.path,await this.getFormatter())),this.persister}async getCatalogLocales(){if(this.catalogLocales)return this.catalogLocales;{const e=s.join(this.projectRoot,this.config.messages.path),t=await this.getFormatter();return this.catalogLocales=new r({messagesDir:e,sourceLocale:this.config.sourceLocale,extension:t.EXTENSION,locales:this.config.messages.locales}),this.catalogLocales}}async getTargetLocales(){return(await 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(){if(this.loadCatalogsPromise=Promise.all([this.loadSourceMessages(),this.loadTargetMessages()]),await this.loadCatalogsPromise,this.isDevelopment){(await this.getCatalogLocales()).subscribeLocalesChange(this.onLocalesChange)}const s=await o.getSourceFiles(this.getSrcPaths());await Promise.all(s.map((async s=>this.extractFileMessages(s,await e.readFile(s,"utf8")))))}async loadSourceMessages(){const e=await this.loadLocaleMessages(this.config.sourceLocale),t=new Map,a=new Map;for(const o of e)if(t.set(o.id,o),o.references)for(const e of o.references){const t=s.join(this.projectRoot,e.path);let i=a.get(t);i||(i=new Map,a.set(t,i)),i.set(o.id,o)}this.messagesById=t,this.messagesByFile=a}async loadLocaleMessages(e){const s=await this.getPersister();try{const t=await s.read(e),a=await s.getLastModified(e);return this.lastWriteByLocale.set(e,a),t}catch{return[]}}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);s&&this.messagesById.set(e.id,{...e,id:s.id,message:s.message,description:s.description,references:s.references})}else{const t=new Map;for(const e of s)t.set(e.id,e);this.translationsByTargetLocale.set(e,t)}}async extractFileMessages(e,t){const a=await this.messageExtractor.processFileContent(e,t),o=this.messagesByFile.get(e),r=Array.from(o?.keys()??[]),c=new Map;for(let e of a.messages){const s=this.messagesById.get(e.id);if(s){const t=[...s.references??[]];e.references.forEach((e=>{t.some((s=>s.path===e.path))||t.push(e)})),t.sort(((e,s)=>i(e.path,s.path))),e={...e,references:t};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)}const l=s.relative(this.projectRoot,e);r.filter((e=>{const s=this.messagesById.get(e);return!s?.references?.some((e=>e.path!==l))})).forEach((e=>{this.messagesById.delete(e)}));a.messages.length>0?this.messagesByFile.set(e,c):this.messagesByFile.delete(e);const n=this.haveMessagesChangedForFile(o,c);return{...a,changed:n}}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(e,c);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)};destroy(){this.saveScheduler.destroy(),this.catalogLocales&&this.isDevelopment&&this.catalogLocales.unsubscribeLocalesChange(this.onLocalesChange)}}export{n as default};
@@ -1 +1 @@
1
- import{createRequire as e}from"module";import t from"path";import{transform as s}from"@swc/core";import o from"./LRUCache.js";const r=e(import.meta.url);class i{compileCache=(()=>new o(750))();constructor(e){this.isDevelopment=e.isDevelopment,this.projectRoot=e.projectRoot,this.sourceMap=e.sourceMap??!1}async processFileContent(e,o){const i=o,a=this.compileCache.get(i);if(a)return a;if(!o.includes("useExtracted")&&!o.includes("getExtracted"))return{messages:[],code:o};const c=t.relative(this.projectRoot,e),p=await s(o,{jsc:{target:"esnext",parser:{syntax:"typescript",tsx:!0,decorators:!0},experimental:{disableBuiltinTransformsForInternalTesting:!0,disableAllLints:!0,plugins:[[r.resolve("next-intl-swc-plugin-extractor"),{isDevelopment:this.isDevelopment,filePath:c}]]}},sourceMaps:this.sourceMap,sourceFileName:c,filename:c}),n=p.output,l=JSON.parse(JSON.parse(n).results),m={code:p.code,map:p.map,messages:l};return this.compileCache.set(i,m),m}}export{i as default};
1
+ import{createRequire as e}from"module";import t from"path";import{transform as s}from"@swc/core";import o from"./LRUCache.js";const r=e(import.meta.url);class c{compileCache=(()=>new o(750))();constructor(e){this.isDevelopment=e.isDevelopment,this.projectRoot=e.projectRoot,this.sourceMap=e.sourceMap??!1}async processFileContent(e,o){const c=o,i=this.compileCache.get(c);if(i)return i;if(!o.includes("useExtracted")&&!o.includes("getExtracted"))return{messages:[],code:o};const a=t.relative(this.projectRoot,e),p=await s(o,{jsc:{target:"esnext",parser:{syntax:"typescript",tsx:!0,decorators:!0},experimental:{cacheRoot:"node_modules/.cache/swc",disableBuiltinTransformsForInternalTesting:!0,disableAllLints:!0,plugins:[[r.resolve("next-intl-swc-plugin-extractor"),{isDevelopment:this.isDevelopment,filePath:a}]]}},sourceMaps:this.sourceMap,sourceFileName:a,filename:a}),n=p.output,l=JSON.parse(JSON.parse(n).results),m={code:p.code,map:p.map,messages:l};return this.compileCache.set(c,m),m}}export{c as default};
@@ -1 +1 @@
1
- import{setNestedProperty as s}from"../utils/ObjectUtils.js";import t from"./Formatter.js";import{getSortedMessages as e}from"./utils.js";class r extends t{static NAMESPACE_SEPARATOR=".";EXTENSION=".json";parse(s){const t=JSON.parse(s),e=[];return this.traverseMessages(t,((s,t)=>{e.push({id:t,message:s})})),e}serialize(t){const r={};for(const o of e(t))s(r,o.id,o.message);return JSON.stringify(r,null,2)}toJSONString(s){return s}traverseMessages(s,t,e=""){for(const o of Object.keys(s)){const i=e?e+r.NAMESPACE_SEPARATOR+o:o,a=s[o];"string"==typeof a?t(a,i):"object"==typeof a&&this.traverseMessages(a,t,i)}}}export{r as default};
1
+ import{setNestedProperty as s}from"../utils.js";import e from"./Formatter.js";import{getSortedMessages as t}from"./utils.js";class r extends e{static NAMESPACE_SEPARATOR=".";EXTENSION=".json";parse(s){const e=JSON.parse(s),t=[];return this.traverseMessages(e,((s,e)=>{t.push({id:e,message:s})})),t}serialize(e){const r={};for(const o of t(e))s(r,o.id,o.message);return JSON.stringify(r,null,2)}toJSONString(s){return s}traverseMessages(s,e,t=""){for(const o of Object.keys(s)){const a=t?t+r.NAMESPACE_SEPARATOR+o:o,i=s[o];"string"==typeof i?e(i,a):"object"==typeof i&&this.traverseMessages(i,e,a)}}}export{r as default};
@@ -1 +1 @@
1
- import t from"po-parser";import{setNestedProperty as e}from"../utils/ObjectUtils.js";import a from"./Formatter.js";import{getSortedMessages as s}from"./utils.js";class r extends a{static DEFAULT_METADATA={"Content-Type":"text/plain; charset=utf-8","Content-Transfer-Encoding":"8bit","X-Generator":"next-intl","X-Crowdin-SourceKey":"msgstr"};EXTENSION=".po";metadataByLocale=(()=>new Map)();parse(e,a){const s=t.parse(e);return s.meta&&this.metadataByLocale.set(a.locale,s.meta),s.messages||[]}serialize(e,a){const o={Language:a.locale,...r.DEFAULT_METADATA,...this.metadataByLocale.get(a.locale)};return t.serialize({meta:o,messages:s(e)})}toJSONString(t,a){const s=this.parse(t,a),r={};for(const t of s)e(r,t.id,t.message);return JSON.stringify(r,null,2)}}export{r as default};
1
+ import t from"po-parser";import{setNestedProperty as e}from"../utils.js";import a from"./Formatter.js";import{getSortedMessages as s}from"./utils.js";class r extends a{static DEFAULT_METADATA={"Content-Type":"text/plain; charset=utf-8","Content-Transfer-Encoding":"8bit","X-Generator":"next-intl","X-Crowdin-SourceKey":"msgstr"};EXTENSION=".po";metadataByLocale=(()=>new Map)();parse(e,a){const s=t.parse(e);return s.meta&&this.metadataByLocale.set(a.locale,s.meta),s.messages||[]}serialize(e,a){const o={Language:a.locale,...r.DEFAULT_METADATA,...this.metadataByLocale.get(a.locale)};return t.serialize({meta:o,messages:s(e)})}toJSONString(t,a){const s=this.parse(t,a),r={};for(const t of s)e(r,t.id,t.message);return JSON.stringify(r,null,2)}}export{r as default};
@@ -1 +1 @@
1
- function e(e){return e.toSorted(((e,r)=>{const o=e.references?.[0]?.path??"",t=r.references?.[0]?.path??"";return o===t?e.id.localeCompare(r.id):o.localeCompare(t)}))}export{e as getSortedMessages};
1
+ import{localeCompare as e}from"../utils.js";function r(r){return r.toSorted(((r,t)=>{const n=r.references?.[0]?.path??"",o=t.references?.[0]?.path??"";return n===o?e(r.id,t.id):e(n,o)}))}export{r as getSortedMessages};
@@ -0,0 +1 @@
1
+ function t(t,n,e){const o=n.split(".");let l=t;for(let t=0;t<o.length-1;t++){const n=o[t];n in l&&"object"==typeof l[n]&&null!==l[n]||(l[n]={}),l=l[n]}l[o[o.length-1]]=e}function n(t,n){return t.localeCompare(n,"en")}export{n as localeCompare,t as setNestedProperty};
@@ -16,6 +16,7 @@ export default class CatalogManager {
16
16
  private formatter?;
17
17
  private catalogLocales?;
18
18
  private messageExtractor;
19
+ loadCatalogsPromise?: Promise<unknown>;
19
20
  constructor(config: ExtractorConfig, opts?: {
20
21
  projectRoot?: string;
21
22
  isDevelopment?: boolean;
@@ -26,21 +27,20 @@ export default class CatalogManager {
26
27
  private getCatalogLocales;
27
28
  private getTargetLocales;
28
29
  getSrcPaths(): Array<string>;
29
- getFileMessages(absoluteFilePath: string): Map<string, ExtractedMessage> | undefined;
30
30
  loadMessages(): Promise<void>;
31
31
  private loadSourceMessages;
32
32
  private loadLocaleMessages;
33
33
  private loadTargetMessages;
34
+ private reloadLocaleCatalog;
34
35
  extractFileMessages(absoluteFilePath: string, source: string): Promise<{
35
36
  messages: Array<ExtractedMessage>;
36
37
  code: string;
37
38
  changed: boolean;
38
39
  map?: string;
39
40
  }>;
40
- private haveMessagesChanged;
41
+ private haveMessagesChangedForFile;
41
42
  private areMessagesEqual;
42
- private areReferencesEqual;
43
- save(): Promise<number>;
43
+ save(): Promise<void>;
44
44
  private saveImpl;
45
45
  private saveLocale;
46
46
  private onLocalesChange;
@@ -7,6 +7,8 @@ export type ExtractedMessage = {
7
7
  references?: Array<{
8
8
  path: string;
9
9
  }>;
10
+ /** Allows for additional properties like .po flags to be read and later written. */
11
+ [key: string]: unknown;
10
12
  };
11
13
  export type MessagesConfig = {
12
14
  path: string;
@@ -1 +1,2 @@
1
1
  export declare function setNestedProperty(obj: Record<string, any>, keyPath: string, value: any): void;
2
+ export declare function localeCompare(a: string, b: string): number;
@@ -3,9 +3,9 @@ import type { RoutingConfigLocalizedNavigation, RoutingConfigSharedNavigation }
3
3
  import type { DomainsConfig, LocalePrefixMode, Locales, Pathnames } from '../../routing/types.js';
4
4
  export default function createNavigation<const AppLocales extends Locales, const AppLocalePrefixMode extends LocalePrefixMode = 'always', const AppPathnames extends Pathnames<AppLocales> = never, const AppDomains extends DomainsConfig<AppLocales> = never>(routing?: [AppPathnames] extends [never] ? RoutingConfigSharedNavigation<AppLocales, AppLocalePrefixMode, AppDomains> | undefined : RoutingConfigLocalizedNavigation<AppLocales, AppLocalePrefixMode, AppPathnames, AppDomains>): {
5
5
  Link: import("react").ForwardRefExoticComponent<Omit<{
6
+ id?: string | undefined | undefined;
6
7
  target?: import("react").HTMLAttributeAnchorTarget | undefined;
7
8
  replace?: boolean | undefined;
8
- id?: string | undefined | undefined;
9
9
  slot?: string | undefined | undefined;
10
10
  style?: import("react").CSSProperties | undefined;
11
11
  title?: string | undefined | undefined;
@@ -4,9 +4,9 @@ export default function createNavigation<const AppLocales extends Locales, const
4
4
  usePathname: () => never;
5
5
  useRouter: () => never;
6
6
  Link: import("react").ForwardRefExoticComponent<Omit<{
7
+ id?: string | undefined | undefined;
7
8
  target?: import("react").HTMLAttributeAnchorTarget | undefined;
8
9
  replace?: boolean | undefined;
9
- id?: string | undefined | undefined;
10
10
  slot?: string | undefined | undefined;
11
11
  style?: import("react").CSSProperties | undefined;
12
12
  title?: string | undefined | undefined;
@@ -22,9 +22,9 @@ export default function createSharedNavigationFns<const AppLocales extends Local
22
22
  alternateLinks: NonNullable<boolean | undefined>;
23
23
  };
24
24
  Link: import("react").ForwardRefExoticComponent<Omit<{
25
+ id?: string | undefined | undefined;
25
26
  target?: import("react").HTMLAttributeAnchorTarget | undefined;
26
27
  replace?: boolean | undefined;
27
- id?: string | undefined | undefined;
28
28
  slot?: string | undefined | undefined;
29
29
  style?: import("react").CSSProperties | undefined;
30
30
  title?: string | undefined | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-intl",
3
- "version": "4.5.4",
3
+ "version": "4.5.6",
4
4
  "sideEffects": false,
5
5
  "author": "Jan Amann <jan@amann.work>",
6
6
  "funding": [
@@ -127,9 +127,9 @@
127
127
  "@formatjs/intl-localematcher": "^0.5.4",
128
128
  "@swc/core": "^1.15.2",
129
129
  "negotiator": "^1.0.0",
130
- "next-intl-swc-plugin-extractor": "^4.5.4",
131
- "po-parser": "^0.1.2",
132
- "use-intl": "^4.5.4"
130
+ "next-intl-swc-plugin-extractor": "^4.5.6",
131
+ "po-parser": "^1.0.2",
132
+ "use-intl": "^4.5.6"
133
133
  },
134
134
  "peerDependencies": {
135
135
  "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
@@ -141,5 +141,5 @@
141
141
  "optional": true
142
142
  }
143
143
  },
144
- "gitHead": "32b170cc341cc847d4a6f4ae353c53d941f626f2"
144
+ "gitHead": "812d5e73688cebd1d1377a727dd2208796bcb670"
145
145
  }
@@ -1 +0,0 @@
1
- function t(t,n,e){const l=n.split(".");let o=t;for(let t=0;t<l.length-1;t++){const n=l[t];n in o&&"object"==typeof o[n]&&null!==o[n]||(o[n]={}),o=o[n]}o[l[l.length-1]]=e}export{t as setNestedProperty};