diff --git a/packages/app/src/components/presets-manager.tsx b/packages/app/src/components/presets-manager.tsx new file mode 100644 index 000000000000..66eba5fc1e63 --- /dev/null +++ b/packages/app/src/components/presets-manager.tsx @@ -0,0 +1,197 @@ +import { createSignal, For, Show } from "solid-js" +import { Dialog } from "@opencode-ai/ui/dialog" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Icon } from "@opencode-ai/ui/icon" +import { Button } from "@opencode-ai/ui/button" +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { TextField } from "@opencode-ai/ui/text-field" +import { useI18n } from "@opencode-ai/ui/context" +import type { Preset, PresetsStore } from "@/hooks/use-presets" + +interface PresetsManagerProps { + store: PresetsStore +} + +export function PresetsManager(props: PresetsManagerProps) { + const { t } = useI18n() + const [editingId, setEditingId] = createSignal(null) + const [editName, setEditName] = createSignal("") + const [editContent, setEditContent] = createSignal("") + const [adding, setAdding] = createSignal(false) + const [newName, setNewName] = createSignal("") + const [newContent, setNewContent] = createSignal("") + + const startEdit = (preset: Preset) => { + setEditingId(preset.id) + setEditName(preset.name) + setEditContent(preset.content) + } + + const cancelEdit = () => { + setEditingId(null) + setEditName("") + setEditContent("") + } + + const saveEdit = () => { + const id = editingId() + if (!id) return + const name = editName().trim() + const content = editContent().trim() + if (!name || !content) return + props.store.update(id, { name, content }) + cancelEdit() + } + + const startAdd = () => { + setAdding(true) + setNewName("") + setNewContent("") + } + + const cancelAdd = () => { + setAdding(false) + setNewName("") + setNewContent("") + } + + const saveAdd = () => { + const name = newName().trim() + const content = newContent().trim() + if (!name || !content) return + props.store.add(name, content) + cancelAdd() + } + + return ( + +
+
+ + {t("presets.count", { count: props.store.presets().length })} + + +
+ + +
+ + +
+ + +
+
+
+ +
+ + {(preset) => { + const isEditing = () => editingId() === preset.id + return ( + +
+
+ {preset.name} +
+
+ {preset.content} +
+
+
+ + props.store.moveUp(preset.id)} + /> + + + props.store.moveDown(preset.id)} + /> + + + startEdit(preset)} + /> + + + props.store.remove(preset.id)} + /> + +
+
+ } + > +
+ + +
+ + +
+
+ + ) + }} + +
+ + +
+ + {t("presets.empty")} + {t("presets.empty.hint")} +
+
+ +
+ ) +} diff --git a/packages/app/src/components/presets-popover.tsx b/packages/app/src/components/presets-popover.tsx new file mode 100644 index 000000000000..089104b25982 --- /dev/null +++ b/packages/app/src/components/presets-popover.tsx @@ -0,0 +1,170 @@ +import { createSignal, For, Show } from "solid-js" +import { Popover } from "@opencode-ai/ui/popover" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Icon } from "@opencode-ai/ui/icon" +import { Button } from "@opencode-ai/ui/button" +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { TextField } from "@opencode-ai/ui/text-field" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useI18n } from "@opencode-ai/ui/context" +import { extractVariables, resolveVariables, type PresetsStore } from "@/hooks/use-presets" +import { PresetsManager } from "./presets-manager" + +interface PresetsPopoverProps { + store: PresetsStore + onInsert: (text: string) => void +} + +export function PresetsPopover(props: PresetsPopoverProps) { + const dialog = useDialog() + const { t } = useI18n() + const [open, setOpen] = createSignal(false) + const [pendingVariables, setPendingVariables] = createSignal(null) + const [pendingContent, setPendingContent] = createSignal("") + const [pendingName, setPendingName] = createSignal("") + const [varValues, setVarValues] = createSignal>({}) + const [currentVarIndex, setCurrentVarIndex] = createSignal(0) + + const handleSelect = (name: string, content: string) => { + const vars = extractVariables(content) + if (vars.length === 0) { + props.onInsert(content) + setOpen(false) + return + } + setPendingVariables(vars) + setPendingContent(content) + setPendingName(name) + setVarValues({}) + setCurrentVarIndex(0) + } + + const handleVarInput = (value: string) => { + const vars = pendingVariables() + if (!vars) return + const idx = currentVarIndex() + const key = vars[idx] + setVarValues((prev) => ({ ...prev, [key]: value })) + } + + const confirmVariable = () => { + const vars = pendingVariables() + if (!vars) return + const idx = currentVarIndex() + if (idx < vars.length - 1) { + setCurrentVarIndex(idx + 1) + } else { + const resolved = resolveVariables(pendingContent(), varValues()) + props.onInsert(resolved) + setOpen(false) + resetPending() + } + } + + const resetPending = () => { + setPendingVariables(null) + setPendingContent("") + setPendingName("") + setVarValues({}) + setCurrentVarIndex(0) + } + + const cancelPending = () => { + resetPending() + } + + const openManager = () => { + setOpen(false) + dialog.show(() => ) + } + + return ( + + + + } + > +
+ +
+ + {(preset) => ( + + )} + + +
+ {t("presets.empty")} +
+
+
+
+ +
+
+ + +
+
+ {t("presets.variables.title", { name: pendingName() })} +
+
+ {pendingVariables()![currentVarIndex()]}({currentVarIndex() + 1}/{pendingVariables()!.length}) +
+ handleVarInput(value)} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === "Enter") confirmVariable() + if (e.key === "Escape") cancelPending() + }} + class="mx-1 w-[calc(100%-8px)]" + autofocus + /> +
+ + +
+
+
+
+
+ ) +} diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index bdf55fee0564..dc5809d896cb 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -76,6 +76,8 @@ import { promptPlaceholder } from "./prompt-input/placeholder" import { useDirectoryPicker } from "./directory-picker" import { showToast } from "@/utils/toast" import { ImagePreview } from "@opencode-ai/ui/image-preview" +import { createPresetsStore } from "@/hooks/use-presets" +import { PresetsPopover } from "@/components/presets-popover" import { useQueries } from "@tanstack/solid-query" import { useQueryOptions } from "@/context/server-sync" import { pathKey } from "@/utils/path-key" @@ -144,6 +146,7 @@ export const PromptInput: Component = (props) => { const platform = usePlatform() const pickDirectory = useDirectoryPicker() const settings = useSettings() + const presetsStore = createPresetsStore() const { params, tabs, view } = useSessionLayout() let editorRef!: HTMLDivElement let fileInputRef: HTMLInputElement | undefined @@ -580,6 +583,13 @@ export const PromptInput: Component = (props) => { }) } + const insertPreset = (text: string) => { + requestAnimationFrame(() => { + editorRef.focus() + document.execCommand("insertText", false, text) + }) + } + const renderEditorWithCursor = (parts: Prompt) => { const cursor = currentCursor() renderEditor(parts) @@ -1597,6 +1607,10 @@ export const PromptInput: Component = (props) => { aria-label={language.t("prompt.action.attachFile")} /> + @@ -1823,6 +1837,10 @@ export const PromptInput: Component = (props) => { + diff --git a/packages/app/src/hooks/use-presets.test.ts b/packages/app/src/hooks/use-presets.test.ts new file mode 100644 index 000000000000..b2c74caef9f6 --- /dev/null +++ b/packages/app/src/hooks/use-presets.test.ts @@ -0,0 +1,55 @@ +import { describe, test, expect } from "bun:test" +import { extractVariables, resolveVariables } from "./use-presets" + +describe("extractVariables", () => { + test("extracts single variable", () => { + expect(extractVariables("Hello {name}")).toEqual(["name"]) + }) + + test("extracts multiple variables", () => { + expect(extractVariables("{date} {time} {file}")).toEqual(["date", "time", "file"]) + }) + + test("deduplicates variables", () => { + expect(extractVariables("{name} and {name}")).toEqual(["name"]) + }) + + test("returns empty array for no variables", () => { + expect(extractVariables("No variables here")).toEqual([]) + }) + + test("handles empty string", () => { + expect(extractVariables("")).toEqual([]) + }) + + test("extracts all supported variable types", () => { + const content = "Date: {date}, Time: {time}, File: {file}, Code: {code}, Text: {text}" + expect(extractVariables(content)).toEqual(["date", "time", "file", "code", "text"]) + }) +}) + +describe("resolveVariables", () => { + test("resolves single variable", () => { + expect(resolveVariables("Hello {name}", { name: "World" })).toBe("Hello World") + }) + + test("resolves multiple variables", () => { + expect(resolveVariables("{date} {time}", { date: "2026-01-01", time: "12:00" })).toBe("2026-01-01 12:00") + }) + + test("keeps unresolved variables as-is", () => { + expect(resolveVariables("Hello {name}", {})).toBe("Hello {name}") + }) + + test("handles empty string", () => { + expect(resolveVariables("", { name: "World" })).toBe("") + }) + + test("handles empty values", () => { + expect(resolveVariables("Hello {name}", {})).toBe("Hello {name}") + }) + + test("resolves duplicate variables", () => { + expect(resolveVariables("{name} and {name}", { name: "World" })).toBe("World and World") + }) +}) diff --git a/packages/app/src/hooks/use-presets.ts b/packages/app/src/hooks/use-presets.ts new file mode 100644 index 000000000000..d34d36dff415 --- /dev/null +++ b/packages/app/src/hooks/use-presets.ts @@ -0,0 +1,102 @@ +import { createMemo } from "solid-js" +import { createStore } from "solid-js/store" +import { persisted, Persist } from "@/utils/persist" + +export interface Preset { + id: string + name: string + content: string + order: number + createdAt: string + updatedAt: string +} + +export interface PresetsConfig { + presets: Preset[] +} + +function generateId(): string { + return Date.now().toString(36) + Math.random().toString(36).slice(2, 8) +} + +export function resolveVariables(content: string, values: Record): string { + return content.replace(/\{(\w+)\}/g, (_, key) => values[key] ?? `{${key}}`) +} + +export function extractVariables(content: string): string[] { + const matches = content.match(/\{(\w+)\}/g) + if (!matches) return [] + return [...new Set(matches.map((m) => m.slice(1, -1)))] +} + +export function createPresetsStore() { + const [store, setStore, _, ready] = persisted( + Persist.global("presets"), + createStore({ presets: [] }), + ) + + const presets = createMemo(() => [...(store.presets ?? [])].sort((a, b) => a.order - b.order)) + + const add = (name: string, content: string): Preset => { + const all = store.presets ?? [] + const maxOrder = all.length > 0 ? Math.max(...all.map((p) => p.order)) : 0 + const now = new Date().toISOString() + const preset: Preset = { + id: generateId(), + name, + content, + order: maxOrder + 1, + createdAt: now, + updatedAt: now, + } + setStore("presets", [...all, preset]) + return preset + } + + const update = (id: string, patch: Partial>) => { + const now = new Date().toISOString() + setStore( + "presets", + (store.presets ?? []).map((p) => (p.id === id ? { ...p, ...patch, updatedAt: now } : p)), + ) + } + + const remove = (id: string) => { + setStore( + "presets", + (store.presets ?? []).filter((p) => p.id !== id), + ) + } + + const moveUp = (id: string) => { + const all = [...(store.presets ?? [])].sort((a, b) => a.order - b.order) + const idx = all.findIndex((p) => p.id === id) + if (idx <= 0) return + const tmp = all[idx].order + all[idx] = { ...all[idx], order: all[idx - 1].order } + all[idx - 1] = { ...all[idx - 1], order: tmp } + setStore("presets", all) + } + + const moveDown = (id: string) => { + const all = [...(store.presets ?? [])].sort((a, b) => a.order - b.order) + const idx = all.findIndex((p) => p.id === id) + if (idx < 0 || idx >= all.length - 1) return + const tmp = all[idx].order + all[idx] = { ...all[idx], order: all[idx + 1].order } + all[idx + 1] = { ...all[idx + 1], order: tmp } + setStore("presets", all) + } + + return { + presets, + ready, + add, + update, + remove, + moveUp, + moveDown, + } +} + +export type PresetsStore = ReturnType diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 5d124024a3b2..b72eebd66c3e 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -844,4 +844,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "فشل إنشاء أيقونة المشروع الدائمة", "error.childStore.storeCreateFailed": "فشل إنشاء المخزن", "terminal.connectionLost.abnormalClose": "تم إغلاق WebSocket بشكل غير طبيعي: {{code}}", + + "presets.title": "الإعدادات المسبقة", + "presets.manage": "إدارة الإعدادات المسبقة", + "presets.count": "{{count}} إعدادات مسبقة", + "presets.empty": "لا توجد إعدادات مسبقة", + "presets.empty.hint": "انقر على \"إضافة\" لإنشاء أول إعداد مسبق", + "presets.add": "إضافة", + "presets.name": "الاسم", + "presets.content": "المحتوى", + "presets.content.placeholder": "المحتوى (يدعم المتغيرات {date} {time} {file} {code} {text})", + "presets.moveUp": "نقل لأعلى", + "presets.moveDown": "نقل لأسفل", + "presets.edit": "تعديل", + "presets.delete": "حذف", + "presets.variables.title": "أدخل المتغيرات لـ \"{{name}}\"", + "presets.variables.next": "التالي", + "presets.variables.insert": "إدراج", } diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 7c6965e330c3..a35ca4d85501 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -857,4 +857,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "Falha ao criar ícone de projeto persistente", "error.childStore.storeCreateFailed": "Falha ao criar armazenamento", "terminal.connectionLost.abnormalClose": "WebSocket fechado anormalmente: {{code}}", + + "presets.title": "Predefinições", + "presets.manage": "Gerenciar predefinições", + "presets.count": "{{count}} predefinições", + "presets.empty": "Sem predefinições", + "presets.empty.hint": "Clique em \"Adicionar\" para criar sua primeira predefinição", + "presets.add": "Adicionar", + "presets.name": "Nome", + "presets.content": "Conteúdo", + "presets.content.placeholder": "Conteúdo (suporta variáveis {date} {time} {file} {code} {text})", + "presets.moveUp": "Mover para cima", + "presets.moveDown": "Mover para baixo", + "presets.edit": "Editar", + "presets.delete": "Excluir", + "presets.variables.title": "Insira variáveis para \"{{name}}\"", + "presets.variables.next": "Próximo", + "presets.variables.insert": "Inserir", } diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 201b6ed81c12..a93da9d2b42f 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -933,4 +933,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "Nije uspjelo kreiranje trajne ikone projekta", "error.childStore.storeCreateFailed": "Nije uspjelo kreiranje skladišta", "terminal.connectionLost.abnormalClose": "WebSocket zatvoren nenormalno: {{code}}", + + "presets.title": "Unaprijed postavljene", + "presets.manage": "Upravljanje unaprijed postavljenim", + "presets.count": "{{count}} unaprijed postavljenih", + "presets.empty": "Nema unaprijed postavljenih", + "presets.empty.hint": "Kliknite \"Dodaj\" da kreirate prvu unaprijed postavljenu", + "presets.add": "Dodaj", + "presets.name": "Naziv", + "presets.content": "Sadržaj", + "presets.content.placeholder": "Sadržaj (podržava {date} {time} {file} {code} {text} varijable)", + "presets.moveUp": "Pomjeri gore", + "presets.moveDown": "Pomjeri dolje", + "presets.edit": "Uredi", + "presets.delete": "Obriši", + "presets.variables.title": "Unesite varijable za \"{{name}}\"", + "presets.variables.next": "Sljedeće", + "presets.variables.insert": "Umetni", } diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index d52aca823eea..1f6a6d884811 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -927,4 +927,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "Kunne ikke oprette vedvarende projektikon", "error.childStore.storeCreateFailed": "Kunne ikke oprette lager", "terminal.connectionLost.abnormalClose": "WebSocket lukkede unormalt: {{code}}", + + "presets.title": "Forudindstillinger", + "presets.manage": "Administrer forudindstillinger", + "presets.count": "{{count}} forudindstillinger", + "presets.empty": "Ingen forudindstillinger", + "presets.empty.hint": "Klik på \"Tilføj\" for at oprette din første forudindstilling", + "presets.add": "Tilføj", + "presets.name": "Navn", + "presets.content": "Indhold", + "presets.content.placeholder": "Indhold (understøtter {date} {time} {file} {code} {text} variabler)", + "presets.moveUp": "Flyt op", + "presets.moveDown": "Flyt ned", + "presets.edit": "Rediger", + "presets.delete": "Slet", + "presets.variables.title": "Indtast variabler for \"{{name}}\"", + "presets.variables.next": "Næste", + "presets.variables.insert": "Indsæt", } diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 4d5a64990287..c6aba65220fd 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -869,4 +869,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "Dauerhaftes Projekticon konnte nicht erstellt werden", "error.childStore.storeCreateFailed": "Speicher konnte nicht erstellt werden", "terminal.connectionLost.abnormalClose": "WebSocket abnormal geschlossen: {{code}}", + + "presets.title": "Voreinstellungen", + "presets.manage": "Voreinstellungen verwalten", + "presets.count": "{{count}} Voreinstellungen", + "presets.empty": "Keine Voreinstellungen", + "presets.empty.hint": "Klicken Sie auf \"Hinzufügen\", um Ihre erste Voreinstellung zu erstellen", + "presets.add": "Hinzufügen", + "presets.name": "Name", + "presets.content": "Inhalt", + "presets.content.placeholder": "Inhalt (unterstützt {date} {time} {file} {code} {text} Variablen)", + "presets.moveUp": "Nach oben", + "presets.moveDown": "Nach unten", + "presets.edit": "Bearbeiten", + "presets.delete": "Löschen", + "presets.variables.title": "Variablen für \"{{name}}\" eingeben", + "presets.variables.next": "Weiter", + "presets.variables.insert": "Einfügen", } satisfies Partial> diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index eef4d80bfc53..d97f7c5f6699 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -1038,4 +1038,21 @@ export const dict = { "workspace.reset.archived.one": "1 session will be archived.", "workspace.reset.archived.many": "{{count}} sessions will be archived.", "workspace.reset.note": "This will reset the workspace to match the default branch.", + + "presets.title": "Presets", + "presets.manage": "Manage presets", + "presets.count": "{{count}} presets", + "presets.empty": "No presets", + "presets.empty.hint": "Click \"Add\" to create your first preset", + "presets.add": "Add", + "presets.name": "Name", + "presets.content": "Content", + "presets.content.placeholder": "Content (supports {date} {time} {file} {code} {text} variables)", + "presets.moveUp": "Move up", + "presets.moveDown": "Move down", + "presets.edit": "Edit", + "presets.delete": "Delete", + "presets.variables.title": "Enter variables for \"{{name}}\"", + "presets.variables.next": "Next", + "presets.variables.insert": "Insert", } diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index b71fe03e8787..8e85929f2c01 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -940,4 +940,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "Error al crear icono de proyecto persistente", "error.childStore.storeCreateFailed": "Error al crear almacén", "terminal.connectionLost.abnormalClose": "WebSocket cerrado anormalmente: {{code}}", + + "presets.title": "Preajustes", + "presets.manage": "Gestionar preajustes", + "presets.count": "{{count}} preajustes", + "presets.empty": "Sin preajustes", + "presets.empty.hint": "Haga clic en \"Agregar\" para crear su primer preajuste", + "presets.add": "Agregar", + "presets.name": "Nombre", + "presets.content": "Contenido", + "presets.content.placeholder": "Contenido (soporta variables {date} {time} {file} {code} {text})", + "presets.moveUp": "Mover arriba", + "presets.moveDown": "Mover abajo", + "presets.edit": "Editar", + "presets.delete": "Eliminar", + "presets.variables.title": "Ingrese variables para \"{{name}}\"", + "presets.variables.next": "Siguiente", + "presets.variables.insert": "Insertar", } diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 385dbdddefcb..380116b7112b 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -868,4 +868,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "Échec de la création de l'icône de projet persistante", "error.childStore.storeCreateFailed": "Échec de la création du stockage", "terminal.connectionLost.abnormalClose": "WebSocket fermé anormalement : {{code}}", + + "presets.title": "Préréglages", + "presets.manage": "Gérer les préréglages", + "presets.count": "{{count}} préréglages", + "presets.empty": "Aucun préréglage", + "presets.empty.hint": "Cliquez sur \"Ajouter\" pour créer votre premier préréglage", + "presets.add": "Ajouter", + "presets.name": "Nom", + "presets.content": "Contenu", + "presets.content.placeholder": "Contenu (prend en charge les variables {date} {time} {file} {code} {text})", + "presets.moveUp": "Déplacer vers le haut", + "presets.moveDown": "Déplacer vers le bas", + "presets.edit": "Modifier", + "presets.delete": "Supprimer", + "presets.variables.title": "Entrez les variables pour \"{{name}}\"", + "presets.variables.next": "Suivant", + "presets.variables.insert": "Insérer", } diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 34ac398bd5e8..5e4ecdf426de 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -851,4 +851,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "永続プロジェクトアイコンの作成に失敗しました", "error.childStore.storeCreateFailed": "ストアの作成に失敗しました", "terminal.connectionLost.abnormalClose": "WebSocket が異常終了しました: {{code}}", + + "presets.title": "プリセット", + "presets.manage": "プリセット管理", + "presets.count": "プリセット {{count}} 件", + "presets.empty": "プリセットなし", + "presets.empty.hint": "「追加」をクリックして最初のプリセットを作成", + "presets.add": "追加", + "presets.name": "名前", + "presets.content": "内容", + "presets.content.placeholder": "内容({date} {time} {file} {code} {text} 変数対応)", + "presets.moveUp": "上に移動", + "presets.moveDown": "下に移動", + "presets.edit": "編集", + "presets.delete": "削除", + "presets.variables.title": "「{{name}}」の変数を入力", + "presets.variables.next": "次へ", + "presets.variables.insert": "挿入", } diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index f1926f3dd28d..3e197825c6d9 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -846,4 +846,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "영구 프로젝트 아이콘 생성 실패", "error.childStore.storeCreateFailed": "저장소 생성 실패", "terminal.connectionLost.abnormalClose": "WebSocket이 비정상적으로 닫힘: {{code}}", + + "presets.title": "프리셋", + "presets.manage": "프리셋 관리", + "presets.count": "프리셋 {{count}}개", + "presets.empty": "프리셋 없음", + "presets.empty.hint": "\"추가\"를 클릭하여 첫 번째 프리셋을 만드세요", + "presets.add": "추가", + "presets.name": "이름", + "presets.content": "내용", + "presets.content.placeholder": "내용 ({date} {time} {file} {code} {text} 변수 지원)", + "presets.moveUp": "위로 이동", + "presets.moveDown": "아래로 이동", + "presets.edit": "편집", + "presets.delete": "삭제", + "presets.variables.title": "\"{{name}}\"의 변수 입력", + "presets.variables.next": "다음", + "presets.variables.insert": "삽입", } diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index b4adebdb7ce7..52d4372e9d01 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -934,4 +934,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "Kunne ikke opprette vedvarende prosjektikon", "error.childStore.storeCreateFailed": "Kunne ikke opprette lager", "terminal.connectionLost.abnormalClose": "WebSocket lukket unormalt: {{code}}", + + "presets.title": "Forhåndsinnstillinger", + "presets.manage": "Administrer forhåndsinnstillinger", + "presets.count": "{{count}} forhåndsinnstillinger", + "presets.empty": "Ingen forhåndsinnstillinger", + "presets.empty.hint": "Klikk \"Legg til\" for å opprette din første forhåndsinnstilling", + "presets.add": "Legg til", + "presets.name": "Navn", + "presets.content": "Innhold", + "presets.content.placeholder": "Innhold (støtter {date} {time} {file} {code} {text} variabler)", + "presets.moveUp": "Flytt opp", + "presets.moveDown": "Flytt ned", + "presets.edit": "Rediger", + "presets.delete": "Slett", + "presets.variables.title": "Skriv inn variabler for \"{{name}}\"", + "presets.variables.next": "Neste", + "presets.variables.insert": "Sett inn", } satisfies Partial> diff --git a/packages/app/src/i18n/parity.test.ts b/packages/app/src/i18n/parity.test.ts index 8ad1beb56b0c..58b1a162ff0c 100644 --- a/packages/app/src/i18n/parity.test.ts +++ b/packages/app/src/i18n/parity.test.ts @@ -20,6 +20,27 @@ import { dict as tr } from "./tr" const locales = [ar, br, bs, da, de, es, fr, ja, ko, no, pl, ru, uk, th, tr, zh, zht] const keys = ["command.session.previous.unseen", "command.session.next.unseen"] as const +const presetKeys = [ + "presets.title", + "presets.manage", + "presets.count", + "presets.empty", + "presets.empty.hint", + "presets.add", + "presets.name", + "presets.content", + "presets.content.placeholder", + "presets.moveUp", + "presets.moveDown", + "presets.edit", + "presets.delete", + "presets.variables.title", + "presets.variables.next", + "presets.variables.insert", +] as const + +// Keys that may have the same translation as English in some languages +const allowedSameAsEnglish = new Set(["presets.name"]) describe("i18n parity", () => { test("non-English locales translate targeted unseen session keys", () => { @@ -30,4 +51,15 @@ describe("i18n parity", () => { } } }) + + test("non-English locales translate all preset keys", () => { + for (const locale of locales) { + for (const key of presetKeys) { + expect(locale[key]).toBeDefined() + if (!allowedSameAsEnglish.has(key)) { + expect(locale[key]).not.toBe(en[key]) + } + } + } + }) }) diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 02637e574f11..f986962fdeaa 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -855,4 +855,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "Nie udało się utworzyć trwałej ikony projektu", "error.childStore.storeCreateFailed": "Nie udało się utworzyć magazynu", "terminal.connectionLost.abnormalClose": "WebSocket zamknięty nieprawidłowo: {{code}}", + + "presets.title": "Ustawienia wstępne", + "presets.manage": "Zarządzaj ustawieniami wstępnymi", + "presets.count": "{{count}} ustawień wstępnych", + "presets.empty": "Brak ustawień wstępnych", + "presets.empty.hint": "Kliknij \"Dodaj\", aby utworzyć pierwsze ustawienie wstępne", + "presets.add": "Dodaj", + "presets.name": "Nazwa", + "presets.content": "Treść", + "presets.content.placeholder": "Treść (obsługuje zmienne {date} {time} {file} {code} {text})", + "presets.moveUp": "Przesuń w górę", + "presets.moveDown": "Przesuń w dół", + "presets.edit": "Edytuj", + "presets.delete": "Usuń", + "presets.variables.title": "Wprowadź zmienne dla \"{{name}}\"", + "presets.variables.next": "Dalej", + "presets.variables.insert": "Wstaw", } diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index a965acf278de..f0c133e76cc9 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -936,4 +936,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "Не удалось создать постоянный значок проекта", "error.childStore.storeCreateFailed": "Не удалось создать хранилище", "terminal.connectionLost.abnormalClose": "WebSocket закрыт аварийно: {{code}}", + + "presets.title": "Пресеты", + "presets.manage": "Управление пресетами", + "presets.count": "Пресетов: {{count}}", + "presets.empty": "Нет пресетов", + "presets.empty.hint": "Нажмите \"Добавить\", чтобы создать первый пресет", + "presets.add": "Добавить", + "presets.name": "Название", + "presets.content": "Содержание", + "presets.content.placeholder": "Содержание (поддерживает переменные {date} {time} {file} {code} {text})", + "presets.moveUp": "Переместить вверх", + "presets.moveDown": "Переместить вниз", + "presets.edit": "Редактировать", + "presets.delete": "Удалить", + "presets.variables.title": "Введите переменные для \"{{name}}\"", + "presets.variables.next": "Далее", + "presets.variables.insert": "Вставить", } diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 00df86a34d09..35258b3613cd 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -923,4 +923,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "ไม่สามารถสร้างไอคอนโปรเจกต์ถาวร", "error.childStore.storeCreateFailed": "ไม่สามารถสร้างที่เก็บ", "terminal.connectionLost.abnormalClose": "WebSocket ปิดอย่างผิดปกติ: {{code}}", + + "presets.title": "ค่าที่ตั้งไว้", + "presets.manage": "จัดการค่าที่ตั้งไว้", + "presets.count": "ค่าที่ตั้งไว้ {{count}} รายการ", + "presets.empty": "ไม่มีค่าที่ตั้งไว้", + "presets.empty.hint": "คลิก \"เพิ่ม\" เพื่อสร้างค่าที่ตั้งไว้แรกของคุณ", + "presets.add": "เพิ่ม", + "presets.name": "ชื่อ", + "presets.content": "เนื้อหา", + "presets.content.placeholder": "เนื้อหา (รองรับตัวแปร {date} {time} {file} {code} {text})", + "presets.moveUp": "เลื่อนขึ้น", + "presets.moveDown": "เลื่อนลง", + "presets.edit": "แก้ไข", + "presets.delete": "ลบ", + "presets.variables.title": "ป้อนตัวแปรสำหรับ \"{{name}}\"", + "presets.variables.next": "ถัดไป", + "presets.variables.insert": "แทรก", } diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index 27a6e03e367c..8146e772b865 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -942,4 +942,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "Kalıcı proje simgesi oluşturulamadı", "error.childStore.storeCreateFailed": "Depo oluşturulamadı", "terminal.connectionLost.abnormalClose": "WebSocket anormal şekilde kapandı: {{code}}", + + "presets.title": "Ön ayarlar", + "presets.manage": "Ön ayarları yönet", + "presets.count": "{{count}} ön ayar", + "presets.empty": "Ön ayar yok", + "presets.empty.hint": "İlk ön ayarınızı oluşturmak için \"Ekle\" tıklayın", + "presets.add": "Ekle", + "presets.name": "Ad", + "presets.content": "İçerik", + "presets.content.placeholder": "İçerik ({date} {time} {file} {code} {text} değişkenlerini destekler)", + "presets.moveUp": "Yukarı taşı", + "presets.moveDown": "Aşağı taşı", + "presets.edit": "Düzenle", + "presets.delete": "Sil", + "presets.variables.title": "\"{{name}}\" için değişkenleri girin", + "presets.variables.next": "Sonraki", + "presets.variables.insert": "Ekle", } satisfies Partial> diff --git a/packages/app/src/i18n/uk.ts b/packages/app/src/i18n/uk.ts index 948c9a6cd449..49cd0e8ef365 100644 --- a/packages/app/src/i18n/uk.ts +++ b/packages/app/src/i18n/uk.ts @@ -966,4 +966,21 @@ export const dict = { "workspace.reset.archived.one": "1 сесію буде заархівовано.", "workspace.reset.archived.many": "{{count}} сесій буде заархівовано.", "workspace.reset.note": "Це скине робочу область, щоб вона відповідала гілці за замовчуванням.", + + "presets.title": "Пресети", + "presets.manage": "Керування пресетами", + "presets.count": "Пресетів: {{count}}", + "presets.empty": "Немає пресетів", + "presets.empty.hint": "Натисніть \"Додати\", щоб створити перший пресет", + "presets.add": "Додати", + "presets.name": "Назва", + "presets.content": "Вміст", + "presets.content.placeholder": "Вміст (підтримує змінні {date} {time} {file} {code} {text})", + "presets.moveUp": "Перемістити вгору", + "presets.moveDown": "Перемістити вниз", + "presets.edit": "Редагувати", + "presets.delete": "Видалити", + "presets.variables.title": "Введіть змінні для \"{{name}}\"", + "presets.variables.next": "Далі", + "presets.variables.insert": "Вставити", } diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 663f2ccf768f..359d03fd73ff 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -928,4 +928,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败", "error.childStore.storeCreateFailed": "创建存储失败", "terminal.connectionLost.abnormalClose": "WebSocket 异常关闭:{{code}}", + + "presets.title": "常用语", + "presets.manage": "管理常用语", + "presets.count": "共 {{count}} 条常用语", + "presets.empty": "暂无常用语", + "presets.empty.hint": "点击「新增」添加你的第一条常用语", + "presets.add": "新增", + "presets.name": "名称", + "presets.content": "内容", + "presets.content.placeholder": "内容(支持 {date} {time} {file} {code} {text} 等变量)", + "presets.moveUp": "上移", + "presets.moveDown": "下移", + "presets.edit": "编辑", + "presets.delete": "删除", + "presets.variables.title": "输入「{{name}}」的变量", + "presets.variables.next": "下一个", + "presets.variables.insert": "插入", } satisfies Partial> diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index edd6f7bc0647..fab5b9ae3d03 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -914,4 +914,21 @@ export const dict = { "error.childStore.persistedProjectIconCreateFailed": "建立持續性專案圖示失敗", "error.childStore.storeCreateFailed": "建立儲存區失敗", "terminal.connectionLost.abnormalClose": "WebSocket 異常關閉:{{code}}", + + "presets.title": "常用語", + "presets.manage": "管理常用語", + "presets.count": "共 {{count}} 條常用語", + "presets.empty": "暫無常用語", + "presets.empty.hint": "點擊「新增」新增你的第一條常用語", + "presets.add": "新增", + "presets.name": "名稱", + "presets.content": "內容", + "presets.content.placeholder": "內容(支援 {date} {time} {file} {code} {text} 等變數)", + "presets.moveUp": "上移", + "presets.moveDown": "下移", + "presets.edit": "編輯", + "presets.delete": "刪除", + "presets.variables.title": "輸入「{{name}}」的變數", + "presets.variables.next": "下一個", + "presets.variables.insert": "插入", } satisfies Partial>