mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 10:14:19 -05:00
* feat: add Vietnamese i18n support for slash command descriptions
Add a simple i18n helper that reads the `language` setting from config
to display localized skill descriptions. Currently supports English
(default) and Vietnamese.
To switch to Vietnamese, set in ~/.claude/settings.json:
{ "language": "vietnamese" }
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* feat(i18n): add Vietnamese translations for all 85 command descriptions
- Fix detectLocale() to read ~/.claude/settings.json directly via
readFileSync instead of broken require('../../utils/config.js')
- Add commandDescVi translation map with 85 Vietnamese descriptions
- Export translateCommandDescription() for use in command rendering
- Modify formatDescriptionWithSource() to translate descriptions
when language is set to "vietnamese"
- Bump version to 0.15.1
* fix: add prepare script for git-based installs
When installing via `npm install -g git+https://...`, npm runs the
`prepare` script automatically. This ensures the CLI is built from
source during installation.
Requires Bun to be installed globally.
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(i18n): read locale from merged settings
* feat(i18n): translate all prompt-type commands + add env validation + node version files
## Changes
### 1. Fix prompt-type command translations (src/commands.ts)
- `formatDescriptionWithSource()` now calls `translateCommandDescription()` for ALL command types
- Previously only translated `builtin`/`mcp` source commands
- Now translates: workflow, plugin, bundled, and default cases
- Fixes: /review, /insights, and other prompt-type commands now display Vietnamese
### 2. Add missing Vietnamese translations (src/skills/bundled/i18n.ts)
Added 17 new command translations:
- /btw: "Đặt câu hỏi nhanh bên lề mà không làm gián đoạn cuộc hội thoại chính"
- /compact: "Xóa lịch sử hội thoại nhưng giữ tóm tắt trong ngữ cảnh"
- /auto-fix: "Cấu hình tự động sửa: chạy lint/test sau khi AI chỉnh sửa"
- /bridge-kick: "Chèn trạng thái lỗi bridge để kiểm thử khôi phục thủ công"
- /review: "Hoàn thành đánh giá bảo mật cho các thay đổi đang chờ trên nhánh hiện tại"
- +12 more commands
### 3. Add Zod env validation at startup (src/utils/envValidation.ts)
- New file: validates critical env vars using Zod at startup
- Crashes immediately if invalid (instead of wasting time)
- Validated vars: ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_CONFIG_DIR, HTTP_PROXY, HTTPS_PROXY, NODE_EXTRA_CA_CERTS
- Integrated into src/entrypoints/init.ts
### 4. Add node version files
- .nvmrc: Node 22
- .node-version: Node 22
- Matches Dockerfile (node:22-slim) and package.json engines (>=22.0.0)
## Test Results
- 3007 pass, 11 fail (all in changeDetector.test.ts - pre-existing, unrelated to i18n)
Co-Authored-By: OpenClaude <noreply@openclaude.ai>
* fix: restore validateBoundedIntEnvVar in envValidation.ts
* Localize bundled skills descriptions at read time
* fix(i18n): localize slash command suggestions
Search rendered localized command descriptions and rebuild the Fuse index when language-sensitive text changes.
Preserve Unicode letters and numbers for Vietnamese slash queries, localize the remaining requested command descriptions, and keep exact slash command submission from following a stale highlighted suggestion.
Tests: bun test src/commands.test.ts; bun test src/utils/suggestions/commandSuggestions.test.ts; bun test src/utils/envValidation.test.ts
Thanks to @jatmn for the patient review and guidance.
* fix(i18n): tighten slash command localization scope
* fix(i18n): centralize localization and preserve external metadata
* fix(commands): scope localized descriptions to OpenClaude-owned commands
* fix(i18n): read session language before initial settings
* fix(i18n): prefer whenToUse localization keys
---------
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
Co-authored-by: OpenClaude <noreply@openclaude.ai>
Co-authored-by: lht3003-rgb <lht3003-rgb@users.noreply.github.com>
322 lines
11 KiB
TypeScript
322 lines
11 KiB
TypeScript
import { afterEach, describe, expect, test } from 'bun:test'
|
||
import {
|
||
builtInCommandNames,
|
||
formatDescriptionWithSource,
|
||
} from './commands.js'
|
||
import { registerBatchSkill } from './skills/bundled/batch.js'
|
||
import { registerDebugSkill } from './skills/bundled/debug.js'
|
||
import { registerLoopSkill } from './skills/bundled/loop.js'
|
||
import { registerSimplifySkill } from './skills/bundled/simplify.js'
|
||
import { registerUpdateConfigSkill } from './skills/bundled/updateConfig.js'
|
||
import {
|
||
clearBundledSkills,
|
||
getBundledSkills,
|
||
registerBundledSkill,
|
||
} from './skills/bundledSkills.js'
|
||
import { isCommand } from './types/command.js'
|
||
import {
|
||
resetSettingsCache,
|
||
setSessionSettingsCache,
|
||
} from './utils/settings/settingsCache.js'
|
||
|
||
function useLanguage(language?: string): void {
|
||
setSessionSettingsCache({
|
||
settings: language ? { language } : {},
|
||
errors: [],
|
||
})
|
||
}
|
||
|
||
afterEach(() => {
|
||
resetSettingsCache()
|
||
clearBundledSkills()
|
||
})
|
||
|
||
describe('builtInCommandNames', () => {
|
||
test('includes the LSP command', () => {
|
||
expect(builtInCommandNames()).toContain('lsp')
|
||
})
|
||
|
||
test('includes the request-size diagnostic command', () => {
|
||
expect(builtInCommandNames()).toContain('request-size')
|
||
})
|
||
|
||
test('includes the /dream command', () => {
|
||
expect(builtInCommandNames()).toContain('dream')
|
||
})
|
||
})
|
||
|
||
describe('isCommand', () => {
|
||
test('rejects generated missing-module noop stubs', () => {
|
||
function noop19() {
|
||
return null
|
||
}
|
||
|
||
expect(isCommand(noop19)).toBe(false)
|
||
expect(isCommand({ isHidden: true, name: 'stub' })).toBe(false)
|
||
})
|
||
|
||
test('accepts real command objects', () => {
|
||
expect(
|
||
isCommand({
|
||
type: 'local',
|
||
name: 'example',
|
||
description: 'example command',
|
||
supportsNonInteractive: false,
|
||
load: async () => ({
|
||
call: async () => ({ type: 'skip' }),
|
||
}),
|
||
}),
|
||
).toBe(true)
|
||
})
|
||
})
|
||
|
||
describe('formatDescriptionWithSource', () => {
|
||
test('returns empty text for prompt commands missing a description', () => {
|
||
const command = {
|
||
name: 'example',
|
||
type: 'prompt',
|
||
source: 'builtin',
|
||
description: undefined,
|
||
} as any
|
||
|
||
expect(formatDescriptionWithSource(command)).toBe('')
|
||
})
|
||
|
||
test('formats plugin commands with missing description safely', () => {
|
||
const command = {
|
||
name: 'example',
|
||
type: 'prompt',
|
||
source: 'plugin',
|
||
description: undefined,
|
||
pluginInfo: {
|
||
pluginManifest: {
|
||
name: 'MyPlugin',
|
||
},
|
||
},
|
||
} as any
|
||
|
||
expect(formatDescriptionWithSource(command)).toBe('(MyPlugin) ')
|
||
})
|
||
|
||
test('translates prompt built-in descriptions using the current language', () => {
|
||
const command = {
|
||
name: 'review',
|
||
type: 'prompt',
|
||
source: 'builtin',
|
||
description: 'Review a pull request',
|
||
localizationKey: 'commands.review.description',
|
||
} as any
|
||
|
||
useLanguage('english')
|
||
expect(formatDescriptionWithSource(command)).toBe('Review a pull request')
|
||
|
||
useLanguage('vietnamese')
|
||
expect(formatDescriptionWithSource(command)).toBe('Đánh giá pull request')
|
||
})
|
||
|
||
test('falls back to English when an OpenClaude localization key is missing', () => {
|
||
const command = {
|
||
name: 'example',
|
||
type: 'prompt',
|
||
source: 'builtin',
|
||
description: 'English fallback description',
|
||
localizationKey: 'commands.example.missing.description',
|
||
} as any
|
||
|
||
useLanguage('vietnamese')
|
||
expect(formatDescriptionWithSource(command)).toBe(
|
||
'English fallback description',
|
||
)
|
||
})
|
||
|
||
test('does not translate project, policy, workflow, or user-authored descriptions', () => {
|
||
const description = 'Review a pull request'
|
||
const promptCommand = (source: string) =>
|
||
({
|
||
name: 'external-review',
|
||
type: 'prompt',
|
||
source,
|
||
description,
|
||
}) as any
|
||
|
||
useLanguage('vietnamese')
|
||
|
||
expect(formatDescriptionWithSource(promptCommand('projectSettings'))).toBe(
|
||
'Review a pull request (project)',
|
||
)
|
||
expect(formatDescriptionWithSource(promptCommand('userSettings'))).toBe(
|
||
'Review a pull request (user)',
|
||
)
|
||
expect(formatDescriptionWithSource(promptCommand('policySettings'))).toBe(
|
||
'Review a pull request (managed)',
|
||
)
|
||
expect(formatDescriptionWithSource(promptCommand('localSettings'))).toBe(
|
||
'Review a pull request (project, gitignored)',
|
||
)
|
||
expect(formatDescriptionWithSource(promptCommand('flagSettings'))).toBe(
|
||
'Review a pull request (cli flag)',
|
||
)
|
||
expect(
|
||
formatDescriptionWithSource({
|
||
...promptCommand('projectSettings'),
|
||
kind: 'workflow',
|
||
}),
|
||
).toBe('Review a pull request (workflow)')
|
||
})
|
||
|
||
test('does not translate plugin descriptions that match built-in English text', () => {
|
||
const command = {
|
||
name: 'external-review',
|
||
type: 'prompt',
|
||
source: 'plugin',
|
||
description: 'Review a pull request',
|
||
pluginInfo: {
|
||
pluginManifest: {
|
||
name: 'MyPlugin',
|
||
},
|
||
},
|
||
} as any
|
||
|
||
useLanguage('vietnamese')
|
||
|
||
expect(formatDescriptionWithSource(command)).toBe(
|
||
'(MyPlugin) Review a pull request',
|
||
)
|
||
})
|
||
|
||
test('does not translate non-prompt local descriptions without a localization key', () => {
|
||
const command = {
|
||
name: 'external-review',
|
||
type: 'local',
|
||
description: 'Review a pull request',
|
||
} as any
|
||
|
||
useLanguage('vietnamese')
|
||
|
||
expect(formatDescriptionWithSource(command)).toBe('Review a pull request')
|
||
})
|
||
|
||
test('translates non-prompt local descriptions only with an explicit localization key', () => {
|
||
const command = {
|
||
name: 'copy',
|
||
type: 'local',
|
||
description:
|
||
"Copy Claude's last response to clipboard (or /copy N for the Nth-latest)",
|
||
localizationKey: 'commands.copy.description',
|
||
} as any
|
||
|
||
useLanguage('vietnamese')
|
||
expect(formatDescriptionWithSource(command)).toBe(
|
||
'Sao chép phản hồi gần nhất của Claude vào clipboard (hoặc /copy N cho phản hồi thứ N gần nhất)',
|
||
)
|
||
|
||
useLanguage('english')
|
||
expect(formatDescriptionWithSource(command)).toBe(
|
||
"Copy Claude's last response to clipboard (or /copy N for the Nth-latest)",
|
||
)
|
||
})
|
||
})
|
||
|
||
describe('bundled skill localization', () => {
|
||
test('resolves descriptions from the current language at read time', () => {
|
||
resetSettingsCache()
|
||
clearBundledSkills()
|
||
registerBatchSkill()
|
||
registerDebugSkill()
|
||
registerLoopSkill()
|
||
registerSimplifySkill()
|
||
registerUpdateConfigSkill()
|
||
const batch = getBundledSkills().find(command => command.name === 'batch')
|
||
const debug = getBundledSkills().find(command => command.name === 'debug')
|
||
const loop = getBundledSkills().find(command => command.name === 'loop')
|
||
const simplify = getBundledSkills().find(
|
||
command => command.name === 'simplify',
|
||
)
|
||
const updateConfig = getBundledSkills().find(
|
||
command => command.name === 'update-config',
|
||
)
|
||
const expectedDebugEnglish =
|
||
process.env.USER_TYPE === 'ant'
|
||
? 'Debug your current Claude Code session by reading the session debug log. Includes all event logging'
|
||
: 'Enable debug logging for this session and help diagnose issues'
|
||
const expectedDebugVietnamese =
|
||
process.env.USER_TYPE === 'ant'
|
||
? 'Debug phiên Claude Code hiện tại bằng cách đọc debug log của phiên. Bao gồm toàn bộ event logging'
|
||
: 'Bật debug logging cho phiên này và hỗ trợ chẩn đoán sự cố'
|
||
|
||
expect(batch).toBeDefined()
|
||
expect(debug).toBeDefined()
|
||
expect(loop).toBeDefined()
|
||
expect(simplify).toBeDefined()
|
||
expect(updateConfig).toBeDefined()
|
||
expect(batch!.localizationKey).toBe('skills.batch.description')
|
||
expect(loop!.localizationKey).toBe('skills.loop.description')
|
||
expect(loop!.whenToUseLocalizationKey).toBe('skills.loop.whenToUse')
|
||
|
||
useLanguage('english')
|
||
expect(batch!.description).toBe(
|
||
'Research and plan a large-scale change, then execute it in parallel across 5–30 isolated worktree agents that each open a PR.',
|
||
)
|
||
expect(debug!.description).toBe(expectedDebugEnglish)
|
||
expect(loop!.description).toBe(
|
||
'Run a prompt on a fixed interval or dynamically reschedule it, including bare maintenance-mode loops.',
|
||
)
|
||
expect(loop!.whenToUse).toBe(
|
||
'When the user wants to poll for status, babysit a workflow, run recurring maintenance, or keep re-running a prompt within the current session.',
|
||
)
|
||
expect(simplify!.description).toBe(
|
||
'Review changed code for reuse, quality, and efficiency, then fix any issues found.',
|
||
)
|
||
expect(updateConfig!.description).toStartWith(
|
||
'Use this skill to configure the Claude Code harness via settings.json.',
|
||
)
|
||
|
||
useLanguage('vietnamese')
|
||
expect(batch!.description).toBe(
|
||
'Nghiên cứu và lập kế hoạch cho một thay đổi quy mô lớn, rồi thực thi song song trên 5–30 agent worktree cô lập, mỗi agent mở một PR.',
|
||
)
|
||
expect(debug!.description).toBe(expectedDebugVietnamese)
|
||
expect(loop!.description).toBe(
|
||
'Chạy một prompt theo khoảng thời gian cố định hoặc lên lịch lại động, bao gồm cả chế độ bảo trì lặp lại.',
|
||
)
|
||
expect(loop!.whenToUse).toBe(
|
||
'Khi người dùng muốn kiểm tra trạng thái, giám sát quy trình, chạy bảo trì định kỳ, hoặc chạy lại một prompt trong phiên hiện tại.',
|
||
)
|
||
expect(simplify!.description).toBe(
|
||
'Đánh giá code đã thay đổi về mặt tái sử dụng, chất lượng và hiệu suất, sau đó sửa các vấn đề tìm được.',
|
||
)
|
||
expect(updateConfig!.description).toStartWith(
|
||
'Sử dụng skill này để cấu hình Claude Code qua settings.json.',
|
||
)
|
||
|
||
useLanguage('english')
|
||
expect(loop!.description).toBe(
|
||
'Run a prompt on a fixed interval or dynamically reschedule it, including bare maintenance-mode loops.',
|
||
)
|
||
expect(updateConfig!.description).toStartWith(
|
||
'Use this skill to configure the Claude Code harness via settings.json.',
|
||
)
|
||
})
|
||
|
||
test('falls back to bundled skill English text when a localization key is missing', () => {
|
||
registerBundledSkill({
|
||
name: 'fallback-skill',
|
||
description: 'English-only bundled skill description',
|
||
descriptionKey: 'skills.fallback-skill.missing.description',
|
||
getPromptForCommand: async () => [],
|
||
})
|
||
|
||
const skill = getBundledSkills().find(
|
||
command => command.name === 'fallback-skill',
|
||
)
|
||
|
||
expect(skill).toBeDefined()
|
||
|
||
useLanguage('vietnamese')
|
||
expect(skill!.description).toBe('English-only bundled skill description')
|
||
expect(formatDescriptionWithSource(skill!)).toBe(
|
||
'English-only bundled skill description (bundled)',
|
||
)
|
||
})
|
||
})
|