From 2e846933e9c3c82c93dca20375bdbc36800bc420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 23 Aug 2026 05:26:43 +0000 Subject: [PATCH] shortcut registry keyed (96 -> 73) shortcuts.ts is data with no component around it, so it carries catalog keys and ShortcutsModal resolves them -- same shape as the settings sections. gen-platform-skill.ts also reads this registry to generate the agent-facing SKILL.md, which docs:check validates. That doc stays in one language, so the generator resolves against en-GB directly and throws if a key is missing rather than emitting a dot-path into a file an agent will quote at somebody. The regenerated skill is byte-identical, which is the check that keying the registry changed nothing an agent reads. --- scripts/gen-platform-skill.ts | 20 ++++-- .../Dashboard/Layout/ShortcutsModal.tsx | 12 ++-- src/apps/officer-web/locales/en-GB.json | 27 ++++++++ src/apps/officer-web/locales/pt-PT.json | 27 ++++++++ src/apps/officer-web/state/shortcuts.ts | 63 ++++++++++--------- 5 files changed, 113 insertions(+), 36 deletions(-) diff --git a/scripts/gen-platform-skill.ts b/scripts/gen-platform-skill.ts index 895b01ab..04db2fc2 100644 --- a/scripts/gen-platform-skill.ts +++ b/scripts/gen-platform-skill.ts @@ -38,6 +38,16 @@ const KIND_MEANING: Record = { export async function renderPlatformSkill(): Promise { const { PERMISSIONS } = await import('../src/servers/permissions/registry'); const { SHORTCUT_SECTIONS } = await import('../src/apps/officer-web/state/shortcuts'); + const enCatalog = (await import('../src/apps/officer-web/locales/en-GB.json')).default as Record; + /** Resolve a dot-path against the English catalog. This doc is for agents and stays in one language. */ + const enString = (key: string): string => { + const value = key.split('.').reduce((node, part) => { + if (node && typeof node === 'object') return (node as Record)[part]; + return undefined; + }, enCatalog); + if (typeof value !== 'string') throw new Error(`gen-platform-skill: no English string for "${key}"`); + return value; + }; const byKind = new Map(); for (const p of PERMISSIONS) { @@ -112,14 +122,16 @@ export async function renderPlatformSkill(): Promise { out('Derived from `src/apps/officer-web/state/shortcuts.ts`. These have moved several times; quote this'); out('list rather than remembering one.'); out(); + // The registry holds catalog keys since the shell was localized; this doc is agent-facing and stays + // English, so it resolves against en-GB rather than the reader's locale. for (const section of SHORTCUT_SECTIONS) { - out(`## ${section.title}`); - if (section.note) { + out(`## ${enString(section.titleKey)}`); + if (section.noteKey) { out(); - out(section.note); + out(enString(section.noteKey)); } out(); - for (const s of section.shortcuts) out(`- \`${s.keys}\` — ${s.description}`); + for (const s of section.shortcuts) out(`- \`${s.keys}\` — ${enString(s.descriptionKey)}`); out(); } diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/ShortcutsModal.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/ShortcutsModal.tsx index 8b5c5242..2415a17d 100644 --- a/src/apps/officer-web/Screens/Dashboard/Layout/ShortcutsModal.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Layout/ShortcutsModal.tsx @@ -66,12 +66,16 @@ export const ShortcutsModal = () => { {/* Only this scrolls — the header stays, for the same reason the theme dropdown's does. */}
{SHORTCUT_SECTIONS.map((section) => ( -
-

{section.title}

- {section.note &&

{section.note}

} +
+

+ {t(section.titleKey)} +

+ {section.noteKey && ( +

{t(section.noteKey)}

+ )}
{section.shortcuts.map((shortcut) => ( - + ))}
diff --git a/src/apps/officer-web/locales/en-GB.json b/src/apps/officer-web/locales/en-GB.json index 20c917dd..12236a60 100644 --- a/src/apps/officer-web/locales/en-GB.json +++ b/src/apps/officer-web/locales/en-GB.json @@ -609,5 +609,32 @@ "camera": "Camera:", "model": "Model:" } + }, + "shortcuts": { + "navigation": "Navigation", + "spotlight": "Spotlight — go to any screen, plugin or setting by name", + "thisList": "This list", + "themes": "Themes", + "themesNote": "The browser applies as you move, so you watch the app change rather than reading names.", + "openBrowser": "Open the theme browser", + "prevNextTheme": "Previous / next theme, without opening anything", + "starCurrent": "Star or unstar the current theme, from anywhere", + "inBrowser": "In the theme browser", + "moveList": "Move through the list", + "applyKeepBrowsing": "Apply the focused theme and keep browsing", + "restoreStart": "Put back the theme you started on", + "starFocused": "Star or unstar the focused theme", + "keepLooking": "Keep what you are looking at", + "cancelRestore": "Cancel and restore", + "inSpotlight": "In Spotlight", + "moveResults": "Move through the results", + "goThere": "Go there", + "close": "Close", + "chat": "Chat", + "chatNote": "Recall only takes the arrow key from an EMPTY composer, and any other key ends a recall — so a draft is never overwritten.", + "prevNextPrompt": "Previous / next prompt you sent, shell-style", + "send": "Send · on an empty composer with something queued, send that now", + "newline": "Newline", + "stopTurn": "Stop the turn that is generating" } } diff --git a/src/apps/officer-web/locales/pt-PT.json b/src/apps/officer-web/locales/pt-PT.json index 3a288e33..cb03a0d1 100644 --- a/src/apps/officer-web/locales/pt-PT.json +++ b/src/apps/officer-web/locales/pt-PT.json @@ -609,5 +609,32 @@ "camera": "Câmara:", "model": "Modelo:" } + }, + "shortcuts": { + "navigation": "Navegação", + "spotlight": "Spotlight — ir para qualquer ecrã, plugin ou definição pelo nome", + "thisList": "Esta lista", + "themes": "Temas", + "themesNote": "O explorador aplica à medida que se move, para que veja a aplicação mudar em vez de ler nomes.", + "openBrowser": "Abrir o explorador de temas", + "prevNextTheme": "Tema anterior / seguinte, sem abrir nada", + "starCurrent": "Marcar ou desmarcar o tema atual, a partir de qualquer lugar", + "inBrowser": "No explorador de temas", + "moveList": "Percorrer a lista", + "applyKeepBrowsing": "Aplicar o tema focado e continuar a explorar", + "restoreStart": "Voltar ao tema em que começou", + "starFocused": "Marcar ou desmarcar o tema focado", + "keepLooking": "Manter o que está a ver", + "cancelRestore": "Cancelar e restaurar", + "inSpotlight": "No Spotlight", + "moveResults": "Percorrer os resultados", + "goThere": "Ir para lá", + "close": "Fechar", + "chat": "Chat", + "chatNote": "A recuperação só usa a tecla de seta num compositor VAZIO, e qualquer outra tecla termina a recuperação — pelo que um rascunho nunca é substituído.", + "prevNextPrompt": "Prompt anterior / seguinte que enviou, à maneira da shell", + "send": "Enviar · num compositor vazio com algo em fila, enviar isso agora", + "newline": "Nova linha", + "stopTurn": "Parar o turno que está a gerar" } } diff --git a/src/apps/officer-web/state/shortcuts.ts b/src/apps/officer-web/state/shortcuts.ts index cec5013c..e4ea9b1f 100644 --- a/src/apps/officer-web/state/shortcuts.ts +++ b/src/apps/officer-web/state/shortcuts.ts @@ -1,3 +1,5 @@ +import type { AppTranslationKey } from '@/lib/i18n'; + /* Every keyboard shortcut the shell owns, as data. @@ -22,92 +24,97 @@ export type Shortcut = { /** As a person reads it — `Ctrl+Shift+Space`, not a KeyboardEvent spelling. */ keys: string; - description: string; + /** A catalog key. This is data with no component around it, so ShortcutsModal resolves it. */ + descriptionKey: AppTranslationKey; /** The file whose handler actually implements it, until this registry is authoritative. */ implementedIn: string; }; export type ShortcutSection = { - title: string; + titleKey: AppTranslationKey; /** Shown under the title when the section needs a caveat that a per-row description cannot carry. */ - note?: string; + noteKey?: AppTranslationKey; shortcuts: Shortcut[]; }; export const SHORTCUT_SECTIONS: ShortcutSection[] = [ { - title: 'Navigation', + titleKey: 'shortcuts.navigation', shortcuts: [ { keys: 'Shift+Space', - description: 'Spotlight — go to any screen, plugin or setting by name', + descriptionKey: 'shortcuts.spotlight', implementedIn: 'Layout/Spotlight/Spotlight.tsx', }, - { keys: 'Ctrl+?', description: 'This list', implementedIn: 'Layout/ShortcutsModal.tsx' }, + { keys: 'Ctrl+?', descriptionKey: 'shortcuts.thisList', implementedIn: 'Layout/ShortcutsModal.tsx' }, ], }, { - title: 'Themes', - note: 'The browser applies as you move, so you watch the app change rather than reading names.', + titleKey: 'shortcuts.themes', + noteKey: 'shortcuts.themesNote', shortcuts: [ { keys: 'Ctrl+Shift+Space', - description: 'Open the theme browser', + descriptionKey: 'shortcuts.openBrowser', implementedIn: 'theme/useThemeKeys.ts', }, { keys: 'Ctrl+Shift+← / →', - description: 'Previous / next theme, without opening anything', + descriptionKey: 'shortcuts.prevNextTheme', implementedIn: 'theme/useThemeKeys.ts', }, { keys: 'Ctrl+Shift+F', - description: 'Star or unstar the current theme, from anywhere', + descriptionKey: 'shortcuts.starCurrent', implementedIn: 'theme/useThemeKeys.ts', }, ], }, { - title: 'In the theme browser', + titleKey: 'shortcuts.inBrowser', shortcuts: [ - { keys: 'j / k ↑ / ↓', description: 'Move through the list', implementedIn: 'theme/useThemeKeys.ts' }, + { keys: 'j / k ↑ / ↓', descriptionKey: 'shortcuts.moveList', implementedIn: 'theme/useThemeKeys.ts' }, { keys: 'l →', - description: 'Apply the focused theme and keep browsing', + descriptionKey: 'shortcuts.applyKeepBrowsing', implementedIn: 'theme/useThemeKeys.ts', }, - { keys: 'h ←', description: 'Put back the theme you started on', implementedIn: 'theme/useThemeKeys.ts' }, - { keys: 'f', description: 'Star or unstar the focused theme', implementedIn: 'theme/useThemeKeys.ts' }, - { keys: 'Enter', description: 'Keep what you are looking at', implementedIn: 'theme/useThemeKeys.ts' }, - { keys: 'Esc', description: 'Cancel and restore', implementedIn: 'theme/useThemeKeys.ts' }, + { keys: 'h ←', descriptionKey: 'shortcuts.restoreStart', implementedIn: 'theme/useThemeKeys.ts' }, + { keys: 'f', descriptionKey: 'shortcuts.starFocused', implementedIn: 'theme/useThemeKeys.ts' }, + { keys: 'Enter', descriptionKey: 'shortcuts.keepLooking', implementedIn: 'theme/useThemeKeys.ts' }, + { keys: 'Esc', descriptionKey: 'shortcuts.cancelRestore', implementedIn: 'theme/useThemeKeys.ts' }, ], }, { - title: 'In Spotlight', + titleKey: 'shortcuts.inSpotlight', shortcuts: [ - { keys: '↑ / ↓', description: 'Move through the results', implementedIn: 'Layout/Spotlight/Spotlight.tsx' }, - { keys: 'Enter', description: 'Go there', implementedIn: 'Layout/Spotlight/Spotlight.tsx' }, - { keys: 'Esc', description: 'Close', implementedIn: 'Layout/Spotlight/Spotlight.tsx' }, + { keys: '↑ / ↓', descriptionKey: 'shortcuts.moveResults', implementedIn: 'Layout/Spotlight/Spotlight.tsx' }, + { keys: 'Enter', descriptionKey: 'shortcuts.goThere', implementedIn: 'Layout/Spotlight/Spotlight.tsx' }, + { keys: 'Esc', descriptionKey: 'shortcuts.close', implementedIn: 'Layout/Spotlight/Spotlight.tsx' }, ], }, { - title: 'Chat', - note: 'Recall only takes the arrow key from an EMPTY composer, and any other key ends a recall — so a draft is never overwritten.', + titleKey: 'shortcuts.chat', + noteKey: 'shortcuts.chatNote', shortcuts: [ { keys: '↑ / ↓', - description: 'Previous / next prompt you sent, shell-style', + descriptionKey: 'shortcuts.prevNextPrompt', implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts', }, { keys: 'Enter', - description: 'Send · on an empty composer with something queued, send that now', + descriptionKey: 'shortcuts.send', + implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts', + }, + { + keys: 'Shift+Enter', + descriptionKey: 'shortcuts.newline', implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts', }, - { keys: 'Shift+Enter', description: 'Newline', implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts' }, { keys: 'Esc', - description: 'Stop the turn that is generating', + descriptionKey: 'shortcuts.stopTurn', implementedIn: 'Chat/EmbeddableChat/useEmbeddableChat.ts', }, ],