diff --git a/examples/example-app-router-extracted/messages/de.po b/examples/example-app-router-extracted/messages/de.po index 949cc1ecb..e919f9dcd 100644 --- a/examples/example-app-router-extracted/messages/de.po +++ b/examples/example-app-router-extracted/messages/de.po @@ -6,19 +6,19 @@ msgstr "" "X-Generator: next-intl\n" "X-Crowdin-SourceKey: msgstr\n" -#: src/app/Counter.tsx +#: src/app/Counter.tsx:16 msgid "jm1lmy" msgstr "Anzahl: {count, number}" -#: src/app/Counter.tsx +#: src/app/Counter.tsx:17 msgid "tQLRmz" msgstr "Erhöhen" #. Default meta title if not overridden by pages -#: src/app/layout.tsx +#: src/app/layout.tsx:17 msgid "lNLCAE" msgstr "next-intl Beispiel" -#: src/app/page.tsx +#: src/app/page.tsx:10 msgid "vlslj0" msgstr "Hallo {name}!" diff --git a/examples/example-app-router-extracted/messages/en.po b/examples/example-app-router-extracted/messages/en.po index 12cca4b5b..3248143c9 100644 --- a/examples/example-app-router-extracted/messages/en.po +++ b/examples/example-app-router-extracted/messages/en.po @@ -6,19 +6,19 @@ msgstr "" "X-Generator: next-intl\n" "X-Crowdin-SourceKey: msgstr\n" -#: src/app/Counter.tsx +#: src/app/Counter.tsx:16 msgid "jm1lmy" msgstr "Count: {count, number}" -#: src/app/Counter.tsx +#: src/app/Counter.tsx:17 msgid "tQLRmz" msgstr "Increment" #. Default meta title if not overridden by pages -#: src/app/layout.tsx +#: src/app/layout.tsx:17 msgid "lNLCAE" msgstr "next-intl example" -#: src/app/page.tsx +#: src/app/page.tsx:10 msgid "vlslj0" msgstr "Hey {name}!" diff --git a/packages/next-intl/package.json b/packages/next-intl/package.json index 0e91c98a3..52a1ee5f5 100644 --- a/packages/next-intl/package.json +++ b/packages/next-intl/package.json @@ -129,7 +129,7 @@ "@swc/core": "^1.15.2", "negotiator": "^1.0.0", "next-intl-swc-plugin-extractor": "workspace:^", - "po-parser": "^2.0.0", + "po-parser": "^2.1.1", "use-intl": "workspace:^" }, "peerDependencies": { diff --git a/packages/next-intl/src/extractor/ExtractionCompiler.test.tsx b/packages/next-intl/src/extractor/ExtractionCompiler.test.tsx index a446360f5..4373c2c77 100644 --- a/packages/next-intl/src/extractor/ExtractionCompiler.test.tsx +++ b/packages/next-intl/src/extractor/ExtractionCompiler.test.tsx @@ -561,8 +561,8 @@ describe('json format', () => { [ "messages/de.json", "{ - "7kKG3Q": "", - "OpKKos": "Hallo!" + "OpKKos": "Hallo!", + "7kKG3Q": "" } ", ], @@ -610,16 +610,16 @@ describe('json format', () => { [ "messages/en.json", "{ - "NnE1NP": "Goodbye!", - "OpKKos": "Hello!" + "OpKKos": "Hello!", + "NnE1NP": "Goodbye!" } ", ], [ "messages/de.json", "{ - "NnE1NP": "", - "OpKKos": "Hallo!" + "OpKKos": "Hallo!", + "NnE1NP": "" } ", ], @@ -701,13 +701,12 @@ describe('json format', () => { await waitForWriteFileCalls(1); - expect(JSON.parse(filesystem.project.messages!['en.json'])) - .toMatchInlineSnapshot(` - { - "7kKG3Q": "World!", - "OpKKos": "Hello!", - } - `); + expect(filesystem.project.messages!['en.json']).toMatchInlineSnapshot(` + "{ + "OpKKos": "Hello!", + "7kKG3Q": "World!" + } + "`); filesystem.project.messages!['de.json'] = '{}'; simulateFileEvent('/project/messages', 'rename', 'de.json'); @@ -717,8 +716,8 @@ describe('json format', () => { [ "messages/de.json", "{ - "7kKG3Q": "", - "OpKKos": "" + "OpKKos": "", + "7kKG3Q": "" } ", ] @@ -1042,9 +1041,57 @@ describe('po format', () => { ); } + it('tracks all line numbers when same message appears multiple times in one file', async () => { + filesystem.project.src['Greeting.tsx'] = + `import {useExtracted} from 'next-intl'; + function Greeting() { + const t = useExtracted(); + return ( +
+ {t('Hello!')} + {t('Hey!')} + {t('Hello!')} +
+ ); + } + `; + filesystem.project.src['Greeting2.tsx'] = + `import {useExtracted} from 'next-intl'; + function Greeting2() { + const t = useExtracted(); + return t('Hello!'); + } + `; + filesystem.project.messages = {}; + + using compiler = createCompiler(); + await compiler.extractAll(); + await waitForWriteFileCalls(1); + expect(vi.mocked(fs.writeFile).mock.calls[0][1]).toMatchInlineSnapshot(` + "msgid "" + msgstr "" + "Language: en\\n" + "Content-Type: text/plain; charset=utf-8\\n" + "Content-Transfer-Encoding: 8bit\\n" + "X-Generator: next-intl\\n" + "X-Crowdin-SourceKey: msgstr\\n" + + #: src/Greeting.tsx:6 + #: src/Greeting.tsx:8 + #: src/Greeting2.tsx:4 + msgid "OpKKos" + msgstr "Hello!" + + #: src/Greeting.tsx:7 + msgid "+YJVTi" + msgstr "Hey!" + " + `); + }); + it('saves messages initially', async () => { - filesystem.project.src['Greeting.tsx'] = ` - import {useExtracted} from 'next-intl'; + filesystem.project.src['Greeting.tsx'] = + `import {useExtracted} from 'next-intl'; function Greeting() { const t = useExtracted(); return
{t('Hey!')}
; @@ -1077,7 +1124,7 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Greeting.tsx + #: src/Greeting.tsx:4 msgid "+YJVTi" msgstr "Hey!" ", @@ -1092,7 +1139,7 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Greeting.tsx + #: src/Greeting.tsx:4 msgid "+YJVTi" msgstr "Hallo!" ", @@ -1153,7 +1200,7 @@ describe('po format', () => { "X-Crowdin-SourceKey: msgstr\\n" #. Shown on home screen - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "+YJVTi" msgstr "Hey!" ", @@ -1169,7 +1216,7 @@ describe('po format', () => { "X-Crowdin-SourceKey: msgstr\\n" #. Shown on home screen - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "+YJVTi" msgstr "Hallo!" ", @@ -1228,8 +1275,8 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Footer.tsx - #: src/Greeting.tsx + #: src/Footer.tsx:5 + #: src/Greeting.tsx:5 msgid "+YJVTi" msgstr "Hey!" ", @@ -1244,8 +1291,8 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Footer.tsx - #: src/Greeting.tsx + #: src/Footer.tsx:5 + #: src/Greeting.tsx:5 msgid "+YJVTi" msgstr "Hallo!" ", @@ -1254,6 +1301,51 @@ describe('po format', () => { `); }); + it('merges descriptions when message appears in multiple files with different descriptions', async () => { + filesystem.project.src['FileY.tsx'] = ` + import {useExtracted} from 'next-intl'; + function FileY() { + const t = useExtracted(); + return
{t({message: 'Message', description: 'Description from FileY'})}
; + } + `; + filesystem.project.src['FileZ.tsx'] = ` + import {useExtracted} from 'next-intl'; + function FileZ() { + const t = useExtracted(); + return ( +
+ {t('Message')} + {t({message: 'Message', description: 'Description from FileZ'})} +
+ ); + } + `; + filesystem.project.messages = {}; + + using compiler = createCompiler(); + await compiler.extractAll(); + await waitForWriteFileCalls(1); + + expect(vi.mocked(fs.writeFile).mock.calls[0][1]).toMatchInlineSnapshot(` + "msgid "" + msgstr "" + "Language: en\\n" + "Content-Type: text/plain; charset=utf-8\\n" + "Content-Transfer-Encoding: 8bit\\n" + "X-Generator: next-intl\\n" + "X-Crowdin-SourceKey: msgstr\\n" + + #. Description from FileZ + #: src/FileY.tsx:5 + #: src/FileZ.tsx:7 + #: src/FileZ.tsx:8 + msgid "T7Ry38" + msgstr "Message" + " + `); + }); + it('updates references in all catalogs when message is reused in another file', async () => { filesystem.project.src['Greeting.tsx'] = ` import {useExtracted} from 'next-intl'; @@ -1285,7 +1377,7 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "+YJVTi" msgstr "Hey!" ", @@ -1300,7 +1392,7 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "+YJVTi" msgstr "" ", @@ -1333,8 +1425,8 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Footer.tsx - #: src/Greeting.tsx + #: src/Footer.tsx:5 + #: src/Greeting.tsx:5 msgid "+YJVTi" msgstr "Hey!" ", @@ -1349,8 +1441,8 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Footer.tsx - #: src/Greeting.tsx + #: src/Footer.tsx:5 + #: src/Greeting.tsx:5 msgid "+YJVTi" msgstr "" ", @@ -1412,11 +1504,11 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Footer.tsx + #: src/Footer.tsx:5 msgid "+YJVTi" msgstr "Hey!" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "4xqPlJ" msgstr "Howdy!" ", @@ -1431,11 +1523,11 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Footer.tsx + #: src/Footer.tsx:5 msgid "+YJVTi" msgstr "" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "4xqPlJ" msgstr "" ", @@ -1512,7 +1604,7 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/component-b.tsx + #: src/component-b.tsx:5 msgid "4xqPlJ" msgstr "Howdy!" ", @@ -1527,7 +1619,7 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/component-b.tsx + #: src/component-b.tsx:5 msgid "4xqPlJ" msgstr "" ", @@ -1641,7 +1733,7 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/component-b.tsx + #: src/component-b.tsx:5 msgid "OpKKos" msgstr "Hello!" ", @@ -1656,7 +1748,7 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/component-b.tsx + #: src/component-b.tsx:5 msgid "OpKKos" msgstr "Hallo!" ", @@ -1705,7 +1797,7 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/component-b.tsx + #: src/component-b.tsx:5 msgid "OpKKos" msgstr "Hello!" ", @@ -1720,7 +1812,7 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/component-b.tsx + #: src/component-b.tsx:5 msgid "OpKKos" msgstr "" ", @@ -1769,7 +1861,7 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/component-b.tsx + #: src/component-b.tsx:5 msgid "OpKKos" msgstr "Hello!" ", @@ -1784,7 +1876,7 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/component-b.tsx + #: src/component-b.tsx:5 msgid "OpKKos" msgstr "" ", @@ -1817,7 +1909,7 @@ describe('po format', () => { "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgctxt "ui" msgid "OpKKos" msgstr "Hello!" @@ -1891,7 +1983,7 @@ msgstr "Hallo!" "MIME-Version: 1.0\\n" "X-Something-Else: test\\n" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "OpKKos" msgstr "Hello!" ", @@ -1907,7 +1999,7 @@ msgstr "Hallo!" "X-Crowdin-SourceKey: msgstr\\n" "POT-Creation-Date: 2025-10-27 16:00+0000\\n" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "OpKKos" msgstr "" ", @@ -1947,11 +2039,11 @@ msgstr "Hallo!" "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/app/page.tsx + #: src/app/page.tsx:5 msgid "NhX4DJ" msgstr "Hello" - #: src/components/Header.tsx + #: src/components/Header.tsx:5 msgid "PwaN2o" msgstr "Welcome" ", @@ -1983,16 +2075,16 @@ msgstr "Hallo!" "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/a.tsx + #: src/a.tsx:5 msgid "PmvAXH" msgstr "Message A" - #: src/b.tsx - #: src/d.tsx + #: src/b.tsx:5 + #: src/d.tsx:5 msgid "5bb321" msgstr "Message B" - #: src/c.tsx + #: src/c.tsx:5 msgid "c3UbA2" msgstr "Message C" ", @@ -2031,12 +2123,12 @@ msgstr "Hallo!" "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Greeting.tsx - msgid "7kKG3Q" + #: src/Greeting.tsx:5 + msgid "OpKKos" msgstr "" - #: src/Greeting.tsx - msgid "OpKKos" + #: src/Greeting.tsx:5 + msgid "7kKG3Q" msgstr "" ", ], @@ -2083,7 +2175,7 @@ msgstr "Hallo!" "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 #, fuzzy msgid "+YJVTi" msgstr "Hey!" @@ -2099,7 +2191,7 @@ msgstr "Hallo!" "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 #, c-format msgid "+YJVTi" msgstr "Hallo!" @@ -2179,12 +2271,12 @@ msgstr "Hey!" "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 #, c-format msgid "+YJVTi" msgstr "Hey!" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "jqdzk6" msgstr "World" ", @@ -2236,15 +2328,15 @@ msgstr "World" "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "+YJVTi" msgstr "Hey!" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "jqdzk6" msgstr "World" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "ODGmph" msgstr "!" ", @@ -2301,20 +2393,20 @@ msgstr "" "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 #, no-wrap msgid "+YJVTi" msgstr "Hallo!" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "jqdzk6" msgstr "" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "ODGmph" msgstr "" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "pE58D7" msgstr "" ", @@ -2374,25 +2466,25 @@ msgstr "" "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "+YJVTi" msgstr "Hallo!" - #: src/Greeting.tsx - msgid "I5NMJ8" - msgstr "" - - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "jqdzk6" msgstr "" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "ODGmph" msgstr "" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "pE58D7" msgstr "" + + #: src/Greeting.tsx:5 + msgid "I5NMJ8" + msgstr "" ", ] `); @@ -2455,12 +2547,12 @@ msgstr "Hey!" "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 #, fuzzy msgid "+YJVTi" msgstr "Hey!" - #: src/Greeting.tsx + #: src/Greeting.tsx:5 msgid "7kKG3Q" msgstr "World!" ", @@ -2540,7 +2632,7 @@ msgstr "Hey!" "X-Crowdin-SourceKey: msgstr\\n" #. This is a description - #: src/Greeting.tsx + #: src/Greeting.tsx:5 #, c-format msgid "OpKKos" msgstr "Hello!" @@ -2557,7 +2649,7 @@ msgstr "Hey!" "X-Crowdin-SourceKey: msgstr\\n" #. This is a description - #: src/Greeting.tsx + #: src/Greeting.tsx:5 #, fuzzy msgid "OpKKos" msgstr "Hallo!" @@ -2573,15 +2665,15 @@ msgstr "Hey!" "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Greeting.tsx - msgid "nm/7yQ" - msgstr "Hi!" - #. This is a description - #: src/Greeting.tsx + #: src/Greeting.tsx:5 #, c-format msgid "OpKKos" msgstr "Hello!" + + #: src/Greeting.tsx:10 + msgid "nm/7yQ" + msgstr "Hi!" ", ], [ @@ -2594,15 +2686,15 @@ msgstr "Hey!" "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/Greeting.tsx - msgid "nm/7yQ" - msgstr "" - #. This is a description - #: src/Greeting.tsx + #: src/Greeting.tsx:5 #, fuzzy msgid "OpKKos" msgstr "Hallo!" + + #: src/Greeting.tsx:10 + msgid "nm/7yQ" + msgstr "" ", ], ] @@ -2888,7 +2980,7 @@ msgstr "Hallo!"` "X-Generator: next-intl\\n" "X-Crowdin-SourceKey: msgstr\\n" - #: src/new/Button.tsx + #: src/new/Button.tsx:5 msgid "cfI2fq" msgstr "Click me updated" ", @@ -3141,20 +3233,20 @@ describe('custom format', () => { "Content-Transfer-Encoding: 8bit\\n" "X-Generator: next-intl\\n" - #: src/Greeting.tsx - msgctxt "misc.Fp6Fab" - msgid "Checking if you're logged in." - msgstr "Checking if you're logged in." + #: src/Greeting.tsx:5 + msgctxt "OpKKos" + msgid "Hello!" + msgstr "Hello!" - #: src/Greeting.tsx + #: src/Greeting.tsx:11 msgctxt "misc.l6ZjWT" msgid "The code you entered is incorrect. Please try again or contact support@example.com." msgstr "The code you entered is incorrect. Please try again or contact support@example.com." - #: src/Greeting.tsx - msgctxt "OpKKos" - msgid "Hello!" - msgstr "Hello!" + #: src/Greeting.tsx:12 + msgctxt "misc.Fp6Fab" + msgid "Checking if you're logged in." + msgstr "Checking if you're logged in." ", ], [ @@ -3166,20 +3258,20 @@ describe('custom format', () => { "Content-Transfer-Encoding: 8bit\\n" "X-Generator: next-intl\\n" - #: src/Greeting.tsx - msgctxt "misc.Fp6Fab" - msgid "Checking if you're logged in." - msgstr "" + #: src/Greeting.tsx:5 + msgctxt "OpKKos" + msgid "Hello!" + msgstr "Hallo!" - #: src/Greeting.tsx + #: src/Greeting.tsx:11 msgctxt "misc.l6ZjWT" msgid "The code you entered is incorrect. Please try again or contact support@example.com." msgstr "" - #: src/Greeting.tsx - msgctxt "OpKKos" - msgid "Hello!" - msgstr "Hallo!" + #: src/Greeting.tsx:12 + msgctxt "misc.Fp6Fab" + msgid "Checking if you're logged in." + msgstr "" ", ], ] diff --git a/packages/next-intl/src/extractor/catalog/CatalogManager.tsx b/packages/next-intl/src/extractor/catalog/CatalogManager.tsx index ed9303042..db9e33b65 100644 --- a/packages/next-intl/src/extractor/catalog/CatalogManager.tsx +++ b/packages/next-intl/src/extractor/catalog/CatalogManager.tsx @@ -7,8 +7,13 @@ import SourceFileScanner from '../source/SourceFileScanner.js'; import SourceFileWatcher, { type SourceFileWatcherEvent } from '../source/SourceFileWatcher.js'; -import type {ExtractorConfig, ExtractorMessage, Locale} from '../types.js'; -import {getDefaultProjectRoot, localeCompare} from '../utils.js'; +import type { + ExtractorConfig, + ExtractorMessage, + ExtractorMessageReference, + Locale +} from '../types.js'; +import {compareReferences, getDefaultProjectRoot} from '../utils.js'; import CatalogLocales from './CatalogLocales.js'; import CatalogPersister from './CatalogPersister.js'; import SaveScheduler from './SaveScheduler.js'; @@ -282,6 +287,7 @@ export default class CatalogManager implements Disposable { } const prevFileMessages = this.messagesByFile.get(absoluteFilePath); + const relativeFilePath = path.relative(this.projectRoot, absoluteFilePath); // Init with all previous ones const idsToRemove = Array.from(prevFileMessages?.keys() ?? []); @@ -294,13 +300,15 @@ export default class CatalogManager implements Disposable { // Merge with previous message if it exists if (prevMessage) { - const validated = prevMessage.references ?? []; - message = { - ...message, - references: this.mergeReferences(validated, { - path: path.relative(this.projectRoot, absoluteFilePath) - }) - }; + message = {...message}; + + if (message.references) { + message.references = this.mergeReferences( + prevMessage.references ?? [], + relativeFilePath, + message.references + ); + } // Merge other properties like description, or unknown // attributes like flags that are opaque to us @@ -319,8 +327,6 @@ export default class CatalogManager implements Disposable { if (index !== -1) idsToRemove.splice(index, 1); } - const relativeFilePath = path.relative(this.projectRoot, absoluteFilePath); - // Clean up removed messages from `messagesById` idsToRemove.forEach((id) => { const message = this.messagesById.get(id); @@ -357,17 +363,16 @@ export default class CatalogManager implements Disposable { } private mergeReferences( - existing: Array<{path: string}>, - current: {path: string} - ): Array<{path: string}> { - const dedup = new Map(); - for (const ref of existing) { - dedup.set(ref.path, ref); - } - dedup.set(current.path, current); - return Array.from(dedup.values()).sort((a, b) => - localeCompare(a.path, b.path) + existing: Array, + currentFilePath: string, + currentFileRefs: Array + ): Array { + // Keep refs from other files, replace all refs from the current file + const otherFileRefs = existing.filter( + (ref) => ref.path !== currentFilePath ); + const merged = [...otherFileRefs, ...currentFileRefs]; + return merged.sort(compareReferences); } private haveMessagesChangedForFile( diff --git a/packages/next-intl/src/extractor/extractor/MessageExtractor.test.tsx b/packages/next-intl/src/extractor/extractor/MessageExtractor.test.tsx index 77f5904b0..95c9a01a8 100644 --- a/packages/next-intl/src/extractor/extractor/MessageExtractor.test.tsx +++ b/packages/next-intl/src/extractor/extractor/MessageExtractor.test.tsx @@ -31,6 +31,43 @@ it('can extract with source maps', async () => { expect(result.map).not.toContain(''); }); +it('extracts same message used multiple times in one file with all references', async () => { + const result = await process( + `import {useExtracted} from 'next-intl'; + + function Component() { + const t = useExtracted(); + return ( +
+ {t('Hello!')} + {t('Hello!')} +
+ ); + } + ` + ); + + expect(result.messages).toMatchInlineSnapshot(` + [ + { + "description": null, + "id": "OpKKos", + "message": "Hello!", + "references": [ + { + "line": 7, + "path": "test.tsx", + }, + { + "line": 8, + "path": "test.tsx", + }, + ], + }, + ] + `); +}); + it('does not add a fallback message in production', async () => { const result = await process( `import {useExtracted} from 'next-intl'; @@ -59,6 +96,7 @@ it('does not add a fallback message in production', async () => { "message": "Hey!", "references": [ { + "line": 5, "path": "test.tsx", }, ], diff --git a/packages/next-intl/src/extractor/types.tsx b/packages/next-intl/src/extractor/types.tsx index 97c15f7fe..1cf213f0a 100644 --- a/packages/next-intl/src/extractor/types.tsx +++ b/packages/next-intl/src/extractor/types.tsx @@ -5,11 +5,16 @@ import type {MessagesFormat} from './format/types.js'; // don't require a match here. export type Locale = string; +export type ExtractorMessageReference = { + path: string; + line?: number; +}; + export type ExtractorMessage = { id: string; message: string; description?: string; - references?: Array<{path: string}>; + references?: Array; /** Allows for additional properties like .po flags to be read and later written. */ [key: string]: unknown; }; diff --git a/packages/next-intl/src/extractor/utils.test.tsx b/packages/next-intl/src/extractor/utils.test.tsx index a7dded9db..62e8be862 100644 --- a/packages/next-intl/src/extractor/utils.test.tsx +++ b/packages/next-intl/src/extractor/utils.test.tsx @@ -8,36 +8,58 @@ describe('getSortedMessages', () => { { id: 'a', message: 'a', - references: [{path: 'components/B.tsx'}] + references: [{path: 'components/B.tsx', line: 1}] }, { id: 'b', message: 'b', - references: [{path: 'components/A.tsx'}] + references: [{path: 'components/A.tsx', line: 1}] } ]).map((message) => message.id) ).toEqual(['b', 'a']); }); - it('uses message ids to break ties when reference paths match', () => { + it('sorts by line number when reference paths match', () => { expect( getSortedMessages([ { - id: 'c', + id: 'b', message: 'b', - references: [{path: 'components/B.tsx'}] + references: [{path: 'components/A.tsx', line: 20}] }, { - id: 'b', + id: 'a', message: 'a', - references: [{path: 'components/A.tsx'}] + references: [{path: 'components/A.tsx', line: 10}] }, { - id: 'a', + id: 'c', message: 'c', - references: [{path: 'components/A.tsx'}] + references: [{path: 'components/A.tsx', line: 30}] } ]).map((message) => message.id) ).toEqual(['a', 'b', 'c']); }); + + it('preserves original order when reference paths and lines match', () => { + expect( + getSortedMessages([ + { + id: 'c', + message: 'c', + references: [{path: 'components/A.tsx', line: 10}] + }, + { + id: 'a', + message: 'a', + references: [{path: 'components/A.tsx', line: 10}] + }, + { + id: 'b', + message: 'b', + references: [{path: 'components/A.tsx', line: 10}] + } + ]).map((message) => message.id) + ).toEqual(['c', 'a', 'b']); + }); }); diff --git a/packages/next-intl/src/extractor/utils.tsx b/packages/next-intl/src/extractor/utils.tsx index 97bcfb1b5..39611f317 100644 --- a/packages/next-intl/src/extractor/utils.tsx +++ b/packages/next-intl/src/extractor/utils.tsx @@ -1,4 +1,4 @@ -import type {ExtractorMessage} from './types.js'; +import type {ExtractorMessage, ExtractorMessageReference} from './types.js'; // Essentialls lodash/set, but we avoid this dependency export function setNestedProperty( @@ -28,14 +28,14 @@ export function getSortedMessages( messages: Array ): Array { return messages.toSorted((messageA, messageB) => { - const pathA = messageA.references?.[0]?.path ?? ''; - const pathB = messageB.references?.[0]?.path ?? ''; + const refA = messageA.references?.[0]; + const refB = messageB.references?.[0]; - if (pathA === pathB) { - return localeCompare(messageA.id, messageB.id); - } else { - return localeCompare(pathA, pathB); - } + // No references: preserve original (extraction) order + if (!refA || !refB) return 0; + + // Sort by path, then line. Same path+line: preserve original order + return compareReferences(refA, refB); }); } @@ -43,6 +43,15 @@ export function localeCompare(a: string, b: string) { return a.localeCompare(b, 'en'); } +export function compareReferences( + refA: ExtractorMessageReference, + refB: ExtractorMessageReference +): number { + const pathCompare = localeCompare(refA.path, refB.path); + if (pathCompare !== 0) return pathCompare; + return (refA.line ?? 0) - (refB.line ?? 0); +} + export function getDefaultProjectRoot() { return process.cwd(); } diff --git a/packages/swc-plugin-extractor/Cargo.toml b/packages/swc-plugin-extractor/Cargo.toml index fdbe723ce..d428a54fc 100644 --- a/packages/swc-plugin-extractor/Cargo.toml +++ b/packages/swc-plugin-extractor/Cargo.toml @@ -15,6 +15,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] base64 = "0.22" +indexmap = "2.7" rustc-hash = "2.1.0" serde = "1.0.203" serde_json = "1.0.117" diff --git a/packages/swc-plugin-extractor/src/lib.rs b/packages/swc-plugin-extractor/src/lib.rs index 271ab506a..8cdab47a1 100644 --- a/packages/swc-plugin-extractor/src/lib.rs +++ b/packages/swc-plugin-extractor/src/lib.rs @@ -3,12 +3,14 @@ mod key_generator; +use indexmap::IndexMap; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use swc_atoms::Wtf8Atom; use swc_common::{errors::HANDLER, Spanned, DUMMY_SP}; use swc_core::{ - plugin::proxies::TransformPluginProgramMetadata, transform_common::output::experimental_emit, + common::SourceMapper, plugin::proxies::TransformPluginProgramMetadata, + transform_common::output::experimental_emit, }; use swc_ecma_ast::*; use swc_ecma_utils::ExprFactory; @@ -24,12 +26,16 @@ fn next_intl_plugin(mut program: Program, data: TransformPluginProgramMetadata) ) .expect("Invalid config"); - let mut visitor = TransformVisitor::new(config.is_development, config.file_path); + let mut visitor = TransformVisitor::new( + config.is_development, + config.file_path, + Some(Box::new(data.source_map) as Box), + ); program.visit_mut_with(&mut visitor); experimental_emit( "results".into(), - serde_json::to_string(&visitor.results).unwrap(), + serde_json::to_string(&visitor.get_results()).unwrap(), ); program @@ -47,25 +53,36 @@ struct Config { pub struct TransformVisitor { is_development: bool, file_path: String, + source_map: Option>, hook_local_names: FxHashMap, translator_map: FxHashMap, - results: Vec, + /// Messages keyed by ID to aggregate duplicate usages (IndexMap preserves insertion order) + results_by_id: IndexMap, } impl TransformVisitor { - pub fn new(is_development: bool, file_path: String) -> Self { + pub fn new( + is_development: bool, + file_path: String, + source_map: Option>, + ) -> Self { Self { is_development, file_path, + source_map, hook_local_names: Default::default(), translator_map: Default::default(), - results: Default::default(), + results_by_id: Default::default(), } } + pub fn get_results(&self) -> Vec { + self.results_by_id.values().cloned().collect() + } + fn define_translator(&mut self, name: Id, namespace: Option) { self.translator_map .insert(name, TranslatorInfo { namespace }); @@ -78,16 +95,17 @@ struct TranslatorInfo { } #[derive(Debug, Clone, Serialize)] -struct StrictExtractedMessage { - id: Wtf8Atom, - message: Wtf8Atom, - description: Option, - references: Vec, +pub struct StrictExtractedMessage { + pub id: Wtf8Atom, + pub message: Wtf8Atom, + pub description: Option, + pub references: Vec, } #[derive(Debug, Clone, Serialize)] -struct Reference { - path: String, +pub struct Reference { + pub path: String, + pub line: usize, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -222,18 +240,30 @@ impl VisitMut for TransformVisitor { .join(NAMESPACE_SEPARATOR) .into() }); - let mut message = StrictExtractedMessage { - id: full_key, - message: message_text.clone(), - description: None, - references: vec![Reference { - path: self.file_path.clone(), - }], + let line = self + .source_map + .as_ref() + .map_or(0, |sm| sm.lookup_char_pos(call.span.lo).line); + let new_reference = Reference { + path: self.file_path.clone(), + line, }; - if let Some(description) = description { - message.description = Some(description.clone()); + + // Aggregate duplicate messages by ID + if let Some(existing) = self.results_by_id.get_mut(&full_key) { + existing.references.push(new_reference); + if existing.description.is_none() { + existing.description = description; + } + } else { + let message = StrictExtractedMessage { + id: full_key.clone(), + message: message_text.clone(), + description, + references: vec![new_reference], + }; + self.results_by_id.insert(full_key, message); } - self.results.push(message); // Transform the argument based on type match &mut *call.args[0].expr { diff --git a/packages/swc-plugin-extractor/target/wasm32-wasip1/release/swc_plugin_extractor.wasm b/packages/swc-plugin-extractor/target/wasm32-wasip1/release/swc_plugin_extractor.wasm index e5a6ddc13..37862fe86 100755 Binary files a/packages/swc-plugin-extractor/target/wasm32-wasip1/release/swc_plugin_extractor.wasm and b/packages/swc-plugin-extractor/target/wasm32-wasip1/release/swc_plugin_extractor.wasm differ diff --git a/packages/swc-plugin-extractor/tests/fixture.rs b/packages/swc-plugin-extractor/tests/fixture.rs index f79e6d331..0c5e2dbaa 100644 --- a/packages/swc-plugin-extractor/tests/fixture.rs +++ b/packages/swc-plugin-extractor/tests/fixture.rs @@ -1,38 +1,74 @@ +use std::cell::RefCell; +use std::fs; use std::path::PathBuf; +use std::rc::Rc; -use swc_common::Mark; +use serde_json::Value; +use swc_common::{FileName, Globals, Mark, SourceMap, GLOBALS}; use swc_core::ecma::{ - parser::{EsSyntax, Syntax}, + parser::{lexer::Lexer, EsSyntax, Parser, StringInput, Syntax}, transforms::{ base::resolver, testing::{test_fixture, FixtureTestConfig}, }, }; -use swc_ecma_ast::Pass; -use swc_ecma_visit::visit_mut_pass; +use swc_ecma_ast::{EsVersion, Pass}; +use swc_ecma_visit::VisitMutWith; use swc_plugin_extractor::TransformVisitor; -fn tr() -> impl Pass { +struct VisitorPass { + visitor: Rc>, +} + +impl Pass for VisitorPass { + fn process(&mut self, program: &mut swc_ecma_ast::Program) { + program.visit_mut_with(&mut *self.visitor.borrow_mut()); + } +} + +fn tr(visitor_rc: Rc>) -> impl Pass { let unresolved_mark = Mark::new(); let top_level_mark = Mark::new(); ( resolver(unresolved_mark, top_level_mark, false), - visit_mut_pass(TransformVisitor::new(true, "input.js".to_string())), + VisitorPass { + visitor: visitor_rc, + }, ) } +fn parse(cm: &SourceMap, code: &str) -> swc_ecma_ast::Program { + let fm = cm.new_source_file(FileName::Anon.into(), code.to_string()); + let lexer = Lexer::new( + Syntax::Es(EsSyntax { + jsx: true, + ..Default::default() + }), + EsVersion::EsNext, + StringInput::from(&*fm), + None, + ); + let mut parser = Parser::new_from(lexer); + parser.parse_program().unwrap() +} + #[testing::fixture("tests/fixture/**/input.js")] fn test(input: PathBuf) { let dir = input.parent().unwrap().to_path_buf(); let output = dir.join("output.js"); + let output_json = dir.join("output.json"); + + let visitor = TransformVisitor::new(true, "input.js".to_string(), None); + let visitor_rc = Rc::new(RefCell::new(visitor)); + // Test JS transformation test_fixture( Syntax::Es(EsSyntax { jsx: true, ..Default::default() }), - &|_| tr(), + &|_| tr(visitor_rc.clone()), &input, &output, FixtureTestConfig { @@ -41,4 +77,48 @@ fn test(input: PathBuf) { ..Default::default() }, ); + + // Test JSON output - run transformation again with SourceMap for accurate line numbers + let globals = Globals::new(); + GLOBALS.set(&globals, || { + let code = fs::read_to_string(&input).unwrap(); + let cm = SourceMap::default(); + let mut program = parse(&cm, &code); + + if !program.is_module() { + panic!("Parsed as script, expected module"); + } + + let unresolved_mark = Mark::new(); + let top_level_mark = Mark::new(); + + program.visit_mut_with(&mut resolver(unresolved_mark, top_level_mark, false)); + + let file_name = input.file_name().unwrap().to_string_lossy().to_string(); + // Use the same SourceMap that was used for parsing so spans match + let mut visitor = TransformVisitor::new( + true, + file_name, + Some(Box::new(cm) as Box), + ); + + program.visit_mut_with(&mut visitor); + + // Use results directly from visitor - it calculates line numbers correctly with SourceMap + let actual_results = visitor.get_results(); + let actual_json: Value = serde_json::to_value(&actual_results).unwrap(); + + let expected_json_str = fs::read_to_string(&output_json) + .unwrap_or_else(|_| panic!("Expected output.json not found at {:?}", output_json)); + let expected_json: Value = serde_json::from_str(&expected_json_str) + .unwrap_or_else(|_| panic!("Failed to parse expected JSON at {:?}", output_json)); + + if actual_json != expected_json { + panic!( + "JSON output mismatch.\nExpected:\n{}\nActual:\n{}", + serde_json::to_string_pretty(&expected_json).unwrap(), + serde_json::to_string_pretty(&actual_json).unwrap() + ); + } + }); } diff --git a/packages/swc-plugin-extractor/tests/fixture/alias-hook/output.json b/packages/swc-plugin-extractor/tests/fixture/alias-hook/output.json new file mode 100644 index 000000000..fdc6772a7 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/alias-hook/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "+YJVTi", + "message": "Hey!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/async-basic/output.json b/packages/swc-plugin-extractor/tests/fixture/async-basic/output.json new file mode 100644 index 000000000..782c01954 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/async-basic/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "0KGiQf", + "message": "Hello there!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/async-explicit-id/output.json b/packages/swc-plugin-extractor/tests/fixture/async-explicit-id/output.json new file mode 100644 index 000000000..67325d5ea --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/async-explicit-id/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "greeting", + "message": "Hello {name}!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/async-locale/output.json b/packages/swc-plugin-extractor/tests/fixture/async-locale/output.json new file mode 100644 index 000000000..782c01954 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/async-locale/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "0KGiQf", + "message": "Hello there!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/async-namespace/output.json b/packages/swc-plugin-extractor/tests/fixture/async-namespace/output.json new file mode 100644 index 000000000..63b06293a --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/async-namespace/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "ui.OpKKos", + "message": "Hello!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/async-rename/output.json b/packages/swc-plugin-extractor/tests/fixture/async-rename/output.json new file mode 100644 index 000000000..782c01954 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/async-rename/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "0KGiQf", + "message": "Hello there!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/basic/output.json b/packages/swc-plugin-extractor/tests/fixture/basic/output.json new file mode 100644 index 000000000..fdc6772a7 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/basic/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "+YJVTi", + "message": "Hey!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/date-format/output.json b/packages/swc-plugin-extractor/tests/fixture/date-format/output.json new file mode 100644 index 000000000..9996f2378 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/date-format/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "5n+ZPU", + "message": "{date, date, short}!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/duplicates/input.js b/packages/swc-plugin-extractor/tests/fixture/duplicates/input.js new file mode 100644 index 000000000..ab9afe8ab --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/duplicates/input.js @@ -0,0 +1,11 @@ +import { useExtracted } from "next-intl"; + +function Component() { + const t = useExtracted(); + t("Hello!"); + + // Some other code + console.log("test"); + + t("Hello!"); +} diff --git a/packages/swc-plugin-extractor/tests/fixture/duplicates/output.js b/packages/swc-plugin-extractor/tests/fixture/duplicates/output.js new file mode 100644 index 000000000..f43723b0d --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/duplicates/output.js @@ -0,0 +1,8 @@ +import { useTranslations as useTranslations$1 } from "next-intl"; +function Component() { + const t = useTranslations$1(); + t("OpKKos", void 0, void 0, "Hello!"); + // Some other code + console.log("test"); + t("OpKKos", void 0, void 0, "Hello!"); +} diff --git a/packages/swc-plugin-extractor/tests/fixture/duplicates/output.json b/packages/swc-plugin-extractor/tests/fixture/duplicates/output.json new file mode 100644 index 000000000..f4db4ee58 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/duplicates/output.json @@ -0,0 +1,17 @@ +[ + { + "id": "OpKKos", + "message": "Hello!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + }, + { + "path": "input.js", + "line": 10 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/duplicates/output.map b/packages/swc-plugin-extractor/tests/fixture/duplicates/output.map new file mode 100644 index 000000000..a1754186c --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/duplicates/output.map @@ -0,0 +1 @@ +{"version":3,"sources":["input.js"],"sourcesContent":["import { useExtracted } from \"next-intl\";\n\nfunction Component() {\n const t = useExtracted();\n t(\"Hello!\");\n\n // Some other code\n console.log(\"test\");\n\n t(\"Hello!\");\n}\n"],"names":[],"mappings":"AAAA,SAAS,oCAAY,QAAQ,YAAY;AAEzC,SAAS;IACP,MAAM,IAAI;IACV,EAAE;IAEF,kBAAkB;IAClB,QAAQ,GAAG,CAAC;IAEZ,EAAE;AACJ"} diff --git a/packages/swc-plugin-extractor/tests/fixture/existing-aliased-hook/output.json b/packages/swc-plugin-extractor/tests/fixture/existing-aliased-hook/output.json new file mode 100644 index 000000000..328c98027 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/existing-aliased-hook/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "piskIR", + "message": "Hello from extracted!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 6 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/existing-hook/output.json b/packages/swc-plugin-extractor/tests/fixture/existing-hook/output.json new file mode 100644 index 000000000..328c98027 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/existing-hook/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "piskIR", + "message": "Hello from extracted!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 6 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/let/output.json b/packages/swc-plugin-extractor/tests/fixture/let/output.json new file mode 100644 index 000000000..fdc6772a7 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/let/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "+YJVTi", + "message": "Hey!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/multiple-hooks/output.json b/packages/swc-plugin-extractor/tests/fixture/multiple-hooks/output.json new file mode 100644 index 000000000..73e1ac353 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/multiple-hooks/output.json @@ -0,0 +1,90 @@ +[ + { + "id": "tnuBMt", + "message": "Page title", + "description": null, + "references": [ + { + "path": "input.js", + "line": 7 + } + ] + }, + { + "id": "OpKKos", + "message": "Hello!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 13 + } + ] + }, + { + "id": "mOPTEA", + "message": "Server data message", + "description": null, + "references": [ + { + "path": "input.js", + "line": 18 + } + ] + }, + { + "id": "MgvtBu", + "message": "Component message", + "description": null, + "references": [ + { + "path": "input.js", + "line": 23 + } + ] + }, + { + "id": "sJK5Uk", + "message": "Another one 1", + "description": null, + "references": [ + { + "path": "input.js", + "line": 28 + } + ] + }, + { + "id": "2k7cS1", + "message": "Another one 2", + "description": null, + "references": [ + { + "path": "input.js", + "line": 33 + } + ] + }, + { + "id": "another.6jb0KP", + "message": "Two 1", + "description": null, + "references": [ + { + "path": "input.js", + "line": 38 + } + ] + }, + { + "id": "another.KVQtmd", + "message": "Two 2", + "description": null, + "references": [ + { + "path": "input.js", + "line": 43 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/namespace/output.json b/packages/swc-plugin-extractor/tests/fixture/namespace/output.json new file mode 100644 index 000000000..63b06293a --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/namespace/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "ui.OpKKos", + "message": "Hello!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/obj-id-double-quotes/output.json b/packages/swc-plugin-extractor/tests/fixture/obj-id-double-quotes/output.json new file mode 100644 index 000000000..135654a5d --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/obj-id-double-quotes/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "greeting", + "message": "Hello!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/obj-id-formats/output.json b/packages/swc-plugin-extractor/tests/fixture/obj-id-formats/output.json new file mode 100644 index 000000000..135654a5d --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/obj-id-formats/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "greeting", + "message": "Hello!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/obj-id-namespace/output.json b/packages/swc-plugin-extractor/tests/fixture/obj-id-namespace/output.json new file mode 100644 index 000000000..4bbaf5c03 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/obj-id-namespace/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "ui.greeting", + "message": "Hello!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/obj-id-rich/output.json b/packages/swc-plugin-extractor/tests/fixture/obj-id-rich/output.json new file mode 100644 index 000000000..227b1ecf3 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/obj-id-rich/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "greeting", + "message": "Hello Alice!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/obj-id-single-quotes/output.json b/packages/swc-plugin-extractor/tests/fixture/obj-id-single-quotes/output.json new file mode 100644 index 000000000..135654a5d --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/obj-id-single-quotes/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "greeting", + "message": "Hello!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/obj-id-template-quotes/output.json b/packages/swc-plugin-extractor/tests/fixture/obj-id-template-quotes/output.json new file mode 100644 index 000000000..135654a5d --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/obj-id-template-quotes/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "greeting", + "message": "Hello!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/obj-id-values/output.json b/packages/swc-plugin-extractor/tests/fixture/obj-id-values/output.json new file mode 100644 index 000000000..135654a5d --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/obj-id-values/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "greeting", + "message": "Hello!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/quote-variations/output.json b/packages/swc-plugin-extractor/tests/fixture/quote-variations/output.json new file mode 100644 index 000000000..1e0cd0a87 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/quote-variations/output.json @@ -0,0 +1,35 @@ +[ + { + "id": "OpKKos", + "message": "Hello!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + }, + { + "id": "+YJVTi", + "message": "Hey!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 6 + } + ] + }, + { + "id": "nm/7yQ", + "message": "Hi!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 7 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/rename/output.json b/packages/swc-plugin-extractor/tests/fixture/rename/output.json new file mode 100644 index 000000000..cb42b23dc --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/rename/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "OpKKos", + "message": "Hello!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/shadow/output.json b/packages/swc-plugin-extractor/tests/fixture/shadow/output.json new file mode 100644 index 000000000..fdc6772a7 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/shadow/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "+YJVTi", + "message": "Hey!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/t-has/input.js b/packages/swc-plugin-extractor/tests/fixture/t-has/input.js index edcacf4f9..a2b99cf35 100644 --- a/packages/swc-plugin-extractor/tests/fixture/t-has/input.js +++ b/packages/swc-plugin-extractor/tests/fixture/t-has/input.js @@ -2,5 +2,9 @@ import {useExtracted} from 'next-intl'; function Component() { const t = useExtracted(); - t.has('Hello there!'); + if (t.has('Hello here!')) { + return t('Hello here!'); + } else { + return t('Hello there!'); + } } diff --git a/packages/swc-plugin-extractor/tests/fixture/t-has/output.js b/packages/swc-plugin-extractor/tests/fixture/t-has/output.js index 22dcd14c9..921157914 100644 --- a/packages/swc-plugin-extractor/tests/fixture/t-has/output.js +++ b/packages/swc-plugin-extractor/tests/fixture/t-has/output.js @@ -1,5 +1,9 @@ import { useTranslations as useTranslations$1 } from 'next-intl'; function Component() { const t = useTranslations$1(); - t.has("0KGiQf"); + if (t.has("j0tI96")) { + return t("j0tI96", void 0, void 0, "Hello here!"); + } else { + return t("0KGiQf", void 0, void 0, "Hello there!"); + } } diff --git a/packages/swc-plugin-extractor/tests/fixture/t-has/output.json b/packages/swc-plugin-extractor/tests/fixture/t-has/output.json new file mode 100644 index 000000000..b135522d6 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/t-has/output.json @@ -0,0 +1,28 @@ +[ + { + "description": null, + "id": "j0tI96", + "message": "Hello here!", + "references": [ + { + "line": 5, + "path": "input.js" + }, + { + "line": 6, + "path": "input.js" + } + ] + }, + { + "description": null, + "id": "0KGiQf", + "message": "Hello there!", + "references": [ + { + "line": 8, + "path": "input.js" + } + ] + } +] \ No newline at end of file diff --git a/packages/swc-plugin-extractor/tests/fixture/t-has/output.map b/packages/swc-plugin-extractor/tests/fixture/t-has/output.map index 458f77738..ebae9e09c 100644 --- a/packages/swc-plugin-extractor/tests/fixture/t-has/output.map +++ b/packages/swc-plugin-extractor/tests/fixture/t-has/output.map @@ -1 +1 @@ -{"version":3,"sources":["input.js"],"sourcesContent":["import {useExtracted} from 'next-intl';\n\nfunction Component() {\n const t = useExtracted();\n t.has('Hello there!');\n}\n"],"names":[],"mappings":"AAAA,SAAQ,oCAAY,QAAO,YAAY;AAEvC,SAAS;IACP,MAAM,IAAI;IACV,EAAE,GAAG,CAAC;AACR"} +{"version":3,"sources":["input.js"],"sourcesContent":["import {useExtracted} from 'next-intl';\n\nfunction Component() {\n const t = useExtracted();\n if (t.has('Hello here!')) {\n return t('Hello here!');\n } else {\n return t('Hello there!');\n }\n}\n"],"names":[],"mappings":"AAAA,SAAQ,oCAAY,QAAO,YAAY;AAEvC,SAAS;IACP,MAAM,IAAI;IACV,IAAI,EAAE,GAAG,CAAC,WAAgB;QACxB,OAAO,EAAE;IACX,OAAO;QACL,OAAO,EAAE;IACX;AACF"} diff --git a/packages/swc-plugin-extractor/tests/fixture/t-markup/output.json b/packages/swc-plugin-extractor/tests/fixture/t-markup/output.json new file mode 100644 index 000000000..a2dc843be --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/t-markup/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "C+nN8a", + "message": "Hello Alice!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/t-rich/output.json b/packages/swc-plugin-extractor/tests/fixture/t-rich/output.json new file mode 100644 index 000000000..a2dc843be --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/t-rich/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "C+nN8a", + "message": "Hello Alice!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/packages/swc-plugin-extractor/tests/fixture/values/output.json b/packages/swc-plugin-extractor/tests/fixture/values/output.json new file mode 100644 index 000000000..1b7f69846 --- /dev/null +++ b/packages/swc-plugin-extractor/tests/fixture/values/output.json @@ -0,0 +1,13 @@ +[ + { + "id": "tBFOH1", + "message": "Hello, {name}!", + "description": null, + "references": [ + { + "path": "input.js", + "line": 5 + } + ] + } +] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d16081696..ff24c781a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -830,8 +830,8 @@ importers: specifier: workspace:^ version: link:../swc-plugin-extractor po-parser: - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^2.1.1 + version: 2.1.1 use-intl: specifier: workspace:^ version: link:../use-intl @@ -12235,8 +12235,8 @@ packages: resolution: {integrity: sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==} engines: {node: '>=6'} - po-parser@2.0.0: - resolution: {integrity: sha512-SZvoKi3PoI/hHa2V9je9CW7Xgxl4dvO74cvaa6tWShIHT51FkPxje6pt0gTJznJrU67ix91nDaQp2hUxkOYhKA==} + po-parser@2.1.1: + resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==} points-on-curve@0.2.0: resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} @@ -15333,6 +15333,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-fetch@3.6.2: resolution: {integrity: sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==} @@ -30938,7 +30939,7 @@ snapshots: transitivePeerDependencies: - typescript - po-parser@2.0.0: {} + po-parser@2.1.1: {} points-on-curve@0.2.0: {}