feat(i18n): extract en translations — context/lib/misc (#1251)

* feat(i18n): extract en translations for context/lib/misc

Extract hardcoded English strings to i18n keys for:
- automations context, providers store, shared-bundles
- mcp-auth-modal, onboarding-workspace-selector, question-modal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(i18n): second pass — extract remaining hardcoded strings

Fix missed strings not covered by reference diff:
- automations: schedule_required, prompt_required, prompt_empty,
  server_unavailable in deleteScheduledJob, failed_to_load in local block,
  provider_id_required (2 missed instances in store.ts)
- mcp-auth-modal: request_timed_out
- question-modal: Submit/Next button labels (common.submit, common.next)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(i18n): drop stale rebase artifacts

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: src-opn <src-opn@users.noreply.github.com>
This commit is contained in:
Johnny Shields
2026-04-04 02:25:19 +09:00
committed by GitHub
parent f25911ec5e
commit c349413f09
5 changed files with 114 additions and 59 deletions

View File

@@ -168,7 +168,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
statusPoll = window.setInterval(async () => {
if (Date.now() - startedAt >= MCP_AUTH_TIMEOUT_MS) {
stopStatusPolling();
setError("Request timed out.");
setError(translate("mcp.auth.request_timed_out"));
return;
}
@@ -694,7 +694,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
<CheckCircle2 size={24} class="text-green-11" />
</div>
<div>
<p class="text-sm font-medium text-gray-12">Already Connected</p>
<p class="text-sm font-medium text-gray-12">{translate("mcp.auth.already_connected")}</p>
<p class="text-xs text-gray-11">
{translate("mcp.auth.already_connected_description", { server: serverName() })}
</p>
@@ -804,7 +804,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
</div>
<div class="rounded-xl border border-gray-6/70 bg-gray-2/40 px-3 py-2 flex items-center gap-3">
<div class="flex-1 min-w-0">
<div class="text-[10px] uppercase tracking-wide text-gray-8">Authorization link</div>
<div class="text-[10px] uppercase tracking-wide text-gray-8">{translate("mcp.auth.authorization_link")}</div>
<div class="text-[11px] text-gray-11 font-mono truncate">
{authorizationUrl()}
</div>
@@ -814,7 +814,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
class="text-xs"
onClick={handleCopyAuthorizationUrl}
>
{authUrlCopied() ? "Copied" : "Copy link"}
{authUrlCopied() ? translate("mcp.auth.copied") : translate("mcp.auth.copy_link")}
</Button>
</div>
<TextInput
@@ -851,7 +851,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
1
</div>
<div>
<p class="text-sm font-medium text-gray-12">Opening your browser</p>
<p class="text-sm font-medium text-gray-12">{translate("mcp.auth.step1_title")}</p>
<p class="text-xs text-gray-10 mt-1">
{translate("mcp.auth.step1_description", { server: serverName() })}
</p>
@@ -863,7 +863,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
2
</div>
<div>
<p class="text-sm font-medium text-gray-12">Authorize OpenWork</p>
<p class="text-sm font-medium text-gray-12">{translate("mcp.auth.step2_title")}</p>
<p class="text-xs text-gray-10 mt-1">
{translate("mcp.auth.step2_description")}
</p>
@@ -875,7 +875,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
3
</div>
<div>
<p class="text-sm font-medium text-gray-12">Return here when you're done</p>
<p class="text-sm font-medium text-gray-12">{translate("mcp.auth.step3_title")}</p>
<p class="text-xs text-gray-10 mt-1">
{translate("mcp.auth.step3_description")}
</p>

View File

@@ -4,6 +4,7 @@ import type { QuestionInfo } from "@opencode-ai/sdk/v2/client";
import { Check, ChevronRight, HelpCircle } from "lucide-solid";
import Button from "./button";
import { t } from "../../i18n";
export type QuestionModalProps = {
open: boolean;
@@ -138,10 +139,10 @@ export default function QuestionModal(props: QuestionModalProps) {
</div>
<div>
<h3 class="text-lg font-semibold text-gray-12">
{currentQuestion()!.header || "Question"}
{currentQuestion()!.header || t("common.question")}
</h3>
<div class="text-xs text-gray-11 font-medium">
Question {currentIndex() + 1} of {props.questions.length}
{t("question_modal.question_counter", undefined, { current: currentIndex() + 1, total: props.questions.length })}
</div>
</div>
</div>
@@ -186,14 +187,14 @@ export default function QuestionModal(props: QuestionModalProps) {
<Show when={currentQuestion()!.custom}>
<div class="mt-4 pt-4 border-t border-dls-border">
<label class="block text-xs font-semibold text-dls-secondary mb-2 uppercase tracking-wide">
Or type a custom answer
{t("question_modal.custom_answer_label")}
</label>
<input
type="text"
value={customInput()}
onInput={(e) => setCustomInput(e.currentTarget.value)}
class="w-full px-4 py-3 rounded-xl bg-dls-surface border border-dls-border focus:border-dls-accent focus:ring-4 focus:ring-[rgba(var(--dls-accent-rgb),0.2)] focus:outline-none text-sm text-dls-text placeholder:text-dls-secondary transition-shadow"
placeholder="Type your answer here..."
placeholder={t("question_modal.custom_answer_placeholder")}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (e.isComposing || e.keyCode === 229) return;
@@ -209,15 +210,15 @@ export default function QuestionModal(props: QuestionModalProps) {
<div class="p-6 border-t border-dls-border bg-dls-hover flex justify-between items-center">
<div class="text-xs text-dls-secondary flex items-center gap-2">
<span class="px-1.5 py-0.5 rounded border border-dls-border bg-dls-active font-mono"></span>
<span>navigate</span>
<span>{t("common.navigate")}</span>
<span class="px-1.5 py-0.5 rounded border border-gray-6 bg-gray-3 font-mono ml-2"></span>
<span>select</span>
<span>{t("common.select")}</span>
</div>
<div class="flex gap-2">
<Show when={currentQuestion()?.multiple || currentQuestion()?.custom}>
<Button onClick={handleNext} disabled={!canProceed() || props.busy} class="!px-6">
{isLastQuestion() ? "Submit" : "Next"}
{isLastQuestion() ? t("common.submit") : t("common.next")}
<Show when={!isLastQuestion()}>
<ChevronRight size={16} class="ml-1 -mr-1 opacity-60" />
</Show>

View File

@@ -5,6 +5,7 @@ import { schedulerDeleteJob, schedulerListJobs } from "../lib/tauri";
import { isTauriRuntime } from "../utils";
import { createWorkspaceContextKey } from "./workspace-context";
import type { OpenworkServerStore } from "../connections/openwork-server-store";
import { t } from "../../i18n";
export type AutomationsStore = ReturnType<typeof createAutomationsStore>;
@@ -33,10 +34,10 @@ const buildCreateAutomationPrompt = (
const schedule = input.schedule.trim();
const prompt = normalizeSentence(input.prompt);
if (!schedule) {
return { ok: false, error: "Schedule is required." };
return { ok: false, error: t("automations.schedule_required") };
}
if (!prompt) {
return { ok: false, error: "Prompt is required." };
return { ok: false, error: t("automations.prompt_required") };
}
const workdir = (input.workdir ?? "").trim();
const nameSegment = name ? ` named \"${name}\"` : "";
@@ -58,7 +59,7 @@ const buildRunAutomationPrompt = (
if (job.run?.prompt || job.prompt) {
const promptBody = (job.run?.prompt ?? job.prompt ?? "").trim();
if (!promptBody) {
return { ok: false, error: "Automation prompt is empty." };
return { ok: false, error: t("automations.prompt_empty") };
}
return {
ok: true,
@@ -136,10 +137,10 @@ export function createAutomationsStore(options: {
if (scheduledJobsContextKey() !== requestContextKey) return "skipped";
const status =
options.openworkServer.openworkServerStatus() === "disconnected"
? "OpenWork server unavailable. Connect to sync scheduled tasks."
? t("automations.server_unavailable")
: options.openworkServer.openworkServerStatus() === "limited"
? "OpenWork server needs a token to load scheduled tasks."
: "OpenWork server not ready.";
? t("automations.server_needs_token")
: t("automations.server_not_ready");
setScheduledJobsStatus(status);
return "unavailable";
}
@@ -155,7 +156,7 @@ export function createAutomationsStore(options: {
} catch (error) {
if (scheduledJobsContextKey() !== requestContextKey) return "skipped";
const message = error instanceof Error ? error.message : String(error);
setScheduledJobsStatus(message || "Failed to load scheduled tasks.");
setScheduledJobsStatus(message || t("automations.failed_to_load"));
return "error";
} finally {
setScheduledJobsBusy(false);
@@ -180,7 +181,7 @@ export function createAutomationsStore(options: {
} catch (error) {
if (scheduledJobsContextKey() !== requestContextKey) return "skipped";
const message = error instanceof Error ? error.message : String(error);
setScheduledJobsStatus(message || "Failed to load scheduled tasks.");
setScheduledJobsStatus(message || t("automations.failed_to_load"));
return "error";
} finally {
setScheduledJobsBusy(false);
@@ -192,7 +193,7 @@ export function createAutomationsStore(options: {
const client = options.openworkServer.openworkServerClient();
const workspaceId = (options.runtimeWorkspaceId() ?? "").trim();
if (!client || !workspaceId) {
throw new Error("OpenWork server unavailable. Connect to sync scheduled tasks.");
throw new Error(t("automations.server_unavailable"));
}
const response = await client.deleteScheduledJob(workspaceId, name);
setScheduledJobs((current) => current.filter((entry) => entry.slug !== response.job.slug));
@@ -200,7 +201,7 @@ export function createAutomationsStore(options: {
}
if (!isTauriRuntime()) {
throw new Error("Scheduled tasks require the desktop app.");
throw new Error(t("automations.desktop_required"));
}
const root = options.selectedWorkspaceRoot().trim();
const job = await schedulerDeleteJob(name, root || undefined);

View File

@@ -2,7 +2,7 @@ import { createMemo, createSignal, type Accessor } from "solid-js";
import type { ProviderAuthAuthorization, ProviderListResponse } from "@opencode-ai/sdk/v2/client";
import { t, currentLocale } from "../../../i18n";
import { t } from "../../../i18n";
import { unwrap, waitForHealthy } from "../../lib/opencode";
import type { Client, ProviderListItem, WorkspaceDisplay } from "../../types";
import { safeStringify } from "../../utils";
@@ -70,7 +70,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
const assertNoClientError = (result: unknown) => {
const maybe = result as { error?: unknown } | null | undefined;
if (!maybe || maybe.error === undefined) return;
throw new Error(describeProviderError(maybe.error, t("app.error_request_failed", currentLocale())));
throw new Error(describeProviderError(maybe.error, t("providers.request_failed")));
};
const describeProviderError = (error: unknown, fallback: string) => {
@@ -125,9 +125,9 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
const generic = raw && /^unknown\s+error$/i.test(raw);
const heading = (() => {
if (status === 401 || status === 403) return t("app.error_auth_failed", currentLocale());
if (status === 429) return t("app.error_rate_limit", currentLocale());
if (provider) return `Provider error (${provider})`;
if (status === 401 || status === 403) return t("providers.auth_failed");
if (status === 429) return t("providers.rate_limit_exceeded");
if (provider) return t("providers.provider_error", undefined, { provider });
return fallback;
})();
@@ -167,7 +167,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
if (!Array.isArray(provider.env) || provider.env.length === 0) continue;
const existing = merged[id] ?? [];
if (existing.some((method) => method.type === "api")) continue;
merged[id] = [...existing, { type: "api", label: "API key" }];
merged[id] = [...existing, { type: "api", label: t("providers.api_key_label") }];
}
for (const [id, providerMethods] of Object.entries(merged)) {
const provider = availableProviders.find((item) => item.id === id);
@@ -188,7 +188,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
const loadProviderAuthMethods = async (workerType: "local" | "remote") => {
const c = options.client();
if (!c) {
throw new Error(t("app.error_not_connected", currentLocale()));
throw new Error(t("providers.not_connected"));
}
const methods = unwrap(await c.provider.auth());
return buildProviderAuthMethods(
@@ -205,7 +205,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
setProviderAuthError(null);
const c = options.client();
if (!c) {
throw new Error(t("app.error_not_connected", currentLocale()));
throw new Error(t("providers.not_connected"));
}
try {
const cachedMethods = providerAuthMethods();
@@ -214,17 +214,17 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
: await loadProviderAuthMethods(providerAuthWorkerType());
const providerIds = Object.keys(authMethods).sort();
if (!providerIds.length) {
throw new Error("No providers available");
throw new Error(t("providers.no_providers_available"));
}
const resolved = providerId?.trim() ?? "";
if (!resolved) {
throw new Error("Provider ID is required");
throw new Error(t("providers.provider_id_required"));
}
const methods = authMethods[resolved];
if (!methods || !methods.length) {
throw new Error(`Unknown provider: ${resolved}`);
throw new Error(`${t("providers.unknown_provider")}: ${resolved}`);
}
const oauthIndex =
@@ -232,12 +232,12 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
? methodIndex
: methods.find((method) => method.type === "oauth")?.methodIndex ?? -1;
if (oauthIndex === -1) {
throw new Error(`No OAuth flow available for ${resolved}. Use an API key instead.`);
throw new Error(`${t("providers.no_oauth_prefix")} ${resolved}. ${t("providers.use_api_key_suffix")}`);
}
const selectedMethod = methods.find((method) => method.methodIndex === oauthIndex);
if (!selectedMethod || selectedMethod.type !== "oauth") {
throw new Error(`Selected auth method is not an OAuth flow for ${resolved}.`);
throw new Error(`${t("providers.not_oauth_flow_prefix")} ${resolved}.`);
}
const auth = unwrap(await c.provider.oauth.authorize({ providerID: resolved, method: oauthIndex }));
@@ -246,7 +246,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
authorization: auth,
};
} catch (error) {
const message = describeProviderError(error, "Failed to connect provider");
const message = describeProviderError(error, t("providers.connect_failed"));
setProviderAuthError(message);
throw error instanceof Error ? error : new Error(message);
}
@@ -310,16 +310,16 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
setProviderAuthError(null);
const c = options.client();
if (!c) {
throw new Error(t("app.error_not_connected", currentLocale()));
throw new Error(t("providers.not_connected"));
}
const resolved = providerId?.trim();
if (!resolved) {
throw new Error("Provider ID is required");
throw new Error(t("providers.provider_id_required"));
}
if (!Number.isInteger(methodIndex) || methodIndex < 0) {
throw new Error("OAuth method is required");
throw new Error(t("providers.oauth_method_required"));
}
const waitForProviderConnection = async (timeoutMs = 15_000, pollMs = 2_000) => {
@@ -354,26 +354,26 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
const updated = await refreshProviders({ dispose: true });
const connectedNow = Array.isArray(updated?.connected) && updated.connected.includes(resolved);
if (connectedNow) {
return { connected: true, message: `Connected ${resolved}` };
return { connected: true, message: `${t("status.connected")} ${resolved}` };
}
const connected = await waitForProviderConnection();
if (connected) {
return { connected: true, message: `Connected ${resolved}` };
return { connected: true, message: `${t("status.connected")} ${resolved}` };
}
return { connected: false, pending: true };
} catch (error) {
if (isPendingOauthError(error)) {
const updated = await refreshProviders({ dispose: true });
if (Array.isArray(updated?.connected) && updated.connected.includes(resolved)) {
return { connected: true, message: `Connected ${resolved}` };
return { connected: true, message: `${t("status.connected")} ${resolved}` };
}
const connected = await waitForProviderConnection();
if (connected) {
return { connected: true, message: `Connected ${resolved}` };
return { connected: true, message: `${t("status.connected")} ${resolved}` };
}
return { connected: false, pending: true };
}
const message = describeProviderError(error, "Failed to complete OAuth");
const message = describeProviderError(error, t("providers.oauth_failed"));
setProviderAuthError(message);
throw error instanceof Error ? error : new Error(message);
}
@@ -383,12 +383,12 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
setProviderAuthError(null);
const c = options.client();
if (!c) {
throw new Error(t("app.error_not_connected", currentLocale()));
throw new Error(t("providers.not_connected"));
}
const trimmed = apiKey.trim();
if (!trimmed) {
throw new Error("API key is required");
throw new Error(t("providers.api_key_required"));
}
try {
@@ -397,9 +397,9 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
auth: { type: "api", key: trimmed },
});
await refreshProviders({ dispose: true });
return `Connected ${providerId}`;
return `${t("status.connected")} ${providerId}`;
} catch (error) {
const message = describeProviderError(error, "Failed to save API key");
const message = describeProviderError(error, t("providers.save_api_key_failed"));
setProviderAuthError(message);
throw error instanceof Error ? error : new Error(message);
}
@@ -409,12 +409,12 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
setProviderAuthError(null);
const c = options.client();
if (!c) {
throw new Error(t("app.error_not_connected", currentLocale()));
throw new Error(t("providers.not_connected"));
}
const resolved = providerId.trim();
if (!resolved) {
throw new Error("Provider ID is required");
throw new Error(t("providers.provider_id_required"));
}
const provider = options.providers().find((entry) => entry.id === resolved) as
@@ -447,7 +447,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
return;
}
throw new Error("Provider auth removal is not supported by this client.");
throw new Error(t("providers.removal_unsupported"));
};
const disableProvider = async () => {
@@ -492,18 +492,18 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
}
if (!Array.isArray(updated?.connected) || !updated.connected.includes(resolved)) {
return disabled
? `Disconnected ${resolved} and disabled it in OpenCode config.`
: `Disconnected ${resolved}.`;
? `${t("providers.disconnected_prefix")} ${resolved} ${t("providers.disabled_in_config_suffix")}`
: `${t("providers.disconnected_prefix")} ${resolved}.`;
}
}
if (Array.isArray(updated?.connected) && updated.connected.includes(resolved)) {
return `Removed stored credentials for ${resolved}, but the worker still reports it as connected. Clear any remaining API key or OAuth credentials and restart the worker to fully disconnect.`;
return `Removed stored credentials for ${resolved}${t("providers.still_connected_suffix")}`;
}
removeProviderFromState(resolved);
return `Disconnected ${resolved}`;
return `${t("providers.disconnected_prefix")} ${resolved}`;
} catch (error) {
const message = describeProviderError(error, "Failed to disconnect provider");
const message = describeProviderError(error, t("providers.disconnect_failed"));
setProviderAuthError(message);
throw error instanceof Error ? error : new Error(message);
}
@@ -524,7 +524,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
} catch (error) {
setProviderAuthPreferredProviderId(null);
setProviderAuthReturnFocusTarget("none");
const message = describeProviderError(error, "Failed to load providers");
const message = describeProviderError(error, t("providers.load_failed"));
setProviderAuthError(message);
throw error;
} finally {

View File

@@ -1284,4 +1284,57 @@ export default {
"session.restart_update_title": "Restart to apply update {version}",
"session.downloading_update_title": "Downloading update {version}",
"session.update_available_title": "Update available {version}",
// ==================== Automations context ====================
"automations.server_unavailable": "OpenWork server unavailable. Connect to sync scheduled tasks.",
"automations.server_needs_token": "OpenWork server needs a token to load scheduled tasks.",
"automations.server_not_ready": "OpenWork server not ready.",
"automations.failed_to_load": "Failed to load scheduled tasks.",
"automations.desktop_required": "Scheduled tasks require the desktop app.",
"automations.schedule_required": "Schedule is required.",
"automations.prompt_required": "Prompt is required.",
"automations.prompt_empty": "Automation prompt is empty.",
// ==================== Question Modal ====================
"question_modal.question_counter": "Question {current} of {total}",
"question_modal.custom_answer_label": "Or type a custom answer",
"question_modal.custom_answer_placeholder": "Type your answer here...",
// ==================== Common (additions) ====================
"common.navigate": "navigate",
"common.select": "select",
"common.submit": "Submit",
"common.next": "Next",
"common.question": "Question",
// ==================== Providers store ====================
"providers.api_key_label": "API key",
"providers.not_connected": "Not connected to a server",
"providers.connect_failed": "Failed to connect provider",
"providers.oauth_failed": "Failed to complete OAuth",
"providers.save_api_key_failed": "Failed to save API key",
"providers.disconnect_failed": "Failed to disconnect provider",
"providers.load_failed": "Failed to load providers",
"providers.auth_failed": "Authentication failed",
"providers.rate_limit_exceeded": "Rate limit exceeded",
"providers.provider_error": "Provider error ({provider})",
"providers.request_failed": "Request failed",
"providers.api_key_required": "API key is required",
"providers.no_providers_available": "No providers available",
"providers.provider_id_required": "Provider ID is required",
"providers.unknown_provider": "Unknown provider",
"providers.no_oauth_prefix": "No OAuth flow available for",
"providers.use_api_key_suffix": "Use an API key instead.",
"providers.not_oauth_flow_prefix": "Selected auth method is not an OAuth flow for",
"providers.oauth_method_required": "OAuth method is required",
"providers.removal_unsupported": "Provider auth removal is not supported by this client.",
"providers.disconnected_prefix": "Disconnected",
"providers.disabled_in_config_suffix": "and disabled it in OpenCode config.",
"providers.still_connected_suffix": ", but the worker still reports it as connected. Clear any remaining API key or OAuth credentials and restart the worker to fully disconnect.",
// ==================== MCP Auth Modal (additions) ====================
"mcp.auth.authorization_link": "Authorization link",
"mcp.auth.copied": "Copied",
"mcp.auth.copy_link": "Copy link",
"mcp.auth.request_timed_out": "Request timed out.",
} as const;