feat(i18n): localize hardcoded application copy (#40377)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
6c3299103c
commit
e85735028c
@@ -52,12 +52,12 @@ describe("message-file", () => {
|
||||
})
|
||||
|
||||
test("labels attachment types from the basename extension", () => {
|
||||
expect(typeLabel("list.md", "text/plain")).toBe("Markdown")
|
||||
expect(typeLabel("/repo/src/main.ts", "text/plain")).toBe("TypeScript")
|
||||
expect(typeLabel("/tmp/report.pdf", "application/pdf")).toBe("PDF")
|
||||
expect(typeLabel("notes.xyz", "text/plain")).toBe("XYZ")
|
||||
expect(typeLabel("/home/user/my.project/Makefile", "text/plain")).toBe("File")
|
||||
expect(typeLabel(".gitignore", "text/plain")).toBe("File")
|
||||
expect(typeLabel("/repo/.env", "text/plain")).toBe("File")
|
||||
expect(typeLabel("list.md", "text/plain", "File")).toBe("Markdown")
|
||||
expect(typeLabel("/repo/src/main.ts", "text/plain", "File")).toBe("TypeScript")
|
||||
expect(typeLabel("/tmp/report.pdf", "application/pdf", "File")).toBe("PDF")
|
||||
expect(typeLabel("notes.xyz", "text/plain", "File")).toBe("XYZ")
|
||||
expect(typeLabel("/home/user/my.project/Makefile", "text/plain", "File")).toBe("File")
|
||||
expect(typeLabel(".gitignore", "text/plain", "File")).toBe("File")
|
||||
expect(typeLabel("/repo/.env", "text/plain", "File")).toBe("File")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,12 +23,12 @@ const LANGUAGE_NAMES = new Map<string, string>(
|
||||
|
||||
// attachments carry text/plain for all text files, so the label comes from the extension;
|
||||
// filename may be an absolute path, so extract the basename before looking for one
|
||||
export function typeLabel(filename: string, mime: string) {
|
||||
export function typeLabel(filename: string, mime: string, fallback: string) {
|
||||
if (mime === "application/pdf") return "PDF"
|
||||
const base = getFilename(filename)
|
||||
// idx 0 is a dotfile like .gitignore, not an extension
|
||||
const idx = base.lastIndexOf(".")
|
||||
const suffix = idx <= 0 ? "" : base.slice(idx + 1).toLowerCase()
|
||||
if (!suffix) return "File"
|
||||
if (!suffix) return fallback
|
||||
return LANGUAGE_NAMES.get(suffix) ?? suffix.toUpperCase()
|
||||
}
|
||||
|
||||
@@ -459,10 +459,10 @@ function newLayout() {
|
||||
return typeof document !== "undefined" && document.body.hasAttribute("data-new-layout")
|
||||
}
|
||||
|
||||
function webSearchProviderLabel(provider: unknown) {
|
||||
if (provider === "parallel") return "Parallel Web Search"
|
||||
if (provider === "exa") return "Exa Web Search"
|
||||
return "Web Search"
|
||||
function webSearchProviderLabel(provider: unknown, i18n: ReturnType<typeof useI18n>) {
|
||||
const name = provider === "parallel" ? "Parallel" : provider === "exa" ? "Exa" : undefined
|
||||
if (name) return i18n.t("ui.tool.websearch.provider", { provider: name })
|
||||
return i18n.t("ui.tool.websearch")
|
||||
}
|
||||
|
||||
export function getToolInfo(
|
||||
@@ -505,7 +505,7 @@ export function getToolInfo(
|
||||
case "websearch":
|
||||
return {
|
||||
icon: "window-cursor",
|
||||
title: webSearchProviderLabel(metadata?.provider),
|
||||
title: webSearchProviderLabel(metadata?.provider, i18n),
|
||||
subtitle: input.query,
|
||||
}
|
||||
case "task": {
|
||||
@@ -1309,7 +1309,7 @@ export function UserMessageDisplay(props: {
|
||||
clickable={!!props.actions?.openAttachment}
|
||||
onClick={() => props.actions?.openAttachment?.(file)}
|
||||
>
|
||||
{typeLabel(name, file.mime)}
|
||||
{typeLabel(name, file.mime, i18n.t("ui.common.file"))}
|
||||
</AttachmentCardV2>
|
||||
</Show>
|
||||
)
|
||||
@@ -1594,7 +1594,11 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
||||
<ToolErrorCard
|
||||
tool={part().tool}
|
||||
error={error()}
|
||||
title={part().tool === "websearch" ? webSearchProviderLabel(partMetadata().provider) : undefined}
|
||||
title={
|
||||
part().tool === "websearch"
|
||||
? webSearchProviderLabel(partMetadata().provider, i18n)
|
||||
: undefined
|
||||
}
|
||||
defaultOpen={props.defaultOpen}
|
||||
open={controlledOpen()}
|
||||
onOpenChange={props.onToolOpenChange ? handleToolOpenChange : undefined}
|
||||
@@ -1956,12 +1960,13 @@ ToolRegistry.register({
|
||||
ToolRegistry.register({
|
||||
name: "websearch",
|
||||
render(props) {
|
||||
const i18n = useI18n()
|
||||
const query = createMemo(() => {
|
||||
const value = props.input.query
|
||||
if (typeof value !== "string") return ""
|
||||
return value
|
||||
})
|
||||
const title = createMemo(() => webSearchProviderLabel(props.metadata.provider))
|
||||
const title = createMemo(() => webSearchProviderLabel(props.metadata.provider, i18n))
|
||||
|
||||
return (
|
||||
<BasicTool
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createMemo } from "solid-js"
|
||||
import { AnimatedNumber } from "@opencode-ai/ui/animated-number"
|
||||
import { pluralCategory, pluralKey, useI18n, type UiI18nPluralKey } from "@opencode-ai/ui/context"
|
||||
import { pluralCategory, pluralKey, useI18n, type UiI18nPluralKey } from "@opencode-ai/ui/context/i18n"
|
||||
|
||||
function split(text: string) {
|
||||
const match = /{{\s*count\s*}}/.exec(text)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { useI18n } from "@opencode-ai/ui/context/i18n"
|
||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||
@@ -46,6 +47,7 @@ export type PromptInputV2Props = {
|
||||
}
|
||||
|
||||
export function PromptInputV2(props: PromptInputV2Props) {
|
||||
const i18n = useI18n()
|
||||
const state = props.controller.state
|
||||
const view = props.controller.view
|
||||
let editor: HTMLDivElement | undefined
|
||||
@@ -87,14 +89,14 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||
/>
|
||||
<Show when={state.popover.type !== "closed"}>
|
||||
<PromptInputV2Popover
|
||||
emptyLabel="No matching items"
|
||||
emptyLabel={i18n.t("ui.promptInput.noMatchingItems")}
|
||||
items={props.controller.suggestions()}
|
||||
activeID={state.popover.type === "closed" ? undefined : state.popover.activeID}
|
||||
search={
|
||||
state.popover.type === "command-menu"
|
||||
? {
|
||||
value: state.popover.query,
|
||||
label: "Commands",
|
||||
label: i18n.t("ui.promptInput.commands"),
|
||||
placeholder: "/",
|
||||
onValueChange: props.controller.setQuery,
|
||||
onKeyDown: props.controller.onKeyDown,
|
||||
@@ -124,7 +126,7 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||
>
|
||||
<Show when={state.drag === "active"}>
|
||||
<div class="pointer-events-none absolute inset-0 z-20 grid place-items-center rounded-xl bg-v2-background-bg-base/90 text-v2-text-text-base">
|
||||
Drop files to attach
|
||||
{i18n.t("ui.promptInput.dropFiles")}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -133,7 +135,7 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||
attachments={props.controller.attachments()}
|
||||
comments={props.controller.comments()}
|
||||
activeCommentID={state.activeContextID}
|
||||
removeLabel="Remove attachment"
|
||||
removeLabel={i18n.t("ui.promptInput.removeAttachment")}
|
||||
onAttachmentClick={props.controller.openAttachment}
|
||||
onAttachmentRemove={(attachment) => props.controller.removeAttachment(attachment.id)}
|
||||
onCommentClick={(comment) => props.controller.toggleContext(comment.key)}
|
||||
@@ -151,7 +153,7 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||
data-component="prompt-input"
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
aria-label="Prompt"
|
||||
aria-label={i18n.t("ui.promptInput.label")}
|
||||
contenteditable={!props.disabled && !props.readOnly}
|
||||
autocapitalize={state.mode === "normal" ? "sentences" : "off"}
|
||||
autocorrect={state.mode === "normal" ? "on" : "off"}
|
||||
@@ -186,7 +188,11 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||
classList={{ "font-mono!": state.mode === "shell" }}
|
||||
>
|
||||
{view.placeholder?.() ??
|
||||
(state.mode === "shell" ? "Enter shell command..." : "Ask anything, / for commands, @ for context...")}
|
||||
(state.mode === "shell" ? (
|
||||
i18n.t("ui.promptInput.placeholder.shell")
|
||||
) : (
|
||||
i18n.t("ui.promptInput.placeholder.normal", { slash: "/", at: "@" })
|
||||
))}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
@@ -200,13 +206,13 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||
>
|
||||
<PromptInputV2AddMenu
|
||||
disabled={state.mode === "shell"}
|
||||
title="Add images and files"
|
||||
title={i18n.t("ui.promptInput.add")}
|
||||
keybind={props.attachKeybind ?? ["Mod", "U"]}
|
||||
attachLabel="Images and files"
|
||||
attachLabel={i18n.t("ui.promptInput.attachments")}
|
||||
attachShortcut={props.attachShortcut ?? "Mod+U"}
|
||||
commandsLabel="Commands"
|
||||
contextLabel="Context"
|
||||
shellLabel="Shell command"
|
||||
commandsLabel={i18n.t("ui.promptInput.commands")}
|
||||
contextLabel={i18n.t("ui.promptInput.context")}
|
||||
shellLabel={i18n.t("ui.promptInput.shell")}
|
||||
onAttach={props.controller.attach}
|
||||
onCommands={props.controller.openCommands}
|
||||
onContext={props.controller.openContext}
|
||||
@@ -214,7 +220,11 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||
/>
|
||||
<Show when={view.agent} keyed>
|
||||
{(control) => (
|
||||
<PromptInputV2ConfiguredSelect title="Choose agent" keybind={["Mod", "."]} control={control} />
|
||||
<PromptInputV2ConfiguredSelect
|
||||
title={i18n.t("ui.promptInput.chooseAgent")}
|
||||
keybind={["Mod", "."]}
|
||||
control={control}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
<Show
|
||||
@@ -223,7 +233,7 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||
<Show when={view.model} keyed>
|
||||
{(control) => (
|
||||
<PromptInputV2ConfiguredSelect
|
||||
title="Choose model"
|
||||
title={i18n.t("ui.promptInput.chooseModel")}
|
||||
keybind={["Mod", "M"]}
|
||||
control={control}
|
||||
model
|
||||
@@ -238,7 +248,7 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||
{(control) => (
|
||||
<Show when={control.options().length > 1}>
|
||||
<PromptInputV2ConfiguredSelect
|
||||
title="Choose model variant"
|
||||
title={i18n.t("ui.promptInput.chooseVariant")}
|
||||
keybind={["Shift", "Mod", "D"]}
|
||||
control={control}
|
||||
/>
|
||||
@@ -250,8 +260,8 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||
mode={state.mode}
|
||||
stopping={view.submit.stopping()}
|
||||
disabled={!props.controller.canSubmit()}
|
||||
sendLabel="Send"
|
||||
stopLabel="Stop"
|
||||
sendLabel={i18n.t("ui.promptInput.send")}
|
||||
stopLabel={i18n.t("ui.promptInput.stop")}
|
||||
onSubmit={props.controller.submit}
|
||||
onStop={props.controller.stop}
|
||||
/>
|
||||
@@ -377,6 +387,7 @@ export function PromptInputV2Attachments(props: {
|
||||
onCommentClick?: (comment: PromptInputV2Comment) => void
|
||||
onCommentRemove?: (comment: PromptInputV2Comment) => void
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<Show when={props.attachments.length > 0 || (props.comments?.length ?? 0) > 0}>
|
||||
<div data-component="prompt-input-v2-attachments" data-slot="prompt-attachments" class="relative">
|
||||
@@ -404,7 +415,7 @@ export function PromptInputV2Attachments(props: {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onCommentRemove?.(comment)}
|
||||
class="absolute -top-1 -right-1 size-4 rounded-full bg-v2-icon-icon-muted outline-solid outline-1 outline-v2-icon-icon-contrast flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
class="absolute -top-1 -end-1 size-4 rounded-full bg-v2-icon-icon-muted outline-solid outline-1 outline-v2-icon-icon-contrast flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
aria-label={props.removeLabel}
|
||||
>
|
||||
<IconV2 name="outline-xmark" class="text-v2-icon-icon-contrast" />
|
||||
@@ -420,7 +431,7 @@ export function PromptInputV2Attachments(props: {
|
||||
when={attachment.mime.startsWith("image/")}
|
||||
fallback={
|
||||
<AttachmentCardV2 title={attachment.filename}>
|
||||
{typeLabel(attachment.filename, attachment.mime)}
|
||||
{typeLabel(attachment.filename, attachment.mime, i18n.t("ui.common.file"))}
|
||||
</AttachmentCardV2>
|
||||
}
|
||||
>
|
||||
@@ -436,7 +447,7 @@ export function PromptInputV2Attachments(props: {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onAttachmentRemove(attachment)}
|
||||
class="absolute -top-1 -right-1 size-4 rounded-full bg-v2-icon-icon-muted outline-solid outline-1 outline-v2-icon-icon-contrast flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
class="absolute -top-1 -end-1 size-4 rounded-full bg-v2-icon-icon-muted outline-solid outline-1 outline-v2-icon-icon-contrast flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
aria-label={props.removeLabel}
|
||||
>
|
||||
<IconV2 name="outline-xmark" class="text-v2-icon-icon-contrast" />
|
||||
@@ -447,11 +458,11 @@ export function PromptInputV2Attachments(props: {
|
||||
</div>
|
||||
<div
|
||||
data-slot="prompt-attachments-fade-left"
|
||||
class="pointer-events-none absolute inset-y-0 left-0 z-10 w-6 bg-[linear-gradient(to_right,var(--v2-background-bg-base),transparent)]"
|
||||
class="pointer-events-none absolute inset-y-0 start-0 z-10 w-6 bg-[linear-gradient(to_right,var(--v2-background-bg-base),transparent)] rtl:bg-[linear-gradient(to_left,var(--v2-background-bg-base),transparent)]"
|
||||
/>
|
||||
<div
|
||||
data-slot="prompt-attachments-fade-right"
|
||||
class="pointer-events-none absolute inset-y-0 right-0 z-10 w-6 bg-[linear-gradient(to_left,var(--v2-background-bg-base),transparent)]"
|
||||
class="pointer-events-none absolute inset-y-0 end-0 z-10 w-6 bg-[linear-gradient(to_left,var(--v2-background-bg-base),transparent)] rtl:bg-[linear-gradient(to_right,var(--v2-background-bg-base),transparent)]"
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -571,7 +582,7 @@ export function PromptInputV2Select(props: {
|
||||
<span class="truncate capitalize leading-5">
|
||||
{props.options.find((option) => option.id === props.current)?.label ?? props.current}
|
||||
</span>
|
||||
<span class="-ml-0.5 -mr-1 flex shrink-0">
|
||||
<span class="-ms-0.5 -me-1 flex shrink-0">
|
||||
<IconV2 name="chevron-down" />
|
||||
</span>
|
||||
</MenuV2.Trigger>
|
||||
@@ -637,7 +648,7 @@ export function PromptInputV2Popover(props: {
|
||||
<button
|
||||
type="button"
|
||||
data-suggestion-id={item.id}
|
||||
class="flex w-full items-center gap-2 rounded-md px-2 py-1 text-left hover:bg-v2-overlay-simple-overlay-hover"
|
||||
class="flex w-full items-center gap-2 rounded-md px-2 py-1 text-start hover:bg-v2-overlay-simple-overlay-hover"
|
||||
classList={{ "bg-v2-overlay-simple-overlay-hover": props.activeID === item.id }}
|
||||
onPointerMove={() => props.onActiveChange(item)}
|
||||
onClick={() => props.onSelect(item)}
|
||||
|
||||
Reference in New Issue
Block a user