diff --git a/apps/web/components/settings/editor-form.tsx b/apps/web/components/settings/editor-form.tsx index b5aab97e56..c6da80988b 100644 --- a/apps/web/components/settings/editor-form.tsx +++ b/apps/web/components/settings/editor-form.tsx @@ -6,9 +6,13 @@ import { Input } from "@kandev/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select"; import type { EditorOption } from "@/lib/types/http"; import { useSettingsSaveContributor } from "./settings-save-provider"; +import { useTranslation } from "react-i18next"; type CustomKind = "custom_command" | "custom_remote_ssh" | "custom_hosted_url"; +/** The `t` shape these helpers need; they are called from render, never at import. */ +type TranslateKey = (key: string) => string; + export type EditorFormState = { name: string; kind: CustomKind; @@ -20,16 +24,31 @@ export type EditorFormState = { enabled: boolean; }; -const CUSTOM_KIND_OPTIONS: Array<{ value: CustomKind; label: string }> = [ - { value: "custom_command", label: "Command" }, - { value: "custom_remote_ssh", label: "VS Code Remote SSH" }, - { value: "custom_hosted_url", label: "Hosted URL" }, +/** + * Holds catalog KEYS, not copy: a module-scope table is evaluated once at import, + * so a `t()` here would freeze on the boot locale and never follow a switch. + * `value` is the persisted editor kind and a string-literal union — it is + * compared with `===`, used as a ``, and sent to the API, so it + * must never be translated. + */ +const CUSTOM_KIND_OPTIONS: Array<{ value: CustomKind; labelKey: string }> = [ + { value: "custom_command", labelKey: "settings:command" }, + { value: "custom_remote_ssh", labelKey: "settings:vsCodeRemoteSsh" }, + { value: "custom_hosted_url", labelKey: "settings:hostedUrl" }, ]; +/** Placeholder tokens the launcher substitutes — identifiers, not copy. */ const PLACEHOLDER_HINT = "{cwd} {file} {rel} {line} {column}"; +/** Example shell command shown in the field; a value the user types verbatim. */ +const COMMAND_EXAMPLE = "code --goto {file}:{line}"; +/** Example of the URL this kind builds — a URL, not translatable copy. */ +const REMOTE_SSH_EXAMPLE = "vscode://vscode-remote/ssh-remote+user@host:/path/file:line"; +/** Accepted URL schemes. The user types one of these verbatim into the field. */ +const EDITOR_SCHEME_EXAMPLES = "vscode, cursor"; -export function getCustomKindLabel(kind: string) { - return CUSTOM_KIND_OPTIONS.find((option) => option.value === kind)?.label ?? "Custom"; +export function getCustomKindLabel(t: TranslateKey, kind: string) { + const option = CUSTOM_KIND_OPTIONS.find((candidate) => candidate.value === kind); + return option ? t(option.labelKey) : t("settings:customEditorType"); } export function isCustomEditor(editor: EditorOption) { @@ -44,23 +63,28 @@ function editorConfigValue(editor: EditorOption, key: string) { return typeof value === "string" ? value : ""; } -export function getCustomEditorSummary(editor: EditorOption) { +/** + * The summary line is either the editor's own configuration (a command, a URL, a + * host — user data, shown verbatim) or, when that is empty, the kind label, + * which is copy. Only the latter goes through `t`. + */ +export function getCustomEditorSummary(t: TranslateKey, editor: EditorOption) { switch (editor.kind) { case "custom_command": { - return editorConfigValue(editor, "command") || getCustomKindLabel(editor.kind); + return editorConfigValue(editor, "command") || getCustomKindLabel(t, editor.kind); } case "custom_hosted_url": { - return editorConfigValue(editor, "url") || getCustomKindLabel(editor.kind); + return editorConfigValue(editor, "url") || getCustomKindLabel(t, editor.kind); } case "custom_remote_ssh": { const host = editorConfigValue(editor, "host"); const user = editorConfigValue(editor, "user"); if (host && user) return `${user}@${host}`; if (host) return host; - return getCustomKindLabel(editor.kind); + return getCustomKindLabel(t, editor.kind); } default: - return getCustomKindLabel(editor.kind); + return getCustomKindLabel(t, editor.kind); } } @@ -166,6 +190,7 @@ function EditorKindFields({ baseline: EditorFormState; setField: (key: K, value: EditorFormState[K]) => void; }) { + const { t } = useTranslation(); if (state.kind === "custom_command") { return (
@@ -173,9 +198,11 @@ function EditorKindFields({ value={state.command} data-settings-dirty={state.command !== baseline.command} onChange={(event) => setField("command", event.target.value)} - placeholder="code --goto {file}:{line}" + placeholder={COMMAND_EXAMPLE} /> -

Supports placeholders: {PLACEHOLDER_HINT}

+

+ {t("settings:supportsPlaceholders", { placeholders: PLACEHOLDER_HINT })} +

); } @@ -192,17 +219,16 @@ function EditorKindFields({ value={state.user} data-settings-dirty={state.user !== baseline.user} onChange={(event) => setField("user", event.target.value)} - placeholder="optional username" + placeholder={t("settings:optionalUsername")} /> setField("scheme", event.target.value)} - placeholder="optional scheme (vscode, cursor)" + placeholder={t("settings:optionalScheme", { schemes: EDITOR_SCHEME_EXAMPLES })} />

- Opens a VS Code Remote SSH URL using the scheme (example:{" "} - vscode://vscode-remote/ssh-remote+user@host:/path/file:line). + {t("settings:opensAVsCodeRemoteSsh", { example: REMOTE_SSH_EXAMPLE })}

); @@ -220,6 +246,35 @@ function EditorKindFields({ return null; } +function EditorKindSelect({ + value, + isDirty, + onChange, +}: { + value: CustomKind; + isDirty: boolean; + onChange: (kind: CustomKind) => void; +}) { + const { t } = useTranslation(); + return ( + + ); +} + export function EditorForm({ title, initialState, @@ -231,6 +286,7 @@ export function EditorForm({ coordinatedSaveId, dirtyWhenMounted = false, }: EditorFormProps) { + const { t } = useTranslation(); const [state, setState] = useState(initialState); const [baseline, setBaseline] = useState(initialState); const stateRef = useRef(state); @@ -271,7 +327,7 @@ export function EditorForm({ revision, isDirty: isCoordinatedDirty, canSave: isValid, - invalidReason: isValid ? undefined : "Complete the required editor fields before saving.", + invalidReason: isValid ? undefined : t("settings:completeTheRequiredEditorFieldsBefore"), save: async () => { const submitted = normalizedState; await onSave(submitted); @@ -291,25 +347,17 @@ export function EditorForm({ value={state.name} data-settings-dirty={state.name !== baseline.name} onChange={(event) => setField("name", event.target.value)} - placeholder="Editor name" + placeholder={t("settings:editorName")} + /> + setField("kind", kind)} /> -
{!coordinatedSaveId && (
diff --git a/apps/web/components/settings/editors-settings.tsx b/apps/web/components/settings/editors-settings.tsx index 697887187f..43ab0d9bab 100644 --- a/apps/web/components/settings/editors-settings.tsx +++ b/apps/web/components/settings/editors-settings.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useMemo } from "react"; +import { useEffect, useMemo } from "react"; import { IconEdit, IconTrash, @@ -31,6 +31,7 @@ import type { RequestStatus } from "@/lib/http/use-request"; import { useEditorsSettingsState, useLspConfigActions, + useLspLanguageToggles, useApplyEditors, useEditorRequests, useSaveRequest, @@ -41,40 +42,17 @@ import { type EditorsSettingsState, } from "@/components/settings/editors-settings-state"; import { isDraftEntryDirty, isEditorsSettingsDirty } from "./settings-dirty"; +import { LSP_LANGUAGE_OPTIONS } from "./lsp-language-options"; +import { Trans, useTranslation } from "react-i18next"; -const LSP_LANGUAGE_OPTIONS = [ - { - id: "typescript", - label: "TypeScript / JavaScript", - binary: "typescript-language-server", - docsUrl: - "https://github.com/typescript-language-server/typescript-language-server#workspace-configuration", - installHint: - "Installs typescript-language-server and typescript via npm into ~/.kandev/lsp-servers/", - }, - { - id: "go", - label: "Go", - binary: "gopls", - docsUrl: "https://github.com/golang/tools/blob/master/gopls/doc/settings.md", - installHint: 'Runs "go install golang.org/x/tools/gopls@latest". Requires Go to be installed.', - }, - { - id: "rust", - label: "Rust", - binary: "rust-analyzer", - docsUrl: "https://rust-analyzer.github.io/book/configuration.html", - installHint: - "Downloads the rust-analyzer binary from GitHub releases into ~/.kandev/lsp-servers/", - }, - { - id: "python", - label: "Python", - binary: "pyright-langserver", - docsUrl: "https://microsoft.github.io/pyright/#/settings", - installHint: "Installs pyright via npm into ~/.kandev/lsp-servers/", - }, -]; +/** + * Code identifiers rendered inside `` copy. They are passed as + * interpolation VALUES rather than left in the tag body: text inside a `` tag + * is part of the message a translator edits, and translating a command name or + * an LSP method breaks the thing it names. + */ +const NPM_INSTALL_COMMAND = "npm install"; +const LSP_CONFIG_METHOD = "workspace/configuration"; type LspLanguageCardsProps = { lspAutoStartLanguages: string[]; @@ -93,17 +71,22 @@ function LspLanguageCards({ toggleAutoStart, toggleAutoInstall, }: LspLanguageCardsProps) { + const { t } = useTranslation(); return (
-
Language Servers
+
{t("settings:languageServers")}
- Auto-start language servers when opening files to get diagnostics, hover info, and - go-to-definition. You can also toggle each server on/off per file. + {t("settings:autoStartLanguageServersWhenOpening")}
- When enabled, install your project's dependencies (e.g.{" "} - npm install via repository - setup scripts) to avoid missing type errors. + + When enabled, install your project's dependencies (e.g.{" "} + {NPM_INSTALL_COMMAND} via + repository setup scripts) to avoid missing type errors. +
@@ -123,7 +106,7 @@ function LspLanguageCards({
{lang.binary}
- Auto-start + {t("settings:autoStart")} toggleAutoStart(lang.id, checked === true)} @@ -142,14 +125,14 @@ function LspLanguageCards({ htmlFor={`lsp-install-${lang.id}`} className="text-xs text-muted-foreground cursor-pointer" > - Auto-install if not found + {t("settings:autoInstallIfNotFound")} - {lang.installHint} + {t(lang.installHintKey, lang.installHintValues)}
@@ -178,14 +161,22 @@ function LspServerConfigSection({ setExpandedConfigLang, updateLspConfigString, }: LspServerConfigSectionProps) { + const { t } = useTranslation(); return (
-
Server Configuration
+
+ {t("settings:serverConfiguration")} +
- Override settings sent to each language server via{" "} - workspace/configuration. JSON - format. + + Override settings sent to each language server via{" "} + {LSP_CONFIG_METHOD}. JSON + format. +
{LSP_LANGUAGE_OPTIONS.map((lang) => { @@ -210,7 +201,7 @@ function LspServerConfigSection({ {lang.label} {configStr.trim() && ( - custom + {t("settings:lspConfigCustomBadge")} )}
@@ -222,7 +213,7 @@ function LspServerConfigSection({
{hasDefaults && (
- Defaults:{" "} + {t("settings:defaults")}{" "} {JSON.stringify(defaultConfig)}
)} @@ -232,7 +223,7 @@ function LspServerConfigSection({ rel="noopener noreferrer" className="inline-flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground transition-colors" > - View available settings + {t("settings:viewAvailableSettings")}