@@ -46,6 +46,10 @@ function getStagedFiles() {
4646 . filter ( Boolean ) ;
4747}
4848
49+ function getStagedFileContent ( file ) {
50+ return execFileSync ( "git" , [ "show" , `:${ file } ` ] , { encoding : "utf8" } ) ;
51+ }
52+
4953function matchesInput ( file , input ) {
5054 return input . endsWith ( "/" ) ? file . startsWith ( input ) : file === input ;
5155}
@@ -64,6 +68,22 @@ function getAffectedExtensionsForPrecommit() {
6468 ) ;
6569}
6670
71+ async function getL10nJsonFromFileContents ( fileContents ) {
72+ logger . step ( "Extracting localization strings from source code..." ) ;
73+
74+ const result = await vscodel10n . getL10nJson (
75+ fileContents . map ( ( f ) => ( {
76+ contents : f . contents ,
77+ extension : f . extension ,
78+ } ) ) ,
79+ ) ;
80+
81+ const stringCount = Object . keys ( result ) . length ;
82+ logger . success ( `Extracted ${ stringCount } localization strings` ) ;
83+
84+ return result ;
85+ }
86+
6787/**
6888 * Scans the src directory of an extension for TypeScript files and extracts their content
6989 * @param {string } extensionPath - Path to the extension directory
@@ -102,26 +122,77 @@ async function getL10nJson(extensionPath) {
102122 }
103123
104124 logger . success ( `Successfully processed ${ processedFiles } source files` ) ;
105- logger . step ( "Extracting localization strings from source code..." ) ;
106-
107- // Extract L10n data using vscode l10n tools
108- const result = await vscodel10n . getL10nJson (
109- fileContents . map ( ( f ) => ( {
110- contents : f . contents ,
111- extension : f . extension ,
112- } ) ) ,
113- ) ;
125+ return await getL10nJsonFromFileContents ( fileContents ) ;
126+ } catch ( error ) {
127+ logger . error ( `Failed to extract L10n JSON: ${ error . message } ` ) ;
128+ throw error ;
129+ }
130+ }
131+
132+ function getStagedSourceFilesForExtension ( extensionDir , stagedFiles ) {
133+ const srcPrefix = `extensions/${ extensionDir } /src/` ;
134+ return stagedFiles . filter (
135+ ( file ) => file . startsWith ( srcPrefix ) && ( file . endsWith ( ".ts" ) || file . endsWith ( ".tsx" ) ) ,
136+ ) ;
137+ }
114138
115- const stringCount = Object . keys ( result ) . length ;
116- logger . success ( `Extracted ${ stringCount } localization strings` ) ;
139+ async function readJsonFile ( filePath , stagedFiles = [ ] ) {
140+ const normalizedPath = filePath . replace ( / \\ / g, "/" ) ;
141+ const content = stagedFiles . includes ( normalizedPath )
142+ ? getStagedFileContent ( normalizedPath )
143+ : await fs . readFile ( filePath , "utf8" ) ;
144+ return JSON . parse ( content ) ;
145+ }
117146
118- return result ;
147+ async function readJsonFileOrEmptyIfMissing ( filePath , stagedFiles = [ ] ) {
148+ try {
149+ return await readJsonFile ( filePath , stagedFiles ) ;
119150 } catch ( error ) {
120- logger . error ( `Failed to extract L10n JSON: ${ error . message } ` ) ;
151+ if ( error . code === "ENOENT" ) {
152+ return { } ;
153+ }
154+
121155 throw error ;
122156 }
123157}
124158
159+ async function writeLocalizationOutputs ( extensionDir , xliffName , packageJSON , bundleJSON ) {
160+ const map = new Map ( ) ;
161+ map . set ( "package" , packageJSON ) ;
162+ map . set ( "bundle" , bundleJSON ) ;
163+
164+ const extensionPath = path . resolve ( "extensions" , extensionDir ) ;
165+ const extensionL10nDir = path . join ( extensionPath , "l10n" ) ;
166+ await fs . mkdir ( extensionL10nDir , { recursive : true } ) ;
167+ await fs . mkdir ( "localization/xliff" , { recursive : true } ) ;
168+
169+ logger . step ( "Writing bundle localization file..." ) ;
170+ const bundlePath = path . join ( extensionL10nDir , "bundle.l10n.json" ) ;
171+ const formatted1 = await writeJsonAndFormat ( bundlePath , bundleJSON ) ;
172+ if ( formatted1 ) {
173+ logger . success ( `Created and formatted ${ bundlePath } ` ) ;
174+ } else {
175+ logger . warning ( `Created ${ bundlePath } (formatting failed)` ) ;
176+ }
177+
178+ logger . step ( "Generating XLIFF file for translation..." ) ;
179+ const stringXLIFF = vscodel10n . getL10nXlf ( map ) ;
180+ const xliffPath = `localization/xliff/${ xliffName } .xlf` ;
181+ const formatted2 = await writeAndFormat (
182+ xliffPath ,
183+ stringXLIFF ,
184+ false , // We don't want to run prettier on XLIFF files
185+ true , // Use CRLF line endings to match .gitattributes
186+ ) ;
187+ if ( formatted2 ) {
188+ logger . success ( `Created ${ xliffPath } ` ) ;
189+ } else {
190+ logger . warning ( `Created ${ xliffPath } (formatting failed)` ) ;
191+ }
192+
193+ return [ bundlePath . replace ( / \\ / g, "/" ) , xliffPath ] ;
194+ }
195+
125196/**
126197 * Extracts localization strings for a single extension
127198 * @param {string } extensionDir - Extension directory name
@@ -138,51 +209,18 @@ async function extractLocalizationForExtension(extensionDir, xliffName) {
138209
139210 logger . step ( "Loading package localization data..." ) ;
140211
141- // Create map with package and bundle localization data
142- const map = new Map ( ) ;
143-
212+ let packageJSON ;
144213 try {
145214 const packageNlsPath = path . join ( extensionPath , "package.nls.json" ) ;
146215 const packageNlsContent = await fs . readFile ( packageNlsPath , "utf8" ) ;
147- map . set ( "package" , JSON . parse ( packageNlsContent ) ) ;
216+ packageJSON = JSON . parse ( packageNlsContent ) ;
148217 logger . success ( "Loaded package.nls.json" ) ;
149218 } catch ( error ) {
150219 logger . warning ( `Could not load package.nls.json: ${ error . message } ` ) ;
151- map . set ( "package" , { } ) ;
152- }
153-
154- map . set ( "bundle" , bundleJSON ) ;
155-
156- // Ensure output directories exist
157- const extensionL10nDir = path . join ( extensionPath , "l10n" ) ;
158- await fs . mkdir ( extensionL10nDir , { recursive : true } ) ;
159- await fs . mkdir ( "localization/xliff" , { recursive : true } ) ;
160-
161- // Write bundle L10n JSON file to extension's l10n directory
162- logger . step ( "Writing bundle localization file..." ) ;
163- const bundlePath = path . join ( extensionL10nDir , "bundle.l10n.json" ) ;
164- const formatted1 = await writeJsonAndFormat ( bundlePath , bundleJSON ) ;
165- if ( formatted1 ) {
166- logger . success ( `Created and formatted ${ bundlePath } ` ) ;
167- } else {
168- logger . warning ( `Created ${ bundlePath } (formatting failed)` ) ;
220+ packageJSON = { } ;
169221 }
170222
171- // Generate XLIFF file for translators
172- logger . step ( "Generating XLIFF file for translation..." ) ;
173- const stringXLIFF = vscodel10n . getL10nXlf ( map ) ;
174- const xliffPath = `localization/xliff/${ xliffName } .xlf` ;
175- const formatted2 = await writeAndFormat (
176- xliffPath ,
177- stringXLIFF ,
178- false , // We don't want to run prettier on XLIFF files
179- true , // Use CRLF line endings to match .gitattributes
180- ) ;
181- if ( formatted2 ) {
182- logger . success ( `Created ${ xliffPath } ` ) ;
183- } else {
184- logger . warning ( `Created ${ xliffPath } (formatting failed)` ) ;
185- }
223+ await writeLocalizationOutputs ( extensionDir , xliffName , packageJSON , bundleJSON ) ;
186224
187225 logger . success ( `Localization extraction for ${ extensionDir } completed successfully!` ) ;
188226 logger . newline ( ) ;
@@ -219,21 +257,78 @@ async function extractLocalizationStrings() {
219257}
220258
221259async function extractLocalizationForPrecommit ( ) {
260+ const stagedFiles = getStagedFiles ( ) ;
222261 const affectedExtensions = getAffectedExtensionsForPrecommit ( ) ;
262+ const forceFullExtraction = stagedFiles . some ( ( file ) =>
263+ PRECOMMIT_FULL_EXTRACTION_INPUTS . includes ( file ) ,
264+ ) ;
223265 if ( ! affectedExtensions . length ) {
224266 logger . info ( "No staged localization inputs; skipping localization extraction." ) ;
225267 return ;
226268 }
227269
228270 for ( const extensionDir of affectedExtensions ) {
229- await extractLocalizationForExtension ( extensionDir , EXTENSION_CONFIG [ extensionDir ] ) ;
271+ await extractLocalizationForExtensionPrecommit (
272+ extensionDir ,
273+ EXTENSION_CONFIG [ extensionDir ] ,
274+ stagedFiles ,
275+ forceFullExtraction ,
276+ ) ;
277+ }
278+ }
279+
280+ async function extractLocalizationForExtensionPrecommit (
281+ extensionDir ,
282+ xliffName ,
283+ stagedFiles ,
284+ forceFullExtraction = false ,
285+ ) {
286+ logger . header ( `Processing staged localization inputs for Extension: ${ extensionDir } ` ) ;
287+
288+ const extensionPath = path . resolve ( "extensions" , extensionDir ) ;
289+ const packageNlsPath = `extensions/${ extensionDir } /package.nls.json` ;
290+ const bundlePath = `extensions/${ extensionDir } /l10n/bundle.l10n.json` ;
291+
292+ try {
293+ const stagedSourceFiles = getStagedSourceFilesForExtension ( extensionDir , stagedFiles ) ;
294+ const bundleJSON =
295+ forceFullExtraction || stagedSourceFiles . length
296+ ? await getL10nJson ( extensionPath )
297+ : await readJsonFileOrEmptyIfMissing ( bundlePath , stagedFiles ) ;
298+
299+ logger . step ( "Loading package localization data..." ) ;
300+ let packageJSON ;
301+ try {
302+ packageJSON = await readJsonFile ( packageNlsPath , stagedFiles ) ;
303+ logger . success ( "Loaded package.nls.json" ) ;
304+ } catch ( error ) {
305+ logger . warning ( `Could not load package.nls.json: ${ error . message } ` ) ;
306+ packageJSON = { } ;
307+ }
308+
309+ const generatedFiles = await writeLocalizationOutputs (
310+ extensionDir ,
311+ xliffName ,
312+ packageJSON ,
313+ bundleJSON ,
314+ ) ;
315+ execFileSync ( "git" , [ "add" , ...generatedFiles ] , { stdio : "inherit" } ) ;
316+
317+ logger . success (
318+ `Staged localization extraction for ${ extensionDir } completed successfully!` ,
319+ ) ;
320+ logger . newline ( ) ;
321+ } catch ( error ) {
322+ logger . error ( `Staged localization extraction for ${ extensionDir } failed: ${ error . message } ` ) ;
323+ throw error ;
230324 }
231325}
232326
233327module . exports = {
234328 extractLocalizationStrings,
235329 extractLocalizationForExtension,
236330 extractLocalizationForPrecommit,
331+ extractLocalizationForExtensionPrecommit,
237332 getL10nJson,
238333} ;
239334
0 commit comments