mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-04-09 14:54:04 +02:00
Merge pull request #20747 from calixteman/check__l10n
Add a script for searching the unused fluent ids
This commit is contained in:
commit
b4f98e8a9b
@ -479,7 +479,7 @@ export default [
|
||||
Other
|
||||
\* ======================================================================== */
|
||||
{
|
||||
files: ["gulpfile.mjs"],
|
||||
files: ["gulpfile.mjs", "check_l10n.mjs"],
|
||||
languageOptions: { globals: globals.node },
|
||||
},
|
||||
];
|
||||
|
||||
190
external/check_l10n/check_l10n.mjs
vendored
Normal file
190
external/check_l10n/check_l10n.mjs
vendored
Normal file
@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/* Copyright 2026 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks that every message ID defined in l10n/en-US/viewer.ftl is referenced
|
||||
* in at least one HTML or JS/MJS file under the web/ directory.
|
||||
*
|
||||
* Usage: node external/check_l10n/check_l10n.mjs
|
||||
*/
|
||||
|
||||
import { extname, join } from "path";
|
||||
import { readdirSync, readFileSync, statSync } from "fs";
|
||||
|
||||
const ROOT = join(import.meta.dirname, "..", "..");
|
||||
const FTL_PATH = join(ROOT, "l10n", "en-US", "viewer.ftl");
|
||||
const SEARCH_DIRS = ["web", "src"];
|
||||
const SEARCH_EXTENSIONS = new Set([".html", ".js", ".mjs"]);
|
||||
// Minimum number of characters a prefix or suffix fragment must have to be
|
||||
// considered a meaningful match when detecting dynamically-built IDs.
|
||||
const MIN_FRAGMENT_LENGTH = 6;
|
||||
|
||||
/**
|
||||
* Extract all message IDs from a Fluent (.ftl) file.
|
||||
* A message ID is an identifier at the start of a line followed by " =".
|
||||
* @param {string} ftlPath - Absolute path to the .ftl file.
|
||||
* @returns {string[]} Ordered list of message IDs.
|
||||
*/
|
||||
function extractFtlIds(ftlPath) {
|
||||
const lines = readFileSync(ftlPath, "utf8").split("\n");
|
||||
const ids = [];
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^([a-zA-Z][a-zA-Z0-9-]*)\s*=/);
|
||||
if (match) {
|
||||
ids.push(match[1]);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively collect all files with matching extensions under a directory.
|
||||
* @param {string} dir - Directory to walk.
|
||||
* @param {Set<string>} extensions - Allowed file extensions (e.g. `".js"`).
|
||||
* @returns {string[]} Absolute paths of matching files.
|
||||
*/
|
||||
function collectFiles(dir, extensions) {
|
||||
const results = [];
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const fullPath = join(dir, entry);
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
results.push(...collectFiles(fullPath, extensions));
|
||||
} else if (extensions.has(extname(entry))) {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the contents of all source files found under the given directories.
|
||||
* @param {string[]} dirs - Directory names relative to ROOT.
|
||||
* @param {Set<string>} extensions - Allowed file extensions.
|
||||
* @returns {{ path: string, content: string }[]}
|
||||
*/
|
||||
function loadSources(dirs, extensions) {
|
||||
const files = dirs.flatMap(d => collectFiles(join(ROOT, d), extensions));
|
||||
return files.map(f => ({ path: f, content: readFileSync(f, "utf8") }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a message ID appears as a quoted string literal in any source
|
||||
* file. Handles double quotes, single quotes, and backticks, covering:
|
||||
* - `data-l10n-id="pdfjs-foo"` (HTML attribute)
|
||||
* - `"pdfjs-foo"` / `'pdfjs-foo'` / `` `pdfjs-foo` `` (JS string literals,
|
||||
* `setAttribute`, `l10n.get`, …)
|
||||
* @param {string} id - Message ID to look up.
|
||||
* @param {{ path: string, content: string }[]} sources
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isUsed(id, sources) {
|
||||
const dq = `"${id}"`;
|
||||
const sq = `'${id}'`;
|
||||
const bt = `\`${id}\``;
|
||||
return sources.some(
|
||||
({ content }) =>
|
||||
content.includes(dq) || content.includes(sq) || content.includes(bt)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* For IDs not found as complete literals, check whether the ID is likely
|
||||
* constructed dynamically via a template literal such as:
|
||||
* `pdfjs-editor-${editorType}-added-alert`
|
||||
*
|
||||
* Strategy: try every (prefix, suffix) pair obtained by splitting the ID's
|
||||
* dash-separated components, leaving one or more components as the "variable"
|
||||
* gap. The prefix must appear immediately followed by `${` in a template
|
||||
* literal; the suffix (if non-empty) must also appear in the same file.
|
||||
* Minimum length guards prevent matches on trivially short fragments.
|
||||
*
|
||||
* @param {string} id - Message ID to test.
|
||||
* @param {{ path: string, content: string }[]} sources
|
||||
* @returns {{ path: string, line: number } | null} Location of the first
|
||||
* matching template literal, or `null` if none found.
|
||||
*/
|
||||
function findDynamicLocation(id, sources) {
|
||||
const parts = id.split("-");
|
||||
// i = end of prefix (exclusive), j = start of suffix (inclusive)
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
for (let j = i + 1; j <= parts.length; j++) {
|
||||
const prefix = parts.slice(0, i).join("-") + "-"; // e.g. "pdfjs-editor-"
|
||||
const suffix = j < parts.length ? "-" + parts.slice(j).join("-") : ""; // e.g. "-added-alert"
|
||||
if (prefix.length < MIN_FRAGMENT_LENGTH) {
|
||||
continue;
|
||||
}
|
||||
if (suffix !== "" && suffix.length < MIN_FRAGMENT_LENGTH) {
|
||||
continue;
|
||||
}
|
||||
// The prefix must be immediately followed by "${" in a template literal.
|
||||
const prefixWithVar = prefix + "${";
|
||||
for (const { path, content } of sources) {
|
||||
if (
|
||||
content.includes(prefixWithVar) &&
|
||||
(suffix === "" || content.includes(suffix))
|
||||
) {
|
||||
const idx = content.indexOf(prefixWithVar);
|
||||
const line = content.slice(0, idx).split("\n").length;
|
||||
return { path, line };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const ids = extractFtlIds(FTL_PATH);
|
||||
console.log(`Found ${ids.length} message IDs in viewer.ftl\n`);
|
||||
|
||||
const sources = loadSources(SEARCH_DIRS, SEARCH_EXTENSIONS);
|
||||
console.log(
|
||||
`Searching in ${sources.length} files under: ${SEARCH_DIRS.join(", ")}\n`
|
||||
);
|
||||
|
||||
const notFound = ids.filter(id => !isUsed(id, sources));
|
||||
const dynamicEntries = notFound
|
||||
.map(id => ({ id, loc: findDynamicLocation(id, sources) }))
|
||||
.filter(({ loc }) => loc !== null);
|
||||
const dynamicIds = new Set(dynamicEntries.map(({ id }) => id));
|
||||
const unused = notFound.filter(id => !dynamicIds.has(id));
|
||||
|
||||
if (dynamicEntries.length > 0) {
|
||||
console.log(
|
||||
`~ ${dynamicEntries.length} ID(s) likely built dynamically (template literals):\n`
|
||||
);
|
||||
for (const { id, loc } of dynamicEntries) {
|
||||
const rel = loc.path.replace(ROOT + "/", "").replace(ROOT + "\\", "");
|
||||
console.log(` ${id}`);
|
||||
console.log(` → ${rel}:${loc.line}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (unused.length === 0) {
|
||||
console.log("✓ All remaining message IDs are used.");
|
||||
} else {
|
||||
console.log(`✗ ${unused.length} unused message ID(s):\n`);
|
||||
for (const id of unused) {
|
||||
console.log(` ${id}`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
15
gulpfile.mjs
15
gulpfile.mjs
@ -2252,6 +2252,21 @@ gulp.task("importl10n", async function () {
|
||||
await downloadL10n(L10N_DIR);
|
||||
});
|
||||
|
||||
gulp.task("check_l10n", function (done) {
|
||||
console.log("\n### Checking for unused l10n IDs");
|
||||
|
||||
const checkProcess = startNode(["external/check_l10n/check_l10n.mjs"], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
checkProcess.on("close", function (code) {
|
||||
if (code !== 0) {
|
||||
done(new Error("check_l10n failed."));
|
||||
return;
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
function ghPagesPrepare() {
|
||||
console.log("\n### Creating web site");
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user