Files
Termix/scripts/generate-settings-search-index.cjs
T
62ac931468 v26.10.1 (#1599)
* feat: AI translations at release, 7 new languages and regional fallback

* fix: proper plural forms for count strings and full translations

* fix: plural forms for selected, hidden and active counts

* fix: synced plugin updates failing on desktop and server-only plugins shown as stopped

* fix: restore rounded corners on app icons and favicon

* feat: desktop app auto updates with automatic, ask first or never modes

* feat: community and custom plugin registries

* feat: plugin guide for AI tools in the SDK with a lint check

* fix: keep the terminal WebSocket open past nginx's 60s default (#1588)

* chore: update changelog

* feat: show plugin download counts only and stop CI builds inflating them

* feat: hide installed plugins in the browse tab by default

* chore: pin bundled plugins to latest releases

* chore: update translations

* fix: focusing a saved password field and saving no longer erases it

* fix: tell desktop users when their 2.8 data needs 2.9 first

* fix: point windows update info at the x64 installer

* fix: keep desktop client tunnel sockets alive behind proxies

* chore: pin bundled plugins to latest releases

* fix: cover client tunnel keepalive in the c2s runtime tests

* fix: typing after opening a host no longer goes to the host search

* fix: accept tab and = separators in SSH config import (#1598)

* chore: update changelog

* chore: drop plugin versions from core changelog

* chore: update translations

* chore: lint, format, and bump version to 26.10.1

---------

Co-authored-by: Darnel Hunter <68697450+innotelinc@users.noreply.github.com>
Co-authored-by: Iain <Iain1970@protonmail.com>
2026-10-12 11:35:48 -05:00

177 lines
5.3 KiB
JavaScript

#!/usr/bin/env node
/**
* Builds src/ui/settings/settings-search-index.ts: the i18n keys each core
* settings section shows, so the Settings search can match on any label in
* a section without mounting it. Run it after changing a settings section.
*
* --check fails when the committed file is out of date.
*/
const fs = require("node:fs");
const path = require("node:path");
const ROOT = path.resolve(__dirname, "..");
const SIDEBAR = path.join(ROOT, "src", "ui", "sidebar");
const OUT = path.join(
ROOT,
"src",
"ui",
"settings",
"settings-search-index.ts",
);
function flatKeys(obj, prefix = "") {
return Object.entries(obj).flatMap(([key, value]) =>
typeof value === "object"
? flatKeys(value, `${prefix}${key}.`)
: [`${prefix}${key}`],
);
}
const EN_KEYS = new Set(
flatKeys(require(path.join(ROOT, "src", "ui", "locales", "en.json"))),
);
// t("x", { count }) reads x_one or x_other, so index the plural key.
function realKey(key) {
return EN_KEYS.has(key) || !EN_KEYS.has(`${key}_other`)
? key
: `${key}_other`;
}
function keysIn(text) {
return [
...new Set(
[...text.matchAll(/\bt\(\s*"([\w.-]+)"/g)].map((m) => realKey(m[1])),
),
];
}
function read(file) {
return fs.readFileSync(path.join(SIDEBAR, file), "utf8");
}
/** The body of one exported or local function, by name. */
function functionBody(source, name) {
const start = source.search(new RegExp(`function ${name}\\b`));
if (start < 0) throw new Error(`No function ${name}`);
const next = source.slice(start + 1).search(/\n(export )?function \w+/);
return next < 0 ? source.slice(start) : source.slice(start, start + 1 + next);
}
/** Profile sections, split on their AccordionSection ids. */
function profileSections() {
const source = read("UserProfilePanel.tsx");
const lines = source.split("\n");
const starts = [];
lines.forEach((line, i) => {
if (/^\s{6}<AccordionSection\b/.test(line)) starts.push(i);
});
const out = {};
starts.forEach((start, k) => {
const end = k + 1 < starts.length ? starts[k + 1] : lines.length;
const chunk = lines.slice(start, end).join("\n");
const id = (chunk.match(/\bid="([^"]+)"/) || [])[1];
if (id) out[id] = keysIn(chunk);
});
out.interface = [
...new Set([
...(out.interface ?? []),
...keysIn(read("InterfacePresetSettings.tsx")),
]),
];
out.security = [
...new Set([
...(out.security ?? []),
...keysIn(read("AuthEnrollmentSections.tsx")),
...keysIn(functionBody(source, "PasswordChangeSection")),
]),
];
out["api-keys"] = [
...new Set([
...(out["api-keys"] ?? []),
...keysIn(functionBody(source, "NewApiKeyDialog")),
]),
];
out.keybindings = keysIn(
fs.readFileSync(
path.join(ROOT, "src", "ui", "settings", "KeybindingsSettings.tsx"),
"utf8",
),
);
return out;
}
function adminSections() {
const settings = read("AdminSettingsSections.tsx");
const management = read("AdminManagementSections.tsx");
return {
"admin-general": [
...new Set([
...keysIn(functionBody(settings, "AdminGeneralSettingsSection")),
...keysIn(read("AdminPluginDeveloperMode.tsx")),
]),
],
"admin-users": [
...new Set([
...keysIn(functionBody(management, "AdminUsersSection")),
...keysIn(read("AdminUserDialogs.tsx")),
]),
],
"admin-sessions": keysIn(functionBody(management, "AdminSessionsSection")),
"admin-roles": keysIn(functionBody(management, "AdminRolesSection")),
"admin-host-defaults": [
...new Set([
...keysIn(functionBody(settings, "AdminHostDefaultsSection")),
...keysIn(read("AdminHostTags.tsx")),
]),
],
"admin-branding": keysIn(read("AdminBrandingSection.tsx")),
"admin-database": keysIn(functionBody(settings, "AdminDatabaseSection")),
"admin-ssl": keysIn(functionBody(settings, "AdminSSLSection")),
"admin-api-keys": keysIn(read("AdminApiKeysSection.tsx")),
"admin-audit-log": keysIn(read("AdminAuditLogSection.tsx")),
updates: [
...new Set(
["UpdatesSettings.tsx", "DesktopUpdatesCard.tsx"].flatMap((file) =>
keysIn(
fs.readFileSync(
path.join(ROOT, "src", "ui", "settings", file),
"utf8",
),
),
),
),
],
};
}
function render() {
const index = { ...profileSections(), ...adminSections() };
const body = Object.entries(index)
.sort(([a], [b]) => a.localeCompare(b))
.map(
([id, keys]) =>
` ${JSON.stringify(id)}: [\n${keys
.sort()
.map((key) => ` ${JSON.stringify(key)},`)
.join("\n")}\n ],`,
)
.join("\n");
return `// Generated by scripts/generate-settings-search-index.cjs. Do not edit.\n\n/** The i18n keys each core settings section shows, for search. */\nexport const SETTINGS_SEARCH_INDEX: Record<string, string[]> = {\n${body}\n};\n`;
}
const next = render();
if (process.argv.includes("--check")) {
const current = fs.existsSync(OUT) ? fs.readFileSync(OUT, "utf8") : "";
if (current !== next) {
console.error(
"src/ui/settings/settings-search-index.ts is out of date. Run: node scripts/generate-settings-search-index.cjs",
);
process.exit(1);
}
} else {
fs.writeFileSync(OUT, next);
console.log(`Wrote ${path.relative(ROOT, OUT)}`);
}