2026-10-10 14:58:27 -05:00
#!/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" ,
);
2026-10-12 11:35:48 -05:00
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` ;
}
2026-10-10 14:58:27 -05:00
function keysIn ( text ) {
return [
2026-10-12 11:35:48 -05:00
... new Set (
[... text . matchAll ( /\bt\(\s*"([\w.-]+)"/g )]. map (( m ) => realKey ( m [ 1 ])),
),
2026-10-10 14:58:27 -05:00
];
}
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" )),
2026-10-12 11:35:48 -05:00
updates : [
... new Set (
[ "UpdatesSettings.tsx" , "DesktopUpdatesCard.tsx" ]. flatMap (( file ) =>
keysIn (
fs . readFileSync (
path . join ( ROOT , "src" , "ui" , "settings" , file ),
"utf8" ,
),
),
),
2026-10-10 14:58:27 -05:00
),
2026-10-12 11:35:48 -05:00
],
2026-10-10 14:58:27 -05:00
};
}
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 ) } ` );
}