feat(i18n): wire initI18n into production plugin startup
6ffea1bc3 added i18n with en/zh locales and plugin config support, but
the initI18n() call lived in the original src/index.ts. When src/index.ts
became an 18-line wrapper that delegates to
src/testing/create-plugin-module.ts createPluginModule(), the call site
was dropped on the floor. Result: i18n.locale config and LANG env both
ignored at runtime, every toast stayed English regardless of user
setting.
Inject initI18n as a managed dependency and call it in
createPluginModule() immediately after loadPluginConfig(), passing
pluginConfig.i18n?.locale through. Add an integration test that boots
the plugin with i18n.locale='zh' and asserts getLocale() returns 'zh'
and t('toast.task_completed') returns the Chinese string. Regression
locked - subsequent moves of the startup path will fail loudly.
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { beforeEach, describe, expect, it, mock } from "bun:test"
|
||||
import { getLocale, initI18n, t } from "../shared/i18n"
|
||||
import { createPluginModule } from "./create-plugin-module"
|
||||
|
||||
const mockInitConfigContext = mock(() => {})
|
||||
const mockDetectExternalSkillPlugin = mock(() => ({ detected: false, pluginName: null, allPlugins: [] }))
|
||||
const mockGetSkillPluginConflictWarning = mock(() => "")
|
||||
const mockInjectServerAuthIntoClient = mock(() => {})
|
||||
const mockLogLegacyPluginStartupWarning = mock(() => {})
|
||||
const mockMigrateLegacyWorkspaceDirectory = mock(() => ({ migrated: false, skipped: [] }))
|
||||
const mockLoadPluginConfig = mock(() => ({}))
|
||||
const mockIsTmuxIntegrationEnabled = mock(
|
||||
(pluginConfig: { tmux?: { enabled?: boolean } | undefined }) => pluginConfig.tmux?.enabled ?? false,
|
||||
)
|
||||
const mockCreateRuntimeTmuxConfig = mock(() => ({
|
||||
enabled: false,
|
||||
layout: "tiled" as const,
|
||||
main_pane_size: 60,
|
||||
main_pane_min_width: 80,
|
||||
agent_pane_min_width: 40,
|
||||
isolation: "inline" as const,
|
||||
}))
|
||||
const mockCreateManagers = mock(() => ({
|
||||
backgroundManager: { shutdown: async () => {} },
|
||||
skillMcpManager: { disconnectAll: async () => {} },
|
||||
configHandler: async () => {},
|
||||
}))
|
||||
const mockCreateTools = mock(async () => ({
|
||||
mergedSkills: [],
|
||||
availableSkills: [],
|
||||
filteredTools: {},
|
||||
}))
|
||||
const mockCreateHooks = mock(() => ({
|
||||
disposeHooks: () => {},
|
||||
compactionContextInjector: undefined,
|
||||
compactionTodoPreserver: undefined,
|
||||
claudeCodeHooks: undefined,
|
||||
}))
|
||||
const mockCreatePluginInterface = mock(() => ({}))
|
||||
const mockInitializeOpenClaw = mock(async () => {})
|
||||
const mockStartTmuxCheck = mock(() => {})
|
||||
const mockInstallAgentSortShim = mock(() => {})
|
||||
const mockSetAgentSortOrder = mock(() => {})
|
||||
const mockLog = mock(() => {})
|
||||
const mockCreateModelCacheState = mock(() => ({}))
|
||||
const mockCreateFirstMessageVariantGate = mock(() => ({
|
||||
shouldOverride: () => false,
|
||||
markApplied: () => {},
|
||||
markSessionCreated: () => {},
|
||||
clear: () => {},
|
||||
}))
|
||||
|
||||
function createTestPluginModule(): ReturnType<typeof createPluginModule> {
|
||||
return createPluginModule({
|
||||
initConfigContext: mockInitConfigContext,
|
||||
detectExternalSkillPlugin: mockDetectExternalSkillPlugin,
|
||||
getSkillPluginConflictWarning: mockGetSkillPluginConflictWarning,
|
||||
injectServerAuthIntoClient: mockInjectServerAuthIntoClient,
|
||||
logLegacyPluginStartupWarning: mockLogLegacyPluginStartupWarning,
|
||||
migrateLegacyWorkspaceDirectory: mockMigrateLegacyWorkspaceDirectory,
|
||||
loadPluginConfig: mockLoadPluginConfig as never,
|
||||
isTmuxIntegrationEnabled: mockIsTmuxIntegrationEnabled as never,
|
||||
createRuntimeTmuxConfig: mockCreateRuntimeTmuxConfig as never,
|
||||
createManagers: mockCreateManagers as never,
|
||||
createTools: mockCreateTools as never,
|
||||
createHooks: mockCreateHooks as never,
|
||||
createPluginInterface: mockCreatePluginInterface as never,
|
||||
initializeOpenClaw: mockInitializeOpenClaw as never,
|
||||
startTmuxCheck: mockStartTmuxCheck,
|
||||
installAgentSortShim: mockInstallAgentSortShim,
|
||||
setAgentSortOrder: mockSetAgentSortOrder,
|
||||
log: mockLog,
|
||||
createModelCacheState: mockCreateModelCacheState as never,
|
||||
createFirstMessageVariantGate: mockCreateFirstMessageVariantGate as never,
|
||||
})
|
||||
}
|
||||
|
||||
describe("createPluginModule()", () => {
|
||||
beforeEach(() => {
|
||||
mockLoadPluginConfig.mockClear()
|
||||
initI18n({ locale: "en", fallback: "en" })
|
||||
})
|
||||
|
||||
describe("#given plugin config sets i18n.locale to zh", () => {
|
||||
it("#then production startup applies the configured locale", async () => {
|
||||
// given
|
||||
const pluginModule = createTestPluginModule()
|
||||
mockLoadPluginConfig.mockReturnValue({
|
||||
i18n: { locale: "zh" },
|
||||
})
|
||||
|
||||
// when
|
||||
await pluginModule.server({
|
||||
directory: "/tmp/project",
|
||||
client: {},
|
||||
} as Parameters<typeof pluginModule.server>[0])
|
||||
|
||||
// then
|
||||
expect(getLocale()).toBe("zh")
|
||||
expect(t("toast.task_completed")).toBe("任务完成")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { installAgentSortShim, setAgentSortOrder } from "../shared/agent-sort-shim"
|
||||
import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "../shared/external-plugin-detector"
|
||||
import { createFirstMessageVariantGate } from "../shared/first-message-variant"
|
||||
import { initI18n } from "../shared/i18n"
|
||||
import { log } from "../shared/logger"
|
||||
import { logLegacyPluginStartupWarning } from "../shared/log-legacy-plugin-startup-warning"
|
||||
import { migrateLegacyWorkspaceDirectory } from "../shared/legacy-workspace-migration"
|
||||
@@ -39,6 +40,7 @@ export type PluginModuleDeps = {
|
||||
getSkillPluginConflictWarning: typeof getSkillPluginConflictWarning
|
||||
injectServerAuthIntoClient: typeof injectServerAuthIntoClient
|
||||
loadPluginConfig: typeof loadPluginConfig
|
||||
initI18n: typeof initI18n
|
||||
initializeOpenClaw: typeof initializeOpenClaw
|
||||
isTmuxIntegrationEnabled: typeof isTmuxIntegrationEnabled
|
||||
startTmuxCheck: typeof startTmuxCheck
|
||||
@@ -62,6 +64,7 @@ const defaultPluginModuleDeps: PluginModuleDeps = {
|
||||
getSkillPluginConflictWarning,
|
||||
injectServerAuthIntoClient,
|
||||
loadPluginConfig,
|
||||
initI18n,
|
||||
initializeOpenClaw,
|
||||
isTmuxIntegrationEnabled,
|
||||
startTmuxCheck,
|
||||
@@ -93,6 +96,7 @@ export function createPluginModule(overrides: Partial<PluginModuleDeps> = {}): P
|
||||
deps.injectServerAuthIntoClient(input.client)
|
||||
|
||||
const pluginConfig = deps.loadPluginConfig(input.directory, input)
|
||||
deps.initI18n(pluginConfig.i18n?.locale ? { locale: pluginConfig.i18n.locale } : undefined)
|
||||
deps.setAgentSortOrder(pluginConfig.agent_order)
|
||||
|
||||
if (pluginConfig.openclaw) {
|
||||
|
||||
Reference in New Issue
Block a user