diff options
| author | Adam <[email protected]> | 2026-02-27 15:50:50 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-27 15:50:50 -0600 |
| commit | 7f851da15ecde7db6535aae1f13009d3d6068daf (patch) | |
| tree | e8978e858c09a6507dfe0ad40b32b008d634b82e /packages/console/app/src/routes | |
| parent | a3bdb974b36714ace5e2a41e76da8aa6682eb4c8 (diff) | |
| download | opencode-7f851da15ecde7db6535aae1f13009d3d6068daf.tar.gz opencode-7f851da15ecde7db6535aae1f13009d3d6068daf.zip | |
chore(console): i18n sync (#15360)
Diffstat (limited to 'packages/console/app/src/routes')
12 files changed, 89 insertions, 49 deletions
diff --git a/packages/console/app/src/routes/[...404].tsx b/packages/console/app/src/routes/[...404].tsx index 414491f9d..e20ec36fd 100644 --- a/packages/console/app/src/routes/[...404].tsx +++ b/packages/console/app/src/routes/[...404].tsx @@ -16,8 +16,8 @@ export default function NotFound() { <div data-component="content"> <section data-component="top"> <a href={language.route("/")} data-slot="logo-link"> - <img data-slot="logo light" src={logoLight} alt="opencode logo light" /> - <img data-slot="logo dark" src={logoDark} alt="opencode logo dark" /> + <img data-slot="logo light" src={logoLight} alt={i18n.t("notFound.logoLightAlt")} /> + <img data-slot="logo dark" src={logoDark} alt={i18n.t("notFound.logoDarkAlt")} /> </a> <h1 data-slot="title">{i18n.t("notFound.heading")}</h1> </section> diff --git a/packages/console/app/src/routes/api/enterprise.ts b/packages/console/app/src/routes/api/enterprise.ts index 6776a7b3c..27e2dc493 100644 --- a/packages/console/app/src/routes/api/enterprise.ts +++ b/packages/console/app/src/routes/api/enterprise.ts @@ -1,5 +1,7 @@ import type { APIEvent } from "@solidjs/start/server" import { AWS } from "@opencode-ai/console-core/aws.js" +import { i18n } from "~/i18n" +import { localeFromRequest } from "~/lib/language" interface EnterpriseFormData { name: string @@ -9,18 +11,19 @@ interface EnterpriseFormData { } export async function POST(event: APIEvent) { + const dict = i18n(localeFromRequest(event.request)) try { const body = (await event.request.json()) as EnterpriseFormData // Validate required fields if (!body.name || !body.role || !body.email || !body.message) { - return Response.json({ error: "All fields are required" }, { status: 400 }) + return Response.json({ error: dict["enterprise.form.error.allFieldsRequired"] }, { status: 400 }) } // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ if (!emailRegex.test(body.email)) { - return Response.json({ error: "Invalid email format" }, { status: 400 }) + return Response.json({ error: dict["enterprise.form.error.invalidEmailFormat"] }, { status: 400 }) } // Create email content @@ -39,9 +42,9 @@ ${body.email}`.trim() replyTo: body.email, }) - return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 }) + return Response.json({ success: true, message: dict["enterprise.form.success.submitted"] }, { status: 200 }) } catch (error) { console.error("Error processing enterprise form:", error) - return Response.json({ error: "Internal server error" }, { status: 500 }) + return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 }) } } diff --git a/packages/console/app/src/routes/auth/[...callback].ts b/packages/console/app/src/routes/auth/[...callback].ts index 664f6cc6d..00bb89406 100644 --- a/packages/console/app/src/routes/auth/[...callback].ts +++ b/packages/console/app/src/routes/auth/[...callback].ts @@ -2,15 +2,17 @@ import { redirect } from "@solidjs/router" import type { APIEvent } from "@solidjs/start/server" import { AuthClient } from "~/context/auth" import { useAuthSession } from "~/context/auth" +import { i18n } from "~/i18n" import { localeFromRequest, route } from "~/lib/language" export async function GET(input: APIEvent) { const url = new URL(input.request.url) const locale = localeFromRequest(input.request) + const dict = i18n(locale) try { const code = url.searchParams.get("code") - if (!code) throw new Error("No code found") + if (!code) throw new Error(dict["auth.callback.error.codeMissing"]) const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`) if (result.err) throw new Error(result.err.message) const decoded = AuthClient.decode(result.tokens.access, {} as any) diff --git a/packages/console/app/src/routes/bench/submission.ts b/packages/console/app/src/routes/bench/submission.ts index 94639439b..969ff1659 100644 --- a/packages/console/app/src/routes/bench/submission.ts +++ b/packages/console/app/src/routes/bench/submission.ts @@ -2,6 +2,8 @@ import type { APIEvent } from "@solidjs/start/server" import { Database } from "@opencode-ai/console-core/drizzle/index.js" import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js" import { Identifier } from "@opencode-ai/console-core/identifier.js" +import { i18n } from "~/i18n" +import { localeFromRequest } from "~/lib/language" interface SubmissionBody { model: string @@ -10,10 +12,11 @@ interface SubmissionBody { } export async function POST(event: APIEvent) { + const dict = i18n(localeFromRequest(event.request)) const body = (await event.request.json()) as SubmissionBody if (!body.model || !body.agent || !body.result) { - return Response.json({ error: "All fields are required" }, { status: 400 }) + return Response.json({ error: dict["bench.submission.error.allFieldsRequired"] }, { status: 400 }) } await Database.use((tx) => diff --git a/packages/console/app/src/routes/brand/index.tsx b/packages/console/app/src/routes/brand/index.tsx index 9140462c7..af89f4985 100644 --- a/packages/console/app/src/routes/brand/index.tsx +++ b/packages/console/app/src/routes/brand/index.tsx @@ -33,6 +33,7 @@ const brandAssets = "/opencode-brand-assets.zip" export default function Brand() { const i18n = useI18n() + const alt = i18n.t("brand.meta.description") const downloadFile = async (url: string, filename: string) => { try { const response = await fetch(url) @@ -88,7 +89,7 @@ export default function Brand() { <div data-component="brand-grid"> <div> - <img src={previewLogoLight} alt="OpenCode brand guidelines" /> + <img src={previewLogoLight} alt={alt} /> <div data-component="actions"> <button onClick={() => downloadFile(logoLightPng, "opencode-logo-light.png")}> PNG @@ -115,7 +116,7 @@ export default function Brand() { </div> </div> <div> - <img src={previewLogoDark} alt="OpenCode brand guidelines" /> + <img src={previewLogoDark} alt={alt} /> <div data-component="actions"> <button onClick={() => downloadFile(logoDarkPng, "opencode-logo-dark.png")}> PNG @@ -142,7 +143,7 @@ export default function Brand() { </div> </div> <div> - <img src={previewLogoLightSquare} alt="OpenCode brand guidelines" /> + <img src={previewLogoLightSquare} alt={alt} /> <div data-component="actions"> <button onClick={() => downloadFile(logoLightSquarePng, "opencode-logo-light-square.png")}> PNG @@ -169,7 +170,7 @@ export default function Brand() { </div> </div> <div> - <img src={previewLogoDarkSquare} alt="OpenCode brand guidelines" /> + <img src={previewLogoDarkSquare} alt={alt} /> <div data-component="actions"> <button onClick={() => downloadFile(logoDarkSquarePng, "opencode-logo-dark-square.png")}> PNG @@ -196,7 +197,7 @@ export default function Brand() { </div> </div> <div> - <img src={previewWordmarkLight} alt="OpenCode brand guidelines" /> + <img src={previewWordmarkLight} alt={alt} /> <div data-component="actions"> <button onClick={() => downloadFile(wordmarkLightPng, "opencode-wordmark-light.png")}> PNG @@ -223,7 +224,7 @@ export default function Brand() { </div> </div> <div> - <img src={previewWordmarkDark} alt="OpenCode brand guidelines" /> + <img src={previewWordmarkDark} alt={alt} /> <div data-component="actions"> <button onClick={() => downloadFile(wordmarkDarkPng, "opencode-wordmark-dark.png")}> PNG @@ -250,7 +251,7 @@ export default function Brand() { </div> </div> <div> - <img src={previewWordmarkSimpleLight} alt="OpenCode brand guidelines" /> + <img src={previewWordmarkSimpleLight} alt={alt} /> <div data-component="actions"> <button onClick={() => downloadFile(wordmarkSimpleLightPng, "opencode-wordmark-simple-light.png")}> PNG @@ -277,7 +278,7 @@ export default function Brand() { </div> </div> <div> - <img src={previewWordmarkSimpleDark} alt="OpenCode brand guidelines" /> + <img src={previewWordmarkSimpleDark} alt={alt} /> <div data-component="actions"> <button onClick={() => downloadFile(wordmarkSimpleDarkPng, "opencode-wordmark-simple-dark.png")}> PNG diff --git a/packages/console/app/src/routes/download/[channel]/[platform].ts b/packages/console/app/src/routes/download/[channel]/[platform].ts index 9a5284263..e9b3f23e7 100644 --- a/packages/console/app/src/routes/download/[channel]/[platform].ts +++ b/packages/console/app/src/routes/download/[channel]/[platform].ts @@ -19,7 +19,7 @@ const downloadNames: Record<string, string> = { export async function GET({ params: { platform, channel } }: APIEvent) { const assetName = assetNames[platform] - if (!assetName) return new Response("Not Found", { status: 404 }) + if (!assetName) return new Response(null, { status: 404 }) const resp = await fetch( `https://github.com/anomalyco/${channel === "stable" ? "opencode" : "opencode-beta"}/releases/latest/download/${assetName}`, diff --git a/packages/console/app/src/routes/stripe/webhook.ts b/packages/console/app/src/routes/stripe/webhook.ts index 47ca442ec..95cd9da21 100644 --- a/packages/console/app/src/routes/stripe/webhook.ts +++ b/packages/console/app/src/routes/stripe/webhook.ts @@ -306,7 +306,7 @@ export async function POST(input: APIEvent) { .update(BillingTable) .set({ reload: false, - reloadError: errorMessage ?? "Payment failed.", + reloadError: errorMessage ?? "workspace.reload.error.paymentFailed", timeReloadError: sql`now()`, }) .where(eq(BillingTable.workspaceID, Actor.workspace())), diff --git a/packages/console/app/src/routes/temp.tsx b/packages/console/app/src/routes/temp.tsx index ac506928e..4eed47857 100644 --- a/packages/console/app/src/routes/temp.tsx +++ b/packages/console/app/src/routes/temp.tsx @@ -47,8 +47,8 @@ export default function Home() { <div data-component="content"> <section data-component="top"> - <img data-slot="logo light" src={logoLight} alt="opencode logo light" /> - <img data-slot="logo dark" src={logoDark} alt="opencode logo dark" /> + <img data-slot="logo light" src={logoLight} alt={i18n.t("temp.logoLightAlt")} /> + <img data-slot="logo dark" src={logoDark} alt={i18n.t("temp.logoDarkAlt")} /> <h1 data-slot="title">{i18n.t("temp.hero.title")}</h1> <div data-slot="login"> <a href="/auth">{i18n.t("temp.zen")}</a> diff --git a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx index 395d008e1..f67775d79 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx @@ -12,6 +12,7 @@ import { queryBillingInfo } from "../../common" import styles from "./lite-section.module.css" import { useI18n } from "~/context/i18n" import { useLanguage } from "~/context/language" +import { formError } from "~/lib/form-error" const queryLiteSubscription = query(async (workspaceID: string) => { "use server" @@ -114,7 +115,7 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) = const setLiteUseBalance = action(async (form: FormData) => { "use server" const workspaceID = form.get("workspaceID")?.toString() - if (!workspaceID) return { error: "Workspace ID is required" } + if (!workspaceID) return { error: formError.workspaceRequired } const useBalance = form.get("useBalance")?.toString() === "true" return json( diff --git a/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx index 7084be133..90c9d7a2e 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx @@ -202,7 +202,8 @@ export function ReloadSection() { minute: "2-digit", second: "2-digit", })} - . {i18n.t("workspace.reload.reason")} {billingInfo()?.reloadError?.replace(/\.$/, "")}.{" "} + . {i18n.t("workspace.reload.reason")}{" "} + {localizeError(i18n.t, billingInfo()?.reloadError ?? undefined).replace(/\.$/, "")}.{" "} {i18n.t("workspace.reload.updatePaymentMethod")} </p> <form action={reload} method="post" data-slot="create-form"> diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 3f6d69bf2..d3a25c5f6 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -35,6 +35,8 @@ import { createTrialLimiter } from "./trialLimiter" import { createStickyTracker } from "./stickyProviderTracker" import { LiteData } from "@opencode-ai/console-core/lite.js" import { Resource } from "@opencode-ai/console-resource" +import { i18n, type Key } from "~/i18n" +import { localeFromRequest } from "~/lib/language" type ZenData = Awaited<ReturnType<typeof ZenData.list>> type RetryOptions = { @@ -43,6 +45,15 @@ type RetryOptions = { } type BillingSource = "anonymous" | "free" | "byok" | "subscription" | "lite" | "balance" +function resolve(text: string, params?: Record<string, string | number>) { + if (!params) return text + return text.replace(/\{\{(\w+)\}\}/g, (raw, key) => { + const value = params[key] + if (value === undefined || value === null) return raw + return String(value) + }) +} + export async function handler( input: APIEvent, opts: { @@ -60,6 +71,8 @@ export async function handler( const MAX_FAILOVER_RETRIES = 3 const MAX_429_RETRIES = 3 + const dict = i18n(localeFromRequest(input.request)) + const t = (key: Key, params?: Record<string, string | number>) => resolve(dict[key], params) const ADMIN_WORKSPACES = [ "wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank "wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench @@ -86,7 +99,7 @@ export async function handler( const dataDumper = createDataDumper(sessionId, requestId, projectId) const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient) const isTrial = await trialLimiter?.isTrial() - const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip, input.request.headers) + const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip, input.request) await rateLimiter?.check() const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId) const stickyProvider = await stickyTracker?.get() @@ -359,14 +372,20 @@ export async function handler( } function validateModel(zenData: ZenData, reqModel: string) { - if (!(reqModel in zenData.models)) throw new ModelError(`Model ${reqModel} not supported`) + if (!(reqModel in zenData.models)) throw new ModelError(t("zen.api.error.modelNotSupported", { model: reqModel })) const modelId = reqModel as keyof typeof zenData.models const modelData = Array.isArray(zenData.models[modelId]) ? zenData.models[modelId].find((model) => opts.format === model.formatFilter) : zenData.models[modelId] - if (!modelData) throw new ModelError(`Model ${reqModel} not supported for format ${opts.format}`) + if (!modelData) + throw new ModelError( + t("zen.api.error.modelFormatNotSupported", { + model: reqModel, + format: opts.format, + }), + ) logger.metric({ model: modelId }) @@ -418,8 +437,9 @@ export async function handler( return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider) })() - if (!modelProvider) throw new ModelError("No provider available") - if (!(modelProvider.id in zenData.providers)) throw new ModelError(`Provider ${modelProvider.id} not supported`) + if (!modelProvider) throw new ModelError(t("zen.api.error.noProviderAvailable")) + if (!(modelProvider.id in zenData.providers)) + throw new ModelError(t("zen.api.error.providerNotSupported", { provider: modelProvider.id })) return { ...modelProvider, @@ -439,7 +459,7 @@ export async function handler( const apiKey = opts.parseApiKey(input.request.headers) if (!apiKey || apiKey === "public") { if (modelInfo.allowAnonymous) return - throw new AuthError("Missing API key.") + throw new AuthError(t("zen.api.error.missingApiKey")) } const data = await Database.use((tx) => @@ -520,13 +540,13 @@ export async function handler( .then((rows) => rows[0]), ) - if (!data) throw new AuthError("Invalid API key.") + if (!data) throw new AuthError(t("zen.api.error.invalidApiKey")) if ( modelInfo.id.startsWith("alpha-") && Resource.App.stage === "production" && !ADMIN_WORKSPACES.includes(data.workspaceID) ) - throw new AuthError(`Model ${modelInfo.id} not supported`) + throw new AuthError(t("zen.api.error.modelNotSupported", { model: modelInfo.id })) logger.metric({ api_key: data.apiKey, @@ -590,7 +610,9 @@ export async function handler( }) if (result.status === "rate-limited") throw new SubscriptionUsageLimitError( - `Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`, + t("zen.api.error.subscriptionQuotaExceeded", { + retryIn: formatRetryTime(result.resetInSec), + }), result.resetInSec, ) } @@ -606,7 +628,9 @@ export async function handler( }) if (result.status === "rate-limited") throw new SubscriptionUsageLimitError( - `Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`, + t("zen.api.error.subscriptionQuotaExceeded", { + retryIn: formatRetryTime(result.resetInSec), + }), result.resetInSec, ) } @@ -632,7 +656,7 @@ export async function handler( }) if (result.status === "rate-limited") throw new SubscriptionUsageLimitError( - `Subscription quota exceeded. You can continue using free models.`, + t("zen.api.error.subscriptionQuotaExceededUseFreeModels"), result.resetInSec, ) } @@ -647,7 +671,7 @@ export async function handler( }) if (result.status === "rate-limited") throw new SubscriptionUsageLimitError( - `Subscription quota exceeded. You can continue using free models.`, + t("zen.api.error.subscriptionQuotaExceededUseFreeModels"), result.resetInSec, ) } @@ -662,7 +686,7 @@ export async function handler( }) if (result.status === "rate-limited") throw new SubscriptionUsageLimitError( - `Subscription quota exceeded. You can continue using free models.`, + t("zen.api.error.subscriptionQuotaExceededUseFreeModels"), result.resetInSec, ) } @@ -675,14 +699,10 @@ export async function handler( // Validate pay as you go billing const billing = authInfo.billing - if (!billing.paymentMethodID) - throw new CreditsError( - `No payment method. Add a payment method here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`, - ) - if (billing.balance <= 0) - throw new CreditsError( - `Insufficient balance. Manage your billing here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`, - ) + const billingUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/billing` + const membersUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/members` + if (!billing.paymentMethodID) throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl })) + if (billing.balance <= 0) throw new CreditsError(t("zen.api.error.insufficientBalance", { billingUrl })) const now = new Date() const currentYear = now.getUTCFullYear() @@ -696,7 +716,10 @@ export async function handler( currentMonth === billing.timeMonthlyUsageUpdated.getUTCMonth() ) throw new MonthlyLimitError( - `Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`, + t("zen.api.error.workspaceMonthlyLimitReached", { + amount: billing.monthlyLimit, + billingUrl, + }), ) if ( @@ -708,7 +731,10 @@ export async function handler( currentMonth === authInfo.user.timeMonthlyUsageUpdated.getUTCMonth() ) throw new UserLimitError( - `You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/members`, + t("zen.api.error.userMonthlyLimitReached", { + amount: authInfo.user.monthlyLimit, + membersUrl, + }), ) return "balance" @@ -716,7 +742,7 @@ export async function handler( function validateModelSettings(authInfo: AuthInfo) { if (!authInfo) return - if (authInfo.isDisabled) throw new ModelError("Model is disabled") + if (authInfo.isDisabled) throw new ModelError(t("zen.api.error.modelDisabled")) } function updateProviderKey(authInfo: AuthInfo, providerInfo: ProviderInfo) { diff --git a/packages/console/app/src/routes/zen/util/rateLimiter.ts b/packages/console/app/src/routes/zen/util/rateLimiter.ts index 5e4f31e67..6325a7b4d 100644 --- a/packages/console/app/src/routes/zen/util/rateLimiter.ts +++ b/packages/console/app/src/routes/zen/util/rateLimiter.ts @@ -3,11 +3,14 @@ import { IpRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js" import { FreeUsageLimitError } from "./error" import { logger } from "./logger" import { ZenData } from "@opencode-ai/console-core/model.js" +import { i18n } from "~/i18n" +import { localeFromRequest } from "~/lib/language" -export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string, headers: Headers) { +export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string, request: Request) { if (!limit) return + const dict = i18n(localeFromRequest(request)) - const limitValue = limit.checkHeader && !headers.get(limit.checkHeader) ? limit.fallbackValue! : limit.value + const limitValue = limit.checkHeader && !request.headers.get(limit.checkHeader) ? limit.fallbackValue! : limit.value const ip = !rawIp.length ? "unknown" : rawIp const now = Date.now() @@ -36,7 +39,7 @@ export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: s logger.debug(`rate limit total: ${total}`) if (total >= limitValue) throw new FreeUsageLimitError( - `Rate limit exceeded. Please try again later.`, + dict["zen.api.error.rateLimitExceeded"], limit.period === "day" ? getRetryAfterDay(now) : getRetryAfterHour(rows, intervals, limitValue, now), ) }, |
