feat(i18n): localize Settings → General → Editors (#2146)
Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
@@ -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'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'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 },
|
||||
},
|
||||
];
|
||||
@@ -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",
|
||||
];
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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ōĩď ḿĩśśĩńĝ ţŷƥē ēŕŕōŕś."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user