Complete client UI i18n migration

Adds client-side i18n support, language settings, translated UI copy, and locale parity coverage.
This commit is contained in:
chyendongnhanh338
2026-06-11 14:15:35 +08:00
committed by GitHub
parent 41cf070fd8
commit 08ebe5e7e1
199 changed files with 16426 additions and 1821 deletions
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import type { AgentModelDefinition, ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
import type { AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
import { i18n } from "@/i18n/i18next";
import {
buildProviderSelectorProviders,
buildSelectableProviderSelectorProviders,
@@ -318,4 +319,54 @@ describe("combined model selector data", () => {
}),
).toEqual({ ok: true });
});
it("uses the active app language for utility labels", async () => {
await i18n.changeLanguage("zh-CN");
try {
const providers = buildSelectableProviderSelectorProviders([
snapshotEntry({
provider: "deepseek-tui",
label: "DeepSeek TUI",
models: [],
}),
snapshotEntry({
provider: "unavailable-provider",
status: "unavailable",
models: [],
}),
]);
expect(getAllModelLabels(providers)).toContain("默认");
expect(providers[1]?.modelSelection).toEqual({
kind: "error",
message: "不可用",
});
expect(
resolveSubmissionReadiness({
text: "",
allowsEmptyAutoSubmit: false,
providerCount: 1,
selection: {
provider: "codex",
modelId: "gpt-5.4",
availableModels: [codexModel],
isModelLoading: false,
},
autoSubmitConfig: null,
workspaceDirectory: "/repo",
hasClient: true,
}),
).toEqual({ ok: false, reason: "初始 prompt 必填" });
} finally {
await i18n.changeLanguage("en");
}
});
});
function getAllModelLabels(providers: ReturnType<typeof buildSelectableProviderSelectorProviders>) {
return providers.flatMap((provider) =>
provider.modelSelection.kind === "models"
? provider.modelSelection.rows.map((row) => row.modelLabel)
: [],
);
}
@@ -7,6 +7,7 @@ import type {
import type { AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
import type { DraftCommandConfig } from "@/hooks/use-agent-commands-query";
import { buildFavoriteModelKey, type FavoriteModelRow } from "@/hooks/use-form-preferences";
import { i18n } from "@/i18n/i18next";
import { compareMatchScores, scoreTextFields } from "@/utils/score-match";
export type ProviderSelectionModelRow = FavoriteModelRow & { isDefault?: boolean };
@@ -61,7 +62,7 @@ function buildSyntheticDefaultRow(
provider,
providerLabel,
modelId: "",
modelLabel: "Default",
modelLabel: i18n.t("providerSelection.defaultModel"),
description: undefined,
isDefault: true,
};
@@ -96,7 +97,11 @@ function buildEntryModelSelection(
}
return {
kind: "error",
message: entry.error ?? (entry.status === "unavailable" ? "Unavailable" : "Unknown error"),
message:
entry.error ??
(entry.status === "unavailable"
? i18n.t("providerSelection.unavailable")
: i18n.t("providerSelection.unknownError")),
};
}
@@ -152,21 +157,23 @@ export function resolveSelectedModelLabel(input: {
}): string {
const selectedProvider = input.selectedProvider.trim();
if (!selectedProvider) {
return "Select model";
return i18n.t("providerSelection.selectModel");
}
const provider = input.providers.find((entry) => entry.id === selectedProvider);
if (!provider) {
return input.isLoading ? "Loading..." : "Select model";
return input.isLoading
? i18n.t("providerSelection.loading")
: i18n.t("providerSelection.selectModel");
}
if (provider.modelSelection.kind === "loading") {
return "Loading...";
return i18n.t("providerSelection.loading");
}
if (provider.modelSelection.kind === "error") {
return "Error";
return i18n.t("providerSelection.error");
}
if (provider.modelSelection.kind !== "models") {
return "Select model";
return i18n.t("providerSelection.selectModel");
}
const model = provider.modelSelection.rows.find((entry) => entry.modelId === input.selectedModel);
@@ -175,7 +182,7 @@ export function resolveSelectedModelLabel(input: {
model?.modelLabel ??
defaultModel?.modelLabel ??
provider.modelSelection.rows[0]?.modelLabel ??
"Select model"
i18n.t("providerSelection.selectModel")
);
}
@@ -280,26 +287,26 @@ export function resolveSubmissionReadiness(input: {
hasClient: boolean;
}): ProviderSelectionReadiness {
if (!input.allowsEmptyAutoSubmit && !input.text.trim()) {
return { ok: false, reason: "Initial prompt is required" };
return { ok: false, reason: i18n.t("providerSelection.readiness.initialPromptRequired") };
}
if (input.providerCount === 0) {
return { ok: false, reason: "No available providers on the selected host" };
return { ok: false, reason: i18n.t("providerSelection.readiness.noProviders") };
}
if (!(input.autoSubmitConfig?.provider ?? input.selection.provider)) {
return { ok: false, reason: "Select a model" };
return { ok: false, reason: i18n.t("providerSelection.selectModel") };
}
if (input.selection.isModelLoading) {
return { ok: false, reason: "Model defaults are still loading" };
return { ok: false, reason: i18n.t("providerSelection.readiness.modelDefaultsLoading") };
}
const hasSelectedModel = Boolean(input.autoSubmitConfig?.model ?? input.selection.modelId);
if (!hasSelectedModel && input.selection.availableModels.length > 0) {
return { ok: false, reason: "No model is available for the selected provider" };
return { ok: false, reason: i18n.t("providerSelection.readiness.noModelAvailable") };
}
if (!input.workspaceDirectory) {
return { ok: false, reason: "Workspace directory not found" };
return { ok: false, reason: i18n.t("providerSelection.readiness.workspaceDirectoryNotFound") };
}
if (!input.hasClient) {
return { ok: false, reason: "Host is not connected" };
return { ok: false, reason: i18n.t("providerSelection.readiness.hostDisconnected") };
}
return { ok: true };
}