feat(i18n): localize Settings → General → Editors (#2146)

Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
João Salavisa
2026-08-02 03:12:48 +02:00
committed by GitHub
parent e4c4efec18
commit 4675391626
8 changed files with 529 additions and 138 deletions
+81 -33
View File
@@ -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 `<SelectItem value>`, 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: <K extends keyof EditorFormState>(key: K, value: EditorFormState[K]) => void;
}) {
const { t } = useTranslation();
if (state.kind === "custom_command") {
return (
<div className="space-y-2">
@@ -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}
/>
<p className="text-xs text-muted-foreground">Supports placeholders: {PLACEHOLDER_HINT}</p>
<p className="text-xs text-muted-foreground">
{t("settings:supportsPlaceholders", { placeholders: PLACEHOLDER_HINT })}
</p>
</div>
);
}
@@ -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")}
/>
<Input
value={state.scheme}
data-settings-dirty={state.scheme !== baseline.scheme}
onChange={(event) => setField("scheme", event.target.value)}
placeholder="optional scheme (vscode, cursor)"
placeholder={t("settings:optionalScheme", { schemes: EDITOR_SCHEME_EXAMPLES })}
/>
<p className="text-xs text-muted-foreground">
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 })}
</p>
</div>
);
@@ -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 (
<Select value={value} onValueChange={(next) => onChange(next as CustomKind)}>
<SelectTrigger data-settings-dirty={isDirty}>
<SelectValue placeholder={t("settings:editorType")} />
</SelectTrigger>
<SelectContent>
<div className="px-2 py-1.5 text-xs text-muted-foreground border-b">
{t("settings:editorType")}
</div>
{CUSTOM_KIND_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{t(option.labelKey)}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
export function EditorForm({
title,
initialState,
@@ -231,6 +286,7 @@ export function EditorForm({
coordinatedSaveId,
dirtyWhenMounted = false,
}: EditorFormProps) {
const { t } = useTranslation();
const [state, setState] = useState<EditorFormState>(initialState);
const [baseline, setBaseline] = useState<EditorFormState>(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")}
/>
<EditorKindSelect
value={state.kind}
isDirty={state.kind !== baseline.kind}
onChange={(kind) => setField("kind", kind)}
/>
<Select value={state.kind} onValueChange={(value) => setField("kind", value as CustomKind)}>
<SelectTrigger data-settings-dirty={state.kind !== baseline.kind}>
<SelectValue placeholder="Editor type" />
</SelectTrigger>
<SelectContent>
<div className="px-2 py-1.5 text-xs text-muted-foreground border-b">Editor type</div>
{CUSTOM_KIND_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<EditorKindFields state={state} baseline={baseline} setField={setField} />
<div className="flex items-center justify-end gap-2">
<Button type="button" variant="outline" onClick={onCancel} disabled={isSaving}>
Cancel
{t("settings:cancel")}
</Button>
{!coordinatedSaveId && (
<Button
@@ -0,0 +1,174 @@
import { act, cleanup, render, renderHook, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { activateLocale, DEFAULT_LOCALE, t } from "@/lib/i18n";
import type { EditorOption } from "@/lib/types/http";
import { getCustomEditorSummary, getCustomKindLabel } from "./editor-form";
import {
buildDefaultEditorOptions,
useLspConfigActions,
useLspLanguageToggles,
} from "./editors-settings-state";
afterEach(async () => {
cleanup();
// Leave the shared instance on `en` for the suites that assert English copy.
await activateLocale(DEFAULT_LOCALE);
});
const translate = (key: string) => `t(${key})`;
function editor(overrides: Partial<EditorOption> = {}): EditorOption {
return {
id: "editor-1",
name: "VS Code",
kind: "custom_command",
config: {},
enabled: true,
installed: true,
...overrides,
} as EditorOption;
}
describe("getCustomKindLabel", () => {
it("resolves each known kind through the catalog", () => {
expect(getCustomKindLabel(translate, "custom_command")).toBe("t(settings:command)");
expect(getCustomKindLabel(translate, "custom_remote_ssh")).toBe("t(settings:vsCodeRemoteSsh)");
expect(getCustomKindLabel(translate, "custom_hosted_url")).toBe("t(settings:hostedUrl)");
});
it("falls back to the generic label for an unknown kind", () => {
expect(getCustomKindLabel(translate, "built_in")).toBe("t(settings:customEditorType)");
});
});
describe("getCustomEditorSummary", () => {
it("shows the configured value verbatim, never translated", () => {
expect(getCustomEditorSummary(translate, editor({ config: { command: "code {file}" } }))).toBe(
"code {file}",
);
expect(
getCustomEditorSummary(
translate,
editor({ kind: "custom_hosted_url", config: { url: "https://code.example.com" } }),
),
).toBe("https://code.example.com");
expect(
getCustomEditorSummary(
translate,
editor({ kind: "custom_remote_ssh", config: { host: "box.example.com", user: "ada" } }),
),
).toBe("ada@box.example.com");
});
it("falls back to the translated kind label when nothing is configured", () => {
expect(getCustomEditorSummary(translate, editor({ config: {} }))).toBe("t(settings:command)");
});
// A user without a host is not addressable, so the summary shows the kind
// rather than a bare username that looks like a target.
it("falls back to the kind label when host is absent, even if user is set", () => {
expect(
getCustomEditorSummary(
translate,
editor({ kind: "custom_remote_ssh", config: { user: "ada" } }),
),
).toBe("t(settings:vsCodeRemoteSsh)");
});
});
describe("buildDefaultEditorOptions", () => {
it("keys options by editor id and translates only the badge", () => {
const options = buildDefaultEditorOptions(
[editor({ id: "vscode", kind: "built_in", name: "VS Code", installed: false })],
"vscode",
// The real `t`, so a missing catalog entry fails here rather than shipping.
t,
);
// `value` is submitted and compared with `===` — it must stay the raw id.
expect(options[0].value).toBe("vscode");
expect(options[0].label).toBe("VS Code");
render(<>{options[0].renderLabel?.()}</>);
expect(screen.getByText("Not installed")).toBeTruthy();
});
it("keeps the option value stable when the locale changes", async () => {
const editors = [editor({ id: "vscode", kind: "built_in" })];
const english = buildDefaultEditorOptions(editors, "vscode", (key) => key);
await act(async () => {
await activateLocale("pseudo");
});
const pseudo = buildDefaultEditorOptions(editors, "vscode", (key) => key);
expect(pseudo[0].value).toBe(english[0].value);
expect(pseudo[0].value).toBe("vscode");
});
});
describe("useLspLanguageToggles", () => {
function trackedState() {
const autoStart: Array<(prev: string[]) => string[]> = [];
const autoInstall: Array<(prev: string[]) => string[]> = [];
return {
calls: { autoStart, autoInstall },
state: {
setLspAutoStartLanguages: (updater: (prev: string[]) => string[]) =>
autoStart.push(updater),
setLspAutoInstallLanguages: (updater: (prev: string[]) => string[]) =>
autoInstall.push(updater),
} as unknown as Parameters<typeof useLspLanguageToggles>[0],
};
}
it("adds a language when checked and removes it when unchecked", () => {
const { calls, state } = trackedState();
const { result } = renderHook(() => useLspLanguageToggles(state));
result.current.toggleAutoStart("go", true);
expect(calls.autoStart[0](["rust"])).toEqual(["rust", "go"]);
result.current.toggleAutoStart("go", false);
expect(calls.autoStart[1](["rust", "go"])).toEqual(["rust"]);
result.current.toggleAutoInstall("python", true);
expect(calls.autoInstall[0]([])).toEqual(["python"]);
});
});
describe("useLspConfigActions", () => {
function collectErrors() {
let errors: Record<string, string> = {};
let configs: Record<string, string> = {};
const hook = renderHook(() =>
useLspConfigActions(
(updater) => {
configs = updater(configs);
},
(updater) => {
errors = updater(errors);
},
),
);
return { hook, read: () => errors };
}
it("reports invalid JSON through the catalog rather than a literal", () => {
const { hook, read } = collectErrors();
hook.result.current.updateLspConfigString("go", "{ not json");
expect(read().go).toBe("Invalid JSON");
});
it("rejects a non-object JSON value", () => {
const { hook, read } = collectErrors();
hook.result.current.updateLspConfigString("go", "[1, 2]");
expect(read().go).toBe("Must be a JSON object");
});
it("clears the error once the value parses", () => {
const { hook, read } = collectErrors();
hook.result.current.updateLspConfigString("go", "{ not json");
hook.result.current.updateLspConfigString("go", '{ "gopls": {} }');
expect(read().go).toBeUndefined();
});
});
@@ -17,6 +17,7 @@ import {
isCustomEditor,
} from "@/components/settings/editor-form";
import { Badge } from "@kandev/ui/badge";
import { useTranslation } from "react-i18next";
export function useEditorsSettingsState() {
const setEditors = useAppStore((state) => state.setEditors);
@@ -99,6 +100,7 @@ export function useLspConfigActions(
setLspConfigStrings: (updater: (prev: Record<string, string>) => Record<string, string>) => void,
setLspConfigErrors: (updater: (prev: Record<string, string>) => Record<string, string>) => void,
) {
const { t } = useTranslation();
const clearLspConfigError = useCallback(
(langId: string) => {
setLspConfigErrors((prev) => {
@@ -127,20 +129,42 @@ export function useLspConfigActions(
try {
const parsed = JSON.parse(value);
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
setLspConfigErrors((prev) => ({ ...prev, [langId]: "Must be a JSON object" }));
setLspConfigErrors((prev) => ({ ...prev, [langId]: t("settings:mustBeAJsonObject") }));
} else {
clearLspConfigError(langId);
}
} catch {
setLspConfigErrors((prev) => ({ ...prev, [langId]: "Invalid JSON" }));
setLspConfigErrors((prev) => ({ ...prev, [langId]: t("settings:invalidJson") }));
}
},
[setLspConfigStrings, setLspConfigErrors, clearLspConfigError],
[setLspConfigStrings, setLspConfigErrors, clearLspConfigError, t],
);
return { updateLspConfigString };
}
/** Add/remove a language id in the auto-start and auto-install sets. */
export function useLspLanguageToggles(state: EditorsSettingsState) {
const { setLspAutoStartLanguages, setLspAutoInstallLanguages } = state;
const toggleAutoStart = useCallback(
(langId: string, checked: boolean) => {
setLspAutoStartLanguages((prev) =>
checked ? [...prev, langId] : prev.filter((id) => id !== langId),
);
},
[setLspAutoStartLanguages],
);
const toggleAutoInstall = useCallback(
(langId: string, checked: boolean) => {
setLspAutoInstallLanguages((prev) =>
checked ? [...prev, langId] : prev.filter((id) => id !== langId),
);
},
[setLspAutoInstallLanguages],
);
return { toggleAutoStart, toggleAutoInstall };
}
export function parseLspConfigStrings(
lspConfigStrings: Record<string, string>,
): Record<string, Record<string, unknown>> | null {
@@ -158,9 +182,15 @@ export function parseLspConfigStrings(
return result;
}
/**
* `t` is a parameter rather than a module import: the option list is built in a
* `useMemo`, so passing the caller's `t` both keeps the copy out of module scope
* and gives the memo a dependency that changes on a locale switch.
*/
export function buildDefaultEditorOptions(
availableEditors: EditorOption[],
defaultEditorId: string,
t: (key: string) => string,
): ComboboxOption[] {
if (availableEditors.length === 0) return [];
const selected = defaultEditorId ? availableEditors.filter((e) => e.id === defaultEditorId) : [];
@@ -174,11 +204,11 @@ export function buildDefaultEditorOptions(
<span className="truncate">{editor.name}</span>
{editor.kind === "built_in" ? (
<Badge variant={editor.installed ? "secondary" : "outline"} className="ml-auto">
{editor.installed ? "Installed" : "Not installed"}
{editor.installed ? t("settings:installed") : t("settings:notInstalled")}
</Badge>
) : (
<Badge variant="secondary" className="ml-auto">
{getCustomKindLabel(editor.kind)}
{getCustomKindLabel(t, editor.kind)}
</Badge>
)}
</div>
@@ -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 `<Trans>` copy. They are passed as
* interpolation VALUES rather than left in the tag body: text inside a `<n>` 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 (
<div className="space-y-3">
<div>
<div className="text-sm font-medium text-foreground">Language Servers</div>
<div className="text-sm font-medium text-foreground">{t("settings:languageServers")}</div>
<div className="text-xs text-muted-foreground">
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")}
<br />
When enabled, install your project&apos;s dependencies (e.g.{" "}
<code className="text-[11px] bg-muted px-1 rounded">npm install</code> via repository
setup scripts) to avoid missing type errors.
<Trans
i18nKey="settings:whenEnabledInstallYourProjectS"
values={{ command: NPM_INSTALL_COMMAND }}
>
When enabled, install your project&apos;s dependencies (e.g.{" "}
<code className="text-[11px] bg-muted px-1 rounded">{NPM_INSTALL_COMMAND}</code> via
repository setup scripts) to avoid missing type errors.
</Trans>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
@@ -123,7 +106,7 @@ function LspLanguageCards({
<div className="text-xs text-muted-foreground">{lang.binary}</div>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Auto-start</span>
<span className="text-xs text-muted-foreground">{t("settings:autoStart")}</span>
<Switch
checked={lspAutoStartLanguages.includes(lang.id)}
onCheckedChange={(checked) => 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")}
</label>
<Tooltip>
<TooltipTrigger asChild>
<IconInfoCircle className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[260px] text-xs">
{lang.installHint}
{t(lang.installHintKey, lang.installHintValues)}
</TooltipContent>
</Tooltip>
</div>
@@ -178,14 +161,22 @@ function LspServerConfigSection({
setExpandedConfigLang,
updateLspConfigString,
}: LspServerConfigSectionProps) {
const { t } = useTranslation();
return (
<div className="space-y-3">
<div>
<div className="text-sm font-medium text-foreground">Server Configuration</div>
<div className="text-sm font-medium text-foreground">
{t("settings:serverConfiguration")}
</div>
<div className="text-xs text-muted-foreground">
Override settings sent to each language server via{" "}
<code className="text-[11px] bg-muted px-1 rounded">workspace/configuration</code>. JSON
format.
<Trans
i18nKey="settings:overrideSettingsSentToEachLanguage"
values={{ method: LSP_CONFIG_METHOD }}
>
Override settings sent to each language server via{" "}
<code className="text-[11px] bg-muted px-1 rounded">{LSP_CONFIG_METHOD}</code>. JSON
format.
</Trans>
</div>
</div>
{LSP_LANGUAGE_OPTIONS.map((lang) => {
@@ -210,7 +201,7 @@ function LspServerConfigSection({
<span className="text-sm text-foreground">{lang.label}</span>
{configStr.trim() && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
custom
{t("settings:lspConfigCustomBadge")}
</Badge>
)}
</div>
@@ -222,7 +213,7 @@ function LspServerConfigSection({
<div className="border-t border-border/60 px-4 py-3 space-y-2">
{hasDefaults && (
<div className="text-[11px] text-muted-foreground">
Defaults:{" "}
{t("settings:defaults")}{" "}
<code className="bg-muted px-1 rounded">{JSON.stringify(defaultConfig)}</code>
</div>
)}
@@ -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")}
<IconExternalLink className="h-3 w-3" />
</a>
<Textarea
@@ -286,6 +277,7 @@ function CustomEditorRow({
updateRequest,
deleteRequest,
}: CustomEditorRowProps) {
const { t } = useTranslation();
return (
<EditableCard
key={editor.id}
@@ -295,12 +287,12 @@ function CustomEditorRow({
onClose={() => setEditingId(null)}
renderEdit={({ close }) => (
<EditorForm
title={`Edit ${editor.name}`}
title={t("settings:editEditor", { name: editor.name })}
initialState={formStateFromEditor(editor)}
onCancel={close}
onSave={(state) => updateRequest.run(editor.id, state)}
onSaved={close}
submitLabel="Save changes"
submitLabel={t("settings:saveChanges")}
isSaving={updateRequest.isLoading}
coordinatedSaveId={`custom-editor:${editor.id}`}
/>
@@ -313,7 +305,7 @@ function CustomEditorRow({
<div className="min-w-0">
<div className="font-medium text-sm text-foreground truncate">{editor.name}</div>
<div className="text-xs text-muted-foreground truncate">
{getCustomEditorSummary(editor)}
{getCustomEditorSummary(t, editor)}
</div>
</div>
<div className="flex items-center gap-2">
@@ -328,7 +320,7 @@ function CustomEditorRow({
}}
>
<IconEdit className="h-4 w-4" />
Edit
{t("settings:edit")}
</Button>
<Button
type="button"
@@ -341,7 +333,7 @@ function CustomEditorRow({
}}
>
<IconTrash className="h-4 w-4" />
Remove
{t("settings:remove")}
</Button>
</div>
</div>
@@ -360,22 +352,23 @@ function CustomEditorsList({
updateRequest,
deleteRequest,
}: CustomEditorsListProps) {
const { t } = useTranslation();
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="text-sm font-medium text-foreground">Custom Editors</div>
<div className="text-sm font-medium text-foreground">{t("settings:customEditors")}</div>
<Button type="button" variant="outline" onClick={() => setIsAdding(true)}>
Add custom editor
{t("settings:addCustomEditor")}
</Button>
</div>
{isAdding && (
<EditorForm
title="New custom editor"
title={t("settings:newCustomEditor")}
initialState={defaultFormState()}
onCancel={() => setIsAdding(false)}
onSave={(state) => createRequest.run(state)}
onSaved={() => setIsAdding(false)}
submitLabel="Add editor"
submitLabel={t("settings:addEditor")}
isSaving={createRequest.isLoading}
coordinatedSaveId="custom-editor:new"
dirtyWhenMounted
@@ -384,7 +377,7 @@ function CustomEditorsList({
<div className="space-y-3">
{customEditors.length === 0 && !isAdding && (
<div className="rounded-lg border border-dashed border-border/70 bg-muted/30 p-4 text-sm text-muted-foreground">
No custom editors yet.
{t("settings:noCustomEditorsYet")}
</div>
)}
{customEditors.map((editor) => (
@@ -435,13 +428,14 @@ function EditorsSection({
updateRequest,
deleteRequest,
}: EditorsSectionProps) {
const { t } = useTranslation();
return (
<div className="space-y-6">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Editors
{t("settings:editors")}
</div>
<div className="space-y-2">
<div className="text-sm font-medium text-foreground">Default</div>
<div className="text-sm font-medium text-foreground">{t("settings:default")}</div>
<div
className="min-w-[280px] rounded-md border border-transparent"
data-settings-dirty={defaultEditorId !== baselineDefaultId}
@@ -453,9 +447,9 @@ function EditorsSection({
if (!value) return;
onDefaultEditorChange(value);
}}
placeholder="Select a default editor"
searchPlaceholder="Search editors..."
emptyMessage="No editor found."
placeholder={t("settings:selectADefaultEditor")}
searchPlaceholder={t("settings:searchEditors")}
emptyMessage={t("settings:noEditorFound")}
disabled={availableEditors.length === 0}
/>
</div>
@@ -472,7 +466,9 @@ function EditorsSection({
/>
{builtInEditors.length > 0 && (
<div className="space-y-2">
<div className="text-sm font-medium text-foreground">Supported Editors</div>
<div className="text-sm font-medium text-foreground">
{t("settings:supportedEditors")}
</div>
<div className="grid gap-2 md:grid-cols-2">
{builtInEditors.map((editor) => (
<div
@@ -481,7 +477,7 @@ function EditorsSection({
>
<span className="text-sm text-foreground truncate">{editor.name}</span>
<Badge variant={editor.installed ? "secondary" : "outline"}>
{editor.installed ? "Installed" : "Not installed"}
{editor.installed ? t("settings:installed") : t("settings:notInstalled")}
</Badge>
</div>
))}
@@ -506,40 +502,18 @@ function useSyncEditors(editors: EditorOption[], setEditors: (editors: EditorOpt
}
export function EditorsSettings() {
const { t } = useTranslation();
const state = useEditorsSettingsState();
const {
setLspAutoStartLanguages,
setLspAutoInstallLanguages,
setLspConfigStrings,
setLspConfigErrors,
setEditors,
editors,
} = state;
const { setLspConfigStrings, setLspConfigErrors, setEditors, editors } = state;
const applyEditors = useApplyEditors(state);
const saveDefaultRequest = useSaveRequest(state);
const { createRequest, updateRequest, deleteRequest } = useEditorRequests(state, applyEditors);
const { updateLspConfigString } = useLspConfigActions(setLspConfigStrings, setLspConfigErrors);
const { toggleAutoStart, toggleAutoInstall } = useLspLanguageToggles(state);
const isDirty = isEditorsSettingsDirty(state);
const saveRevision = getEditorsSaveRevision(state);
const hasInvalidConfig = Object.keys(state.lspConfigErrors).length > 0;
const toggleAutoStart = useCallback(
(langId: string, checked: boolean) => {
setLspAutoStartLanguages((prev) =>
checked ? [...prev, langId] : prev.filter((id) => id !== langId),
);
},
[setLspAutoStartLanguages],
);
const toggleAutoInstall = useCallback(
(langId: string, checked: boolean) => {
setLspAutoInstallLanguages((prev) =>
checked ? [...prev, langId] : prev.filter((id) => id !== langId),
);
},
[setLspAutoInstallLanguages],
);
const customEditors = useMemo(() => sortCustomEditors(editors.filter(isCustomEditor)), [editors]);
const builtInEditors = useMemo(
() => editors.filter((editor) => !isCustomEditor(editor)),
@@ -547,29 +521,32 @@ export function EditorsSettings() {
);
const availableEditors = useMemo(() => resolveAvailableEditors(editors), [editors]);
const defaultOptions = useMemo<ComboboxOption[]>(
() => buildDefaultEditorOptions(availableEditors, state.defaultEditorId),
[availableEditors, state.defaultEditorId],
() => buildDefaultEditorOptions(availableEditors, state.defaultEditorId, t),
[availableEditors, state.defaultEditorId, t],
);
useSyncEditors(editors, setEditors);
return (
<SettingsPageTemplate
title="Editors"
description="Configure the included code editor and external editors"
title={t("settings:editors")}
description={t("settings:configureTheIncludedCodeEditorAnd")}
// Explicit, because the template otherwise derives the save-contributor id
// from `title` — which is now translated, and an identity must not be.
saveId="settings-page:editors"
isDirty={isDirty}
saveStatus={saveDefaultRequest.status}
saveRevision={saveRevision}
canSave={!hasInvalidConfig}
invalidReason={
hasInvalidConfig ? "Fix invalid LSP server configuration before saving." : undefined
hasInvalidConfig ? t("settings:fixInvalidLspServerConfigurationBefore") : undefined
}
onSave={() => saveDefaultRequest.run()}
>
<div className="space-y-6">
<div className="space-y-4">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
File Editor
{t("settings:fileEditor")}
</div>
<LspLanguageCards
lspAutoStartLanguages={state.lspAutoStartLanguages}
@@ -0,0 +1,68 @@
/**
* The language servers Settings → General → Editors can auto-start.
*
* Almost everything here is data rather than copy. `id` keys the auto-start and
* auto-install sets, the server-config map, and `LSP_DEFAULT_CONFIGS` — a
* sentinel, never translated. `label` is a language name, `binary` an executable
* name, and `docsUrl` a URL, so all three are shown verbatim.
*
* Only `installHintKey` is prose, and it is a catalog KEY: this table is
* evaluated once at import, so a `t()` here would resolve before a locale is
* active and freeze on the boot locale. The package names, commands and paths
* the hint mentions are supplied separately as `installHintValues`, so no
* identifier ends up inside translator-controlled text — a translated install
* path or `go install` command would name something that does not exist.
*/
/** Where the auto-installer puts servers — a real path, shown verbatim. */
const LSP_SERVERS_DIR = "~/.kandev/lsp-servers/";
export type LspLanguageOption = {
id: string;
label: string;
binary: string;
docsUrl: string;
installHintKey: string;
installHintValues: Record<string, string>;
};
export const LSP_LANGUAGE_OPTIONS: LspLanguageOption[] = [
{
id: "typescript",
label: "TypeScript / JavaScript",
binary: "typescript-language-server",
docsUrl:
"https://github.com/typescript-language-server/typescript-language-server#workspace-configuration",
installHintKey: "settings:lspInstallHintTypescript",
installHintValues: {
server: "typescript-language-server",
runtime: "typescript",
manager: "npm",
dir: LSP_SERVERS_DIR,
},
},
{
id: "go",
label: "Go",
binary: "gopls",
docsUrl: "https://github.com/golang/tools/blob/master/gopls/doc/settings.md",
installHintKey: "settings:lspInstallHintGo",
installHintValues: { command: "go install golang.org/x/tools/gopls@latest" },
},
{
id: "rust",
label: "Rust",
binary: "rust-analyzer",
docsUrl: "https://rust-analyzer.github.io/book/configuration.html",
installHintKey: "settings:lspInstallHintRust",
installHintValues: { binary: "rust-analyzer", dir: LSP_SERVERS_DIR },
},
{
id: "python",
label: "Python",
binary: "pyright-langserver",
docsUrl: "https://microsoft.github.io/pyright/#/settings",
installHintKey: "settings:lspInstallHintPython",
installHintValues: { package: "pyright", manager: "npm", dir: LSP_SERVERS_DIR },
},
];
+10
View File
@@ -200,4 +200,14 @@ export const i18nGuardFiles = [
"components/settings/startup-page-settings-card.tsx",
"components/settings/system-metrics-settings-card.tsx",
"components/settings/terminal-settings.tsx",
// Settings → General → Editors: the page, its state/section components, the
// custom-editor form, and the editable-card shell that form renders inside.
// `editable-card.tsx` is shared with repository-card.tsx, which is not
// migrated — the guard is per-file, so that stays unaffected.
"app/settings/general/editors/**/*.{ts,tsx}",
"components/settings/editable-card.tsx",
"components/settings/editor-form.tsx",
"components/settings/editors-settings-state.tsx",
"components/settings/editors-settings.tsx",
"components/settings/lsp-language-options.ts",
];
+43 -1
View File
@@ -2,6 +2,8 @@
"aSecretNameAndValueAreRequired": "A secret name and value are required.",
"aSecretNameIsRequired": "A secret name is required.",
"addAppriseProvider": "Add Apprise Provider",
"addCustomEditor": "Add custom editor",
"addEditor": "Add editor",
"addProvider": "Add provider",
"addSecret": "Add secret",
"agent": "Agent",
@@ -10,6 +12,9 @@
"appriseProvider": "Apprise Provider",
"appriseServiceUrls": "Service URL(s)",
"appriseUrlRequired": "At least one Apprise service URL is required.",
"autoInstallIfNotFound": "Auto-install if not found",
"autoStart": "Auto-start",
"autoStartLanguageServersWhenOpening": "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.",
"builtInBrowserPanel": "Built-in browser panel",
"cancel": "Cancel",
"changesPanel": "Changes Panel",
@@ -25,10 +30,13 @@
"cmdCtrlEnterToSend": "Cmd/Ctrl+Enter to send",
"collectExecutionEnvironmentMetrics": "Collect execution environment metrics",
"colorTheme": "Color Theme",
"command": "Command",
"completeTheRequiredEditorFieldsBefore": "Complete the required editor fields before saving.",
"configureArchiveSafeguardsAndDefaultsFor": "Configure archive safeguards and defaults for tasks created by agents",
"configureBackendAndExecutionResourceSampling": "Configure backend and execution resource sampling",
"configureChatInputBehavior": "Configure chat input behavior",
"configureTerminalAppearanceAndBehavior": "Configure terminal appearance and behavior",
"configureTheIncludedCodeEditorAnd": "Configure the included code editor and external editors",
"connectionLostDescription": "Connection lost for at least 10 seconds. Live updates may be stale.",
"connectionUnstableDescription": "Connection unstable. Reconnecting to Kandev.",
"continueEditing": "Continue editing",
@@ -37,6 +45,8 @@
"cpu": "CPU %",
"cpuTemp": "CPU temp",
"custom": "Custom",
"customEditorType": "Custom",
"customEditors": "Custom Editors",
"customEllipsis": "Custom...",
"customFontExample": "e.g. \"My Custom Font\"",
"customizeHowChangedFilesAreDisplayed": "Customize how changed files are displayed",
@@ -45,6 +55,7 @@
"dark": "Dark",
"default": "Default",
"defaultMenloMonaco": "Default (Menlo / Monaco)",
"defaults": "Defaults:",
"deleteSecret": "Delete secret",
"desktopNotifications": "Desktop Notifications",
"desktopNotificationsDescription": "Notify this device when a selected agent turn, question, or Office event occurs.",
@@ -56,8 +67,11 @@
"displayLanguage": "Display language",
"done": "Done",
"edit": "Edit",
"editEditor": "Edit {{name}}",
"editSecret": "Edit secret",
"editorIntegrationsAndDefaults": "Editor integrations and defaults",
"editorName": "Editor name",
"editorType": "Editor type",
"editors": "Editors",
"enableDesktopNotifications": "Enable desktop notifications",
"enableNotificationSound": "Enable notification sound",
@@ -65,36 +79,52 @@
"enterToSend": "Enter to send",
"externalProviders": "External Providers",
"externalProvidersDescription": "Configure external providers for remote notifications.",
"fileEditor": "File Editor",
"fileListView": "File list view",
"fixInvalidLspServerConfigurationBefore": "Fix invalid LSP server configuration before saving.",
"flatList": "Flat list",
"fontFamily": "Font Family",
"fontSize": "Font Size",
"frequencySeconds": "Frequency (seconds)",
"general": "General",
"home": "Home",
"hostedUrl": "Hosted URL",
"icons": "Icons",
"installed": "Installed",
"invalidJson": "Invalid JSON",
"keyboardShortcuts": "Keyboard Shortcuts",
"language": "Language",
"languageServers": "Language Servers",
"lastVisitedTask": "Last visited task",
"layouts": "Layouts",
"ligatures": "Ligatures",
"light": "Light",
"loadingSecrets": "Loading secrets...",
"lspConfigCustomBadge": "custom",
"lspInstallHintGo": "Runs \"{{command}}\". Requires Go to be installed.",
"lspInstallHintPython": "Installs {{package}} via {{manager}} into {{dir}}",
"lspInstallHintRust": "Downloads the {{binary}} binary from GitHub releases into {{dir}}",
"lspInstallHintTypescript": "Installs {{server}} and {{runtime}} via {{manager}} into {{dir}}",
"makesAgentctlValuesAvailableToPlugins": "Makes agentctl values available to plugins and other consumers. The built-in status bar remains host-only.",
"manageApiKeysAndCredentialsSecrets": "Manage API keys and credentials. Secrets are encrypted at rest and injected into agent environments via executor profile env vars.",
"mcpTaskDefaultsAndArchiveSafeguards": "MCP task defaults, archive safeguards, and transcript preferences",
"memory": "Memory %",
"messageSubmitKey": "Message Submit Key",
"metrics": "Metrics",
"mustBeAJsonObject": "Must be a JSON object",
"nameEGOpenaiProductionKey": "Name (e.g. OpenAI Production Key)",
"nerdFonts": "Nerd Fonts",
"new": "New",
"newBrowserTab": "New browser tab",
"newCustomEditor": "New custom editor",
"newSecret": "New secret",
"newTaskSessionsWillUseThis": "New task sessions will use this shell. Existing sessions keep their current shell.",
"noAppriseProviders": "No Apprise providers configured yet.",
"noCustomEditorsYet": "No custom editors yet.",
"noEditorFound": "No editor found.",
"noProvidersConfiguredYet": "No providers configured yet.",
"noSecretsYetAddYourFirst": "No secrets yet. Add your first secret to get started.",
"notInstalled": "Not installed",
"notificationEventClarificationRequested": "Agent needs an answer",
"notificationEventClarificationRequestedDescription": "Notify when the agent explicitly asks you a question.",
"notificationEventOfficeInboxItem": "Office inbox item",
@@ -125,6 +155,10 @@
"openLinksIn": "Open links in",
"openSettingsMenu": "Open settings menu",
"openSettingsMenuWithConnectionIssue": "{{description}} Open settings menu",
"opensAVsCodeRemoteSsh": "Opens a VS Code Remote SSH URL using the scheme (example: {{example}}).",
"optionalScheme": "optional scheme ({{schemes}})",
"optionalUsername": "optional username",
"overrideSettingsSentToEachLanguage": "Override settings sent to each language server via <2>{{method}}</2>. JSON format.",
"pickTheDefaultShellForTask": "Pick the default shell for task sessions",
"preferredShell": "Preferred Shell",
"pressCmdCtrlEnterToSend": "Press Cmd/Ctrl+Enter to send messages. Press Enter for newlines.",
@@ -147,11 +181,14 @@
"saved": "Saved",
"saving": "Saving...",
"savingChanges": "Saving changes",
"searchEditors": "Search editors...",
"secretValue": "Secret value",
"secrets": "Secrets",
"selectADefaultEditor": "Select a default editor",
"selectAShell": "Select a shell",
"sendTestNotification": "Send test notification",
"sendTestNotificationFor": "Send test notification for {{name}}",
"serverConfiguration": "Server Configuration",
"setTheFontSizeForThe": "Set the font size for the terminal. Default is 13px.",
"settings": "Settings",
"shellOptionsUnavailable": "Shell options unavailable",
@@ -168,6 +205,8 @@
"startupPage": "Startup Page",
"statusWithConnectionIssue": "Status. {{description}}",
"submitShortcut": "Submit Shortcut",
"supportedEditors": "Supported Editors",
"supportsPlaceholders": "Supports placeholders: {{placeholders}}",
"systemDefault": "System default",
"systemLoad1Min": "System load (1 min)",
"taskActions": "Task Actions",
@@ -189,5 +228,8 @@
"tree": "Tree",
"usefulWhenKandevIsSelfHosted": "Useful when Kandev is self-hosted on a remote server and you want a lightweight view of the machine resources from the global status bar or phone Status drawer.",
"utilityAgents": "Utility Agents",
"voiceMode": "Voice Mode"
"viewAvailableSettings": "View available settings",
"voiceMode": "Voice Mode",
"vsCodeRemoteSsh": "VS Code Remote SSH",
"whenEnabledInstallYourProjectS": "When enabled, install your project's dependencies (e.g. <2>{{command}}</2> via repository setup scripts) to avoid missing type errors."
}
+43 -1
View File
@@ -2,6 +2,8 @@
"aSecretNameAndValueAreRequired": "À śēćŕēţ ńàḿē àńď vàĺũē àŕē ŕēqũĩŕēď.",
"aSecretNameIsRequired": "À śēćŕēţ ńàḿē ĩś ŕēqũĩŕēď.",
"addAppriseProvider": "Àďď Àƥƥŕĩśē Ƥŕōvĩďēŕ",
"addCustomEditor": "Àďď ćũśţōḿ ēďĩţōŕ",
"addEditor": "Àďď ēďĩţōŕ",
"addProvider": "Àďď ƥŕōvĩďēŕ",
"addSecret": "Àďď śēćŕēţ",
"agent": "Àĝēńţ",
@@ -10,6 +12,9 @@
"appriseProvider": "Àƥƥŕĩśē Ƥŕōvĩďēŕ",
"appriseServiceUrls": "Śēŕvĩćē ŨŔĹ(ś)",
"appriseUrlRequired": "Àţ ĺēàśţ ōńē Àƥƥŕĩśē śēŕvĩćē ŨŔĹ ĩś ŕēqũĩŕēď.",
"autoInstallIfNotFound": "Àũţō-ĩńśţàĺĺ ĩƒ ńōţ ƒōũńď",
"autoStart": "Àũţō-śţàŕţ",
"autoStartLanguageServersWhenOpening": "Àũţō-śţàŕţ ĺàńĝũàĝē śēŕvēŕś ŵĥēń ōƥēńĩńĝ ƒĩĺēś ţō ĝēţ ďĩàĝńōśţĩćś, ĥōvēŕ ĩńƒō, àńď ĝō-ţō-ďēƒĩńĩţĩōń. Ŷōũ ćàń àĺśō ţōĝĝĺē ēàćĥ śēŕvēŕ ōń/ōƒƒ ƥēŕ ƒĩĺē.",
"builtInBrowserPanel": "Ɓũĩĺţ-ĩń ƀŕōŵśēŕ ƥàńēĺ",
"cancel": "Ćàńćēĺ",
"changesPanel": "Ćĥàńĝēś Ƥàńēĺ",
@@ -25,10 +30,13 @@
"cmdCtrlEnterToSend": "Ćḿď/Ćţŕĺ+Ēńţēŕ ţō śēńď",
"collectExecutionEnvironmentMetrics": "Ćōĺĺēćţ ēxēćũţĩōń ēńvĩŕōńḿēńţ ḿēţŕĩćś",
"colorTheme": "Ćōĺōŕ Ţĥēḿē",
"command": "Ćōḿḿàńď",
"completeTheRequiredEditorFieldsBefore": "Ćōḿƥĺēţē ţĥē ŕēqũĩŕēď ēďĩţōŕ ƒĩēĺďś ƀēƒōŕē śàvĩńĝ.",
"configureArchiveSafeguardsAndDefaultsFor": "Ćōńƒĩĝũŕē àŕćĥĩvē śàƒēĝũàŕďś àńď ďēƒàũĺţś ƒōŕ ţàśķś ćŕēàţēď ƀŷ àĝēńţś",
"configureBackendAndExecutionResourceSampling": "Ćōńƒĩĝũŕē ƀàćķēńď àńď ēxēćũţĩōń ŕēśōũŕćē śàḿƥĺĩńĝ",
"configureChatInputBehavior": "Ćōńƒĩĝũŕē ćĥàţ ĩńƥũţ ƀēĥàvĩōŕ",
"configureTerminalAppearanceAndBehavior": "Ćōńƒĩĝũŕē ţēŕḿĩńàĺ àƥƥēàŕàńćē àńď ƀēĥàvĩōŕ",
"configureTheIncludedCodeEditorAnd": "Ćōńƒĩĝũŕē ţĥē ĩńćĺũďēď ćōďē ēďĩţōŕ àńď ēxţēŕńàĺ ēďĩţōŕś",
"connectionLostDescription": "Ćōńńēćţĩōń ĺōśţ ƒōŕ àţ ĺēàśţ 10 śēćōńďś. Ĺĩvē ũƥďàţēś ḿàŷ ƀē śţàĺē.",
"connectionUnstableDescription": "Ćōńńēćţĩōń ũńśţàƀĺē. Ŕēćōńńēćţĩńĝ ţō Ķàńďēv.",
"continueEditing": "Ćōńţĩńũē ēďĩţĩńĝ",
@@ -37,6 +45,8 @@
"cpu": "ĆƤŨ %",
"cpuTemp": "ĆƤŨ ţēḿƥ",
"custom": "Ćũśţōḿ",
"customEditorType": "Ćũśţōḿ",
"customEditors": "Ćũśţōḿ Ēďĩţōŕś",
"customEllipsis": "Ćũśţōḿ...",
"customFontExample": "ē.ĝ. \"Ḿŷ Ćũśţōḿ Ƒōńţ\"",
"customizeHowChangedFilesAreDisplayed": "Ćũśţōḿĩźē ĥōŵ ćĥàńĝēď ƒĩĺēś àŕē ďĩśƥĺàŷēď",
@@ -45,6 +55,7 @@
"dark": "Ďàŕķ",
"default": "Ďēƒàũĺţ",
"defaultMenloMonaco": "Ďēƒàũĺţ (Ḿēńĺō / Ḿōńàćō)",
"defaults": "Ďēƒàũĺţś:",
"deleteSecret": "Ďēĺēţē śēćŕēţ",
"desktopNotifications": "Ďēśķţōƥ Ńōţĩƒĩćàţĩōńś",
"desktopNotificationsDescription": "Ńōţĩƒŷ ţĥĩś ďēvĩćē ŵĥēń à śēĺēćţēď àĝēńţ ţũŕń, qũēśţĩōń, ōŕ Ōƒƒĩćē ēvēńţ ōććũŕś.",
@@ -56,8 +67,11 @@
"displayLanguage": "Ďĩśƥĺàŷ ĺàńĝũàĝē",
"done": "Ďōńē",
"edit": "Ēďĩţ",
"editEditor": "Ēďĩţ {{name}}",
"editSecret": "Ēďĩţ śēćŕēţ",
"editorIntegrationsAndDefaults": "Ēďĩţōŕ ĩńţēĝŕàţĩōńś àńď ďēƒàũĺţś",
"editorName": "Ēďĩţōŕ ńàḿē",
"editorType": "Ēďĩţōŕ ţŷƥē",
"editors": "Ēďĩţōŕś",
"enableDesktopNotifications": "Ēńàƀĺē ďēśķţōƥ ńōţĩƒĩćàţĩōńś",
"enableNotificationSound": "Ēńàƀĺē ńōţĩƒĩćàţĩōń śōũńď",
@@ -65,36 +79,52 @@
"enterToSend": "Ēńţēŕ ţō śēńď",
"externalProviders": "Ēxţēŕńàĺ Ƥŕōvĩďēŕś",
"externalProvidersDescription": "Ćōńƒĩĝũŕē ēxţēŕńàĺ ƥŕōvĩďēŕś ƒōŕ ŕēḿōţē ńōţĩƒĩćàţĩōńś.",
"fileEditor": "Ƒĩĺē Ēďĩţōŕ",
"fileListView": "Ƒĩĺē ĺĩśţ vĩēŵ",
"fixInvalidLspServerConfigurationBefore": "Ƒĩx ĩńvàĺĩď ĹŚƤ śēŕvēŕ ćōńƒĩĝũŕàţĩōń ƀēƒōŕē śàvĩńĝ.",
"flatList": "Ƒĺàţ ĺĩśţ",
"fontFamily": "Ƒōńţ Ƒàḿĩĺŷ",
"fontSize": "Ƒōńţ Śĩźē",
"frequencySeconds": "Ƒŕēqũēńćŷ (śēćōńďś)",
"general": "Ĝēńēŕàĺ",
"home": "Ĥōḿē",
"hostedUrl": "Ĥōśţēď ŨŔĹ",
"icons": "Ĩćōńś",
"installed": "Ĩńśţàĺĺēď",
"invalidJson": "Ĩńvàĺĩď ĴŚŌŃ",
"keyboardShortcuts": "Ķēŷƀōàŕď Śĥōŕţćũţś",
"language": "Ĺàńĝũàĝē",
"languageServers": "Ĺàńĝũàĝē Śēŕvēŕś",
"lastVisitedTask": "Ĺàśţ vĩśĩţēď ţàśķ",
"layouts": "Ĺàŷōũţś",
"ligatures": "Ĺĩĝàţũŕēś",
"light": "Ĺĩĝĥţ",
"loadingSecrets": "Ĺōàďĩńĝ śēćŕēţś...",
"lspConfigCustomBadge": "ćũśţōḿ",
"lspInstallHintGo": "Ŕũńś \"{{command}}\". Ŕēqũĩŕēś Ĝō ţō ƀē ĩńśţàĺĺēď.",
"lspInstallHintPython": "Ĩńśţàĺĺś {{package}} vĩà {{manager}} ĩńţō {{dir}}",
"lspInstallHintRust": "Ďōŵńĺōàďś ţĥē {{binary}} ƀĩńàŕŷ ƒŕōḿ ĜĩţĤũƀ ŕēĺēàśēś ĩńţō {{dir}}",
"lspInstallHintTypescript": "Ĩńśţàĺĺś {{server}} àńď {{runtime}} vĩà {{manager}} ĩńţō {{dir}}",
"makesAgentctlValuesAvailableToPlugins": "Ḿàķēś àĝēńţćţĺ vàĺũēś àvàĩĺàƀĺē ţō ƥĺũĝĩńś àńď ōţĥēŕ ćōńśũḿēŕś. Ţĥē ƀũĩĺţ-ĩń śţàţũś ƀàŕ ŕēḿàĩńś ĥōśţ-ōńĺŷ.",
"manageApiKeysAndCredentialsSecrets": "Ḿàńàĝē ÀƤĨ ķēŷś àńď ćŕēďēńţĩàĺś. Śēćŕēţś àŕē ēńćŕŷƥţēď àţ ŕēśţ àńď ĩńĵēćţēď ĩńţō àĝēńţ ēńvĩŕōńḿēńţś vĩà ēxēćũţōŕ ƥŕōƒĩĺē ēńv vàŕś.",
"mcpTaskDefaultsAndArchiveSafeguards": "ḾĆƤ ţàśķ ďēƒàũĺţś, àŕćĥĩvē śàƒēĝũàŕďś, àńď ţŕàńśćŕĩƥţ ƥŕēƒēŕēńćēś",
"memory": "Ḿēḿōŕŷ %",
"messageSubmitKey": "Ḿēśśàĝē Śũƀḿĩţ Ķēŷ",
"metrics": "Ḿēţŕĩćś",
"mustBeAJsonObject": "Ḿũśţ ƀē à ĴŚŌŃ ōƀĵēćţ",
"nameEGOpenaiProductionKey": "Ńàḿē (ē.ĝ. ŌƥēńÀĨ Ƥŕōďũćţĩōń Ķēŷ)",
"nerdFonts": "Ńēŕď Ƒōńţś",
"new": "Ńēŵ",
"newBrowserTab": "Ńēŵ ƀŕōŵśēŕ ţàƀ",
"newCustomEditor": "Ńēŵ ćũśţōḿ ēďĩţōŕ",
"newSecret": "Ńēŵ śēćŕēţ",
"newTaskSessionsWillUseThis": "Ńēŵ ţàśķ śēśśĩōńś ŵĩĺĺ ũśē ţĥĩś śĥēĺĺ. Ēxĩśţĩńĝ śēśśĩōńś ķēēƥ ţĥēĩŕ ćũŕŕēńţ śĥēĺĺ.",
"noAppriseProviders": "Ńō Àƥƥŕĩśē ƥŕōvĩďēŕś ćōńƒĩĝũŕēď ŷēţ.",
"noCustomEditorsYet": "Ńō ćũśţōḿ ēďĩţōŕś ŷēţ.",
"noEditorFound": "Ńō ēďĩţōŕ ƒōũńď.",
"noProvidersConfiguredYet": "Ńō ƥŕōvĩďēŕś ćōńƒĩĝũŕēď ŷēţ.",
"noSecretsYetAddYourFirst": "Ńō śēćŕēţś ŷēţ. Àďď ŷōũŕ ƒĩŕśţ śēćŕēţ ţō ĝēţ śţàŕţēď.",
"notInstalled": "Ńōţ ĩńśţàĺĺēď",
"notificationEventClarificationRequested": "Àĝēńţ ńēēďś àń àńśŵēŕ",
"notificationEventClarificationRequestedDescription": "Ńōţĩƒŷ ŵĥēń ţĥē àĝēńţ ēxƥĺĩćĩţĺŷ àśķś ŷōũ à qũēśţĩōń.",
"notificationEventOfficeInboxItem": "Ōƒƒĩćē ĩńƀōx ĩţēḿ",
@@ -125,6 +155,10 @@
"openLinksIn": "Ōƥēń ĺĩńķś ĩń",
"openSettingsMenu": "Ōƥēń śēţţĩńĝś ḿēńũ",
"openSettingsMenuWithConnectionIssue": "{{description}} Ōƥēń śēţţĩńĝś ḿēńũ",
"opensAVsCodeRemoteSsh": "Ōƥēńś à VŚ Ćōďē Ŕēḿōţē ŚŚĤ ŨŔĹ ũśĩńĝ ţĥē śćĥēḿē (ēxàḿƥĺē: {{example}}).",
"optionalScheme": "ōƥţĩōńàĺ śćĥēḿē ({{schemes}})",
"optionalUsername": "ōƥţĩōńàĺ ũśēŕńàḿē",
"overrideSettingsSentToEachLanguage": "Ōvēŕŕĩďē śēţţĩńĝś śēńţ ţō ēàćĥ ĺàńĝũàĝē śēŕvēŕ vĩà <2>{{method}}</2>. ĴŚŌŃ ƒōŕḿàţ.",
"pickTheDefaultShellForTask": "Ƥĩćķ ţĥē ďēƒàũĺţ śĥēĺĺ ƒōŕ ţàśķ śēśśĩōńś",
"preferredShell": "Ƥŕēƒēŕŕēď Śĥēĺĺ",
"pressCmdCtrlEnterToSend": "Ƥŕēśś Ćḿď/Ćţŕĺ+Ēńţēŕ ţō śēńď ḿēśśàĝēś. Ƥŕēśś Ēńţēŕ ƒōŕ ńēŵĺĩńēś.",
@@ -147,11 +181,14 @@
"saved": "Śàvēď",
"saving": "Śàvĩńĝ...",
"savingChanges": "Śàvĩńĝ ćĥàńĝēś",
"searchEditors": "Śēàŕćĥ ēďĩţōŕś...",
"secretValue": "Śēćŕēţ vàĺũē",
"secrets": "Śēćŕēţś",
"selectADefaultEditor": "Śēĺēćţ à ďēƒàũĺţ ēďĩţōŕ",
"selectAShell": "Śēĺēćţ à śĥēĺĺ",
"sendTestNotification": "Śēńď ţēśţ ńōţĩƒĩćàţĩōń",
"sendTestNotificationFor": "Śēńď ţēśţ ńōţĩƒĩćàţĩōń ƒōŕ {{name}}",
"serverConfiguration": "Śēŕvēŕ Ćōńƒĩĝũŕàţĩōń",
"setTheFontSizeForThe": "Śēţ ţĥē ƒōńţ śĩźē ƒōŕ ţĥē ţēŕḿĩńàĺ. Ďēƒàũĺţ ĩś 13ƥx.",
"settings": "Śēţţĩńĝś",
"shellOptionsUnavailable": "Śĥēĺĺ ōƥţĩōńś ũńàvàĩĺàƀĺē",
@@ -168,6 +205,8 @@
"startupPage": "Śţàŕţũƥ Ƥàĝē",
"statusWithConnectionIssue": "Śţàţũś. {{description}}",
"submitShortcut": "Śũƀḿĩţ Śĥōŕţćũţ",
"supportedEditors": "Śũƥƥōŕţēď Ēďĩţōŕś",
"supportsPlaceholders": "Śũƥƥōŕţś ƥĺàćēĥōĺďēŕś: {{placeholders}}",
"systemDefault": "Śŷśţēḿ ďēƒàũĺţ",
"systemLoad1Min": "Śŷśţēḿ ĺōàď (1 ḿĩń)",
"taskActions": "Ţàśķ Àćţĩōńś",
@@ -189,5 +228,8 @@
"tree": "Ţŕēē",
"usefulWhenKandevIsSelfHosted": "Ũśēƒũĺ ŵĥēń Ķàńďēv ĩś śēĺƒ-ĥōśţēď ōń à ŕēḿōţē śēŕvēŕ àńď ŷōũ ŵàńţ à ĺĩĝĥţŵēĩĝĥţ vĩēŵ ōƒ ţĥē ḿàćĥĩńē ŕēśōũŕćēś ƒŕōḿ ţĥē ĝĺōƀàĺ śţàţũś ƀàŕ ōŕ ƥĥōńē Śţàţũś ďŕàŵēŕ.",
"utilityAgents": "Ũţĩĺĩţŷ Àĝēńţś",
"voiceMode": "Vōĩćē Ḿōďē"
"viewAvailableSettings": "Vĩēŵ àvàĩĺàƀĺē śēţţĩńĝś",
"voiceMode": "Vōĩćē Ḿōďē",
"vsCodeRemoteSsh": "VŚ Ćōďē Ŕēḿōţē ŚŚĤ",
"whenEnabledInstallYourProjectS": "Ŵĥēń ēńàƀĺēď, ĩńśţàĺĺ ŷōũŕ ƥŕōĵēćţ'ś ďēƥēńďēńćĩēś (ē.ĝ. <2>{{command}}</2> vĩà ŕēƥōśĩţōŕŷ śēţũƥ śćŕĩƥţś) ţō àvōĩď ḿĩśśĩńĝ ţŷƥē ēŕŕōŕś."
}