Skip to content

Commit 218a589

Browse files
authored
fix: limit precommit localization extraction to staged files (#22096)
1 parent 1da5be6 commit 218a589

1 file changed

Lines changed: 146 additions & 51 deletions

File tree

scripts/localization-extract.js

Lines changed: 146 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
4953
function 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

221259
async 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

233327
module.exports = {
234328
extractLocalizationStrings,
235329
extractLocalizationForExtension,
236330
extractLocalizationForPrecommit,
331+
extractLocalizationForExtensionPrecommit,
237332
getL10nJson,
238333
};
239334

0 commit comments

Comments
 (0)