fix(desktop): prevent raw i18n key from leaking in slash command descriptions

getLocalizedFallbackCommands previously passed a constructed key to t()
without checking if the translation actually resolved. When the i18n
table lacked an entry, t() returned the raw key (e.g.
'slashCmd.clear.description'), which dosubot flagged in PR #593.

Fix: default to the static English description, only override when
t(key) returns a different string. Add two tests covering missing-key
fallback and partial-translation scenarios.

Co-Authored-By: qwen3.6-plus <QwenLM@claude-code-best.win>
This commit is contained in:
派大星 2026-05-26 22:52:18 +08:00
parent 2eaae94d40
commit b2dfeb11cf
2 changed files with 62 additions and 6 deletions

View File

@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
filterSlashCommands,
findSlashToken,
getLocalizedFallbackCommands,
insertSlashTrigger,
mergeSlashCommands,
replaceSlashCommand,
@ -143,4 +144,38 @@ describe('composerUtils', () => {
expect(resolveSlashUiAction('context')).toEqual({ type: 'panel', command: 'context' })
expect(resolveSlashUiAction('status')).toEqual({ type: 'panel', command: 'status' })
})
it('falls back to the static English description when a translation key is missing', () => {
// Simulate an i18n t() function that returns the raw key for missing entries
// (this is what the real translate() does via zh[key] ?? en[key] ?? key).
const mockT = (key: string) => key
const commands = getLocalizedFallbackCommands(mockT)
const clearCmd = commands.find((c) => c.name === 'clear')
expect(clearCmd?.description).toBe('Clear conversation history')
expect(clearCmd?.description).not.toBe('slashCmd.clear.description')
// Verify every command renders a human-readable description, never a raw key
for (const cmd of commands) {
expect(cmd.description).not.toMatch(/^slashCmd\./)
}
})
it('uses the localized description when the translation key resolves to a real string', () => {
const mockT = (key: string) => {
const map: Record<string, string> = {
'slashCmd.clear.description': '清空会话历史',
}
return map[key] ?? key
}
const commands = getLocalizedFallbackCommands(mockT)
const clearCmd = commands.find((c) => c.name === 'clear')
expect(clearCmd?.description).toBe('清空会话历史')
// A command without a translated key should still fall back to English
const mcpCmd = commands.find((c) => c.name === 'mcp')
expect(mcpCmd?.description).toBe('Open available MCP tools for the current chat context')
expect(mcpCmd?.description).not.toBe('slashCmd.mcp.description')
})
})

View File

@ -79,13 +79,34 @@ export const FALLBACK_SLASH_COMMANDS: SlashCommandOption[] = [
{ name: 'vim', description: 'Toggle vim editing mode' },
]
/** Build localized fallback commands using the current locale */
/** Build localized fallback commands using the current locale.
*
* Resolution order for each command's description:
* 1. Localized string from the i18n table (zh -> en) when a key is registered.
* 2. The static English description shipped in FALLBACK_SLASH_COMMANDS.
*
* This guarantees we never render a raw key (e.g. "slashCmd.foo.description")
* in the UI even if a command is missing from SLASH_CMD_DESCRIPTION_KEYS or
* its translation entry is absent.
*/
export function getLocalizedFallbackCommands(t: (key: TranslationKey) => string): SlashCommandOption[] {
return FALLBACK_SLASH_COMMANDS.map((cmd) => ({
name: cmd.name,
description: t(SLASH_CMD_DESCRIPTION_KEYS[cmd.name] ?? ('slashCmd.' + cmd.name + '.description' as TranslationKey)),
...(cmd.argumentHint && { argumentHint: cmd.argumentHint }),
}))
return FALLBACK_SLASH_COMMANDS.map((cmd) => {
const key = SLASH_CMD_DESCRIPTION_KEYS[cmd.name]
let description = cmd.description
if (key) {
const translated = t(key)
// i18n returns the key itself when no translation is found; fall back to
// the static English description in that case.
if (translated && translated !== key) {
description = translated
}
}
return {
name: cmd.name,
description,
...(cmd.argumentHint && { argumentHint: cmd.argumentHint }),
}
})
}
export type SlashCommandOption = {