feat: add support for localising text in the frontend and add German language data (#360)

Co-authored-by: nekename <63245705+nekename@users.noreply.github.com>
This commit is contained in:
Terrorwolf01andnekename authored and GitHub committed 2026-06-26 15:44:18 +01:00
1 parent a7b5d24625
commit 5687776ff6
17 files changed
+532 -164

No files matched your search

+3 -2
View File
@@ -3,6 +3,7 @@
import MagnifyingGlass from "phosphor-svelte/lib/MagnifyingGlass";
import { t } from "$lib/i18n";
import { getWebserverUrl } from "$lib/ports";
import { copiedItem } from "$lib/propertyInspector";
import { localisations } from "$lib/settings";
@@ -81,13 +82,13 @@
<input
bind:value={query}
class="w-full p-1 text-sm text-neutral-300"
placeholder="Search actions"
placeholder={$t("action_list.search_placeholder")}
type="search"
spellcheck="false"
/>
</div>
<span id="action-list-hint" class="sr-only">Use arrow keys to navigate between actions within a category.</span>
<span id="action-list-hint" class="sr-only">{$t("action_list.hint")}</span>
<div class="grow overflow-auto select-none divide-y divide-neutral-800!">
{#each filteredCategories as [name, { icon, actions }]}
<details open>
+3 -2
View File
@@ -2,6 +2,7 @@
import type { DeviceInfo } from "$lib/DeviceInfo";
import type { Profile } from "$lib/Profile";
import { t } from "$lib/i18n";
import { profileManager } from "$lib/singletons";
import { invoke } from "@tauri-apps/api/core";
@@ -73,8 +74,8 @@
{#if Object.keys(devices).length > 0}
<div class="select-device-wrapper">
<span bind:this={measure} class="invisible fixed whitespace-pre pointer-events-none text-xl font-semibold" aria-hidden="true"></span>
<select bind:value style:width="{selectWidth}px" aria-label="Device">
<option value="" disabled selected>Choose a device...</option>
<select bind:value style:width="{selectWidth}px" aria-label={$t("device_selector.device")}>
<option value="" disabled selected>{$t("device_selector.choose_device")}</option>
{#each Object.entries(devices).sort() as [id, device]}
<option value={id}>{device.name}</option>
+5 -4
View File
@@ -7,6 +7,7 @@
import Key from "./Key.svelte";
import { t } from "$lib/i18n";
import { inspectedInstance, inspectedParentAction } from "$lib/propertyInspector";
import { invoke } from "@tauri-apps/api/core";
@@ -173,7 +174,7 @@
</style>
{#key device}
<span id="grid-description" class="sr-only">Use arrow keys to navigate between keys. Moving to a key will display its property inspector.</span>
<span id="grid-description" class="sr-only">{$t("device_view.grid_description")}</span>
<div
class="flex flex-col justify-center grow px-16 py-6 overflow-auto"
class:items-center={device.columns <= 8}
@@ -202,7 +203,7 @@
on:dragstart={(event) => handleDragStart(event, "Keypad", (r * device.columns) + c)}
{handlePaste}
size={device.id.startsWith("sd-") && device.rows == 4 && device.columns == 8 ? 192 : 144}
label="Key {String.fromCharCode(65 + r)}{c + 1}"
label="{$t('device_view.key')} {String.fromCharCode(65 + r)}{c + 1}"
tabindex={focusedRow === r && focusedCol === c ? 0 : -1}
/>
{/each}
@@ -220,7 +221,7 @@
on:dragstart={(event) => handleDragStart(event, "Encoder", i)}
{handlePaste}
size={device.id.startsWith("sd-") && device.rows == 4 && device.columns == 8 ? 192 : 144}
label="Encoder {i + 1}"
label="{$t('device_view.encoder')} {i + 1}"
tabindex={focusedRow === encoderRowIndex && focusedCol === i ? 0 : -1}
/>
{/each}
@@ -255,7 +256,7 @@
{handlePaste}
size={device.id.startsWith("sd-") && device.rows == 4 && device.columns == 8 ? 192 : 144}
isTouchPoint
label="Touch point {i + 1}"
label="{$t('device_view.touchpoint')} {i + 1}"
tabindex={focusedRow === touchpointRowIndex && focusedCol === i ? 0 : -1}
/>
{/each}
+25 -24
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import type { ActionInstance } from "$lib/ActionInstance";
import { t } from "$lib/i18n";
import { renderImage, resizeImage } from "$lib/rendererHelper";
import { invoke } from "@tauri-apps/api/core";
@@ -63,13 +64,13 @@
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 p-2 text-neutral-300 bg-neutral-700 border border-neutral-600 rounded-lg z-10">
<div class="flex flex-row">
<div class="select-wrapper m-1 w-full">
<select class="w-full bg-neutral-600! border-neutral-500!" bind:value={state} aria-label="State">
<select class="w-full bg-neutral-600! border-neutral-500!" bind:value={state} aria-label={$t("instance_editor.state")}>
{#each instance.states as _, i}
<option value={i}>State {i + 1}</option>
<option value={i}>{$t("instance_editor.state.n", { n: i + 1 })}</option>
{/each}
</select>
</div>
<button class="ml-2 mr-1 float-right text-xl text-neutral-300" on:click={() => showEditor = false} aria-label="Close">✕</button>
<button class="ml-2 mr-1 float-right text-xl text-neutral-300" on:click={() => showEditor = false} aria-label={$t("settings.close")}>✕</button>
</div>
<div class="flex flex-row mx-1">
<div class="flex flex-col justify-center items-center mt-2 mb-1">
@@ -87,8 +88,8 @@
event.preventDefault();
instance.states[state].image = instance.action.states[state]?.image ?? instance.action.icon;
}}
title="Click to select an image, or right-click to reset to the default image."
aria-label="Click to select an image, or right-click to reset to the default image."
title={$t("instance_editor.image.hint")}
aria-label={$t("instance_editor.image.hint")}
>
{#await renderImage(null, null, instance.states[state], instance.action.states[state]?.image ?? instance.action.icon, false, false, true, false, false, 0, true)}
<div class="w-32 min-w-32 h-32 bg-neutral-800 animate-pulse border border-neutral-600 rounded-xl"></div>
@@ -96,7 +97,7 @@
<img
src={resolvedSrc}
class="my-auto w-32 min-w-32 h-min aspect-square bg-black border border-neutral-600 rounded-xl cursor-pointer"
alt="State {state + 1} image"
alt={$t("instance_editor.image.n", { n: state + 1 })}
/>
{/await}
</button>
@@ -104,8 +105,8 @@
<button
on:click={() => adjustImageScale(-10)}
class="w-6 h-6 text-sm bg-neutral-600 hover:bg-neutral-500 transition-colors border border-neutral-500 rounded-md"
title="Decrease image scale"
aria-label="Decrease image scale"
title={$t("instance_editor.image.scale.decrease")}
aria-label={$t("instance_editor.image.scale.decrease")}
>
-
</button>
@@ -115,8 +116,8 @@
<button
on:click={() => adjustImageScale(10)}
class="w-6 h-6 text-sm bg-neutral-600 hover:bg-neutral-500 transition-colors border border-neutral-500 rounded-md"
title="Increase image scale"
aria-label="Increase image scale"
title={$t("instance_editor.image.scale.increase")}
aria-label={$t("instance_editor.image.scale.increase")}
>
+
</button>
@@ -137,7 +138,7 @@
}}
class="mt-1 px-0.5 text-sm text-neutral-300 bg-neutral-600 hover:bg-neutral-500 transition-colors border border-neutral-500 rounded-lg"
>
Set background
{$t("instance_editor.background")}
<input
bind:this={backgroundColourInput}
type="color"
@@ -161,7 +162,7 @@
}}
class="mt-1 px-0.5 text-sm text-neutral-300 bg-neutral-600 hover:bg-neutral-500 transition-colors border border-neutral-500 rounded-lg"
>
Use solid colour
{$t("instance_editor.solid_colour")}
<input
bind:this={solidColourInput}
type="color"
@@ -204,7 +205,7 @@
<div class="flex flex-col justify-center pl-4 pr-2 pt-4 pb-2 space-y-2">
<div class="flex flex-row items-center space-x-2">
<label for="editor-text">Text</label>
<label for="editor-text">{$t("instance_editor.text")}</label>
<textarea
bind:value={instance.states[state].text}
placeholder={instance.action.states[state]?.text || instance.action.name}
@@ -214,14 +215,14 @@
/>
</div>
<div class="flex flex-row items-center">
<label for="editor-colour" class="mr-2">Colour</label>
<label for="editor-colour" class="mr-2">{$t("instance_editor.colour")}</label>
<input
type="color"
bind:value={instance.states[state].colour}
class="mr-2 px-0.5 bg-neutral-600 border border-neutral-500 rounded-lg"
id="editor-colour"
/>
<label for="editor-show" class="mr-2">Show</label>
<label for="editor-show" class="mr-2">{$t("instance_editor.show")}</label>
<input
type="checkbox"
bind:checked={instance.states[state].show}
@@ -231,22 +232,22 @@
<select
bind:value={instance.states[state].alignment}
class="px-1! py-0.5!"
aria-label="Alignment"
aria-label={$t("instance_editor.alignment")}
>
<option value="top">Top</option>
<option value="middle">Middle</option>
<option value="bottom">Bottom</option>
<option value="top">{$t("instance_editor.alignment.top")}</option>
<option value="middle">{$t("instance_editor.alignment.middle")}</option>
<option value="bottom">{$t("instance_editor.alignment.bottom")}</option>
</select>
</div>
<div class="flex flex-row items-center">
<label for="editor-stroke" class="mr-2">Stroke</label>
<label for="editor-stroke" class="mr-2">{$t("instance_editor.stroke")}</label>
<input
type="color"
bind:value={instance.states[state].stroke_colour}
class="mr-2 px-0.5 bg-neutral-600 border border-neutral-500 rounded-lg"
id="editor-stroke"
/>
<label for="editor-outline" class="mr-2">Outline</label>
<label for="editor-outline" class="mr-2">{$t("instance_editor.outline")}</label>
<input
type="number"
bind:value={instance.states[state].stroke_size}
@@ -255,11 +256,11 @@
/>
</div>
<div class="flex flex-row items-center">
<label for="editor-font" class="mr-2">Font</label>
<label for="editor-font" class="mr-2">{$t("instance_editor.font")}</label>
<input
list="families"
bind:value={instance.states[state].family}
placeholder="Font family"
placeholder={$t("instance_editor.font.placeholder")}
class="w-full px-1 text-neutral-300 bg-neutral-600 border border-neutral-500 rounded-lg"
id="editor-font"
/>
@@ -303,7 +304,7 @@
class="mr-4 mt-1 scale-125"
id="editor-underline"
/>
<label for="editor-size" class="mr-2">Size</label>
<label for="editor-size" class="mr-2">{$t("instance_editor.font.size")}</label>
<input
type="number"
bind:value={instance.states[state].size}
+5 -4
View File
@@ -10,6 +10,7 @@
import Trash from "phosphor-svelte/lib/Trash";
import InstanceEditor from "./InstanceEditor.svelte";
import { t } from "$lib/i18n";
import { copiedItem, inspectedInstance, inspectedParentAction, openContextMenu } from "$lib/propertyInspector";
import { CanvasLock, renderImage } from "$lib/rendererHelper";
import { settings } from "$lib/settings";
@@ -243,7 +244,7 @@
on:click|stopPropagation={paste}
>
<Clipboard size="18" class="text-neutral-300" />
<span class="ml-2"> Paste </span>
<span class="ml-2">{$t("key.paste")}</span>
</button>
{:else}
<button
@@ -251,21 +252,21 @@
on:click|stopPropagation={edit}
>
<Pencil size="18" class="text-neutral-300" />
<span class="ml-2"> Edit </span>
<span class="ml-2">{$t("key.edit")}</span>
</button>
<button
class="flex flex-row items-center w-full p-2 hover:bg-neutral-600 transition-colors cursor-pointer"
on:click|stopPropagation={copy}
>
<Copy size="18" class="text-neutral-300" />
<span class="ml-2"> Copy </span>
<span class="ml-2">{$t("key.copy")}</span>
</button>
<button
class="flex flex-row items-center w-full p-2 hover:bg-neutral-600 transition-colors rounded-b-lg cursor-pointer"
on:click|stopPropagation={clear}
>
<Trash size="18" class="text-red-400" />
<span class="ml-2"> Delete </span>
<span class="ml-2">{$t("key.delete")}</span>
</button>
{/if}
</div>
+6 -5
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { t } from "$lib/i18n";
import { PRODUCT_NAME } from "$lib/singletons";
import { invoke } from "@tauri-apps/api/core";
@@ -9,17 +10,17 @@
<div class="flex flex-col justify-center items-center w-full h-full text-center text-neutral-300">
<div class="w-80 text-sm">
<h2 class="text-lg font-bold mb-2">No devices detected</h2>
<p class="mb-2">Make sure your devices are connected properly and you have permission to access them.</p>
<h2 class="text-lg font-bold mb-2">{$t("no_devices_detected.title")}</h2>
<p class="mb-2">{$t("no_devices_detected.check_connection")}</p>
{#if buildInfo?.split("</summary>")[0]?.includes("linux")}
<p class="mb-2">Ensure you have the correct udev subsystem rules installed.</p>
<p class="mb-2">{$t("no_devices_detected.check_udev")}</p>
{/if}
<p class="mb-4">You may need to install a plugin that adds support for your device.</p>
<p class="mb-4">{$t("no_devices_detected.install_plugin")}</p>
<button
class="px-2 py-1 text-sm text-neutral-300 bg-neutral-700 hover:bg-neutral-600 transition-colors border border-neutral-600 rounded-lg"
on:click={() => invoke("restart")}
>
Restart {PRODUCT_NAME}
{$t("no_devices_detected.restart", { PRODUCT_NAME })}
</button>
</div>
</div>
+7 -6
View File
@@ -6,6 +6,7 @@
import Trash from "phosphor-svelte/lib/Trash";
import Key from "./Key.svelte";
import { t } from "$lib/i18n";
import { copiedItem, inspectedInstance, inspectedParentAction } from "$lib/propertyInspector";
import { invoke } from "@tauri-apps/api/core";
@@ -112,8 +113,8 @@
/>
<div class="px-6 pt-6 pb-4 text-neutral-300">
<button class="float-right text-xl" on:click={() => $inspectedParentAction = null} aria-label="Close">✕</button>
<h1 class="font-semibold text-2xl">{parentUuid == "opendeck.toggleaction" ? "Toggle Action" : "Multi Action"}</h1>
<button class="float-right text-xl" on:click={() => $inspectedParentAction = null} aria-label={$t("settings.close")}>✕</button>
<h1 class="font-semibold text-2xl">{parentUuid == "opendeck.toggleaction" ? $t("parent_action.toggle") : $t("parent_action.multi")}</h1>
</div>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
@@ -122,7 +123,7 @@
class="flex flex-col h-128 overflow-auto"
on:click={() => $inspectedInstance = null}
role="list"
aria-label="{parentUuid == 'opendeck.toggleaction' ? 'Toggle Action' : 'Multi Action'} children"
aria-label="{parentUuid == 'opendeck.toggleaction' ? $t('parent_action.toggle') : $t('parent_action.multi')} {$t('parent_action.children')}"
on:keydown={handleListKeydown}
>
{#each children as instance, index}
@@ -151,7 +152,7 @@
class="ml-auto mr-10"
on:click|stopPropagation={() => removeInstance(index)}
tabindex={-1}
aria-label="Remove {instance.action.name}"
aria-label={$t("parent_action.remove", { name: instance.action.name })}
>
<Trash size="32" class="text-neutral-400" />
</button>
@@ -169,9 +170,9 @@
}}
role="listitem"
tabindex={children.length == 0 ? 0 : -1}
aria-label="Drag a new action here or copy one with Control+C and paste with Control+V."
aria-label={$t("action_view.dragcopy")}
>
<img src="/cube.png" class="m-2 w-24 rounded-xl" alt="" />
<p class="ml-4 text-xl text-neutral-400">Drop or paste actions here</p>
<p class="ml-4 text-xl text-neutral-400">{$t("action_view.dragpaste")}</p>
</div>
</div>
+8 -7
View File
@@ -3,6 +3,7 @@
import DownloadSimple from "phosphor-svelte/lib/DownloadSimple";
import Popup from "./Popup.svelte";
import { t } from "$lib/i18n.ts";
import "$lib/shims.ts";
import { invoke } from "@tauri-apps/api/core";
@@ -14,7 +15,7 @@
export let id: string;
export let details: { repository: string; name: string; author: string; download_url: string | undefined };
let readme = "<strong>Loading plugin details...</strong>";
let readme = `<strong>${$t("plugin_details.loading")}</strong>`;
let downloadCount = 0;
export let install: () => void;
@@ -44,7 +45,7 @@
return await marked.parse(DOMPurify.sanitize(await response.text()).replace(/<a/g, '<a target="_blank" '));
}
}
return await marked.parse("**Plugin README file not found**\n\n[View plugin on GitHub](https://github.com/" + repo + ")");
return await marked.parse($t("plugin_details.readme.not_found", { repo }));
}
function handleReadmeClick(event: MouseEvent | KeyboardEvent) {
@@ -70,8 +71,8 @@
});
</script>
<Popup show label="{details.name} plugin details">
<button class="mr-2 my-1 float-right text-xl text-neutral-300" on:click={close} aria-label="Close">✕</button>
<Popup show label={$t("plugin_details.title").replaceAll("{details.name}", details.name)}>
<button class="mr-2 my-1 float-right text-xl text-neutral-300" on:click={close} aria-label={$t("settings.close")}>✕</button>
<div class="flex flex-row items-start">
<img
src={"https://openactionapi.github.io/plugins/icons/" + id + ".png"}
@@ -81,7 +82,7 @@
<div class="flex flex-col justify-center h-48 ml-8">
<div class="text-3xl text-neutral-200">{details.name}</div>
<div class="flex items-center mt-2 text-lg text-neutral-400">
<span class="mr-2">by</span>
<span class="mr-2">{$t("plugin_details.by")}</span>
<img
src={"https://avatars.githubusercontent.com/" + details.repository.split("/")[3]}
alt="Author avatar"
@@ -105,13 +106,13 @@
on:click={install}
class="px-8 py-3 active:translate-y-0.5 text-lg text-neutral-100 bg-indigo-600 hover:bg-indigo-500 transition-colors border border-indigo-500 rounded-l-lg"
>
Install
{$t("plugin_details.install")}
</button>
<button
on:click={() => invoke("open_url", { url: details.download_url ?? details.repository + "/releases/latest" })}
class="ml-1 p-3.5 active:translate-y-0.5 text-lg text-neutral-100 bg-indigo-600 hover:bg-indigo-500 transition-colors border border-indigo-500 rounded-r-lg"
aria-label="Download latest release from GitHub"
aria-label={$t("plugin_details.download_latest")}
>
<ArrowSquareOut size={24} />
</button>
+42 -35
View File
@@ -12,6 +12,7 @@
import Popup from "./Popup.svelte";
import Tooltip from "./Tooltip.svelte";
import { t } from "$lib/i18n";
import { getWebserverUrl } from "$lib/ports";
import { localisations, settings } from "$lib/settings";
import { actionList, deviceSelector, PRODUCT_NAME } from "$lib/singletons";
@@ -29,14 +30,14 @@
}, 1e3);
async function installPlugin(name: string, url: string | null, file: string | null, fallback_id: string | null) {
if (!file && !await ask(`It may take a while to install the plugin.`, { title: `Install "${name}"?` })) return;
if (!file && !await ask($t("plugin_manager.install.prompt"), { title: $t("plugin_manager.install.title", { name }), okLabel: $t("dialog.yes"), cancelLabel: $t("dialog.no") })) return;
try {
await invoke("install_plugin", { url, file, fallback_id });
message(`Successfully installed "${name}".`, { title: `Installed "${name}"` });
message($t("plugin_manager.install.success", { name }), { title: $t("plugin_manager.install.success.title", { name }), buttons: { ok: $t("dialog.ok") } });
$actionList?.reload();
installed = await invoke("list_plugins");
} catch (error: any) {
message(error, { title: `Failed to install "${name}"` });
message(error, { title: $t("plugin_manager.install.error", { name }), buttons: { ok: $t("dialog.ok") } });
}
}
@@ -82,7 +83,7 @@
try {
res = await (await fetch(endpoint)).json();
} catch (error: any) {
message(error, { title: `Failed to install "${plugin.name}"` });
message(error, { title: $t("plugin_manager.install.error", { name: plugin.name }), buttons: { ok: $t("dialog.ok") } });
return;
}
@@ -116,15 +117,21 @@
}
async function removePlugin(plugin: any) {
if (!await ask(`Are you sure you want to remove "${plugin.name}"?`, { title: `Remove "${plugin.name}"?` })) return;
if (
!await ask($t("plugin_manager.remove.prompt", { name: plugin.name }), {
title: $t("plugin_manager.remove.title", { name: plugin.name }),
okLabel: $t("dialog.yes"),
cancelLabel: $t("dialog.no"),
})
) return;
try {
await invoke("remove_plugin", { id: plugin.id });
message(`Successfully removed "${plugin.name}".`, { title: `Removed "${plugin.name}"` });
message($t("plugin_manager.remove.success", { name: plugin.name }), { title: $t("plugin_manager.remove.success.title", { name: plugin.name }), buttons: { ok: $t("dialog.ok") } });
$actionList?.reload();
$deviceSelector?.reloadProfiles();
installed = await invoke("list_plugins");
} catch (error: any) {
message(error, { title: `Failed to remove "${plugin.name}"` });
message(error, { title: $t("plugin_manager.remove.error", { name: plugin.name }), buttons: { ok: $t("dialog.ok") } });
}
}
@@ -199,7 +206,7 @@
class="px-3 py-1 text-sm text-neutral-300 bg-neutral-700 hover:bg-neutral-600 transition-colors border border-neutral-600 rounded-lg"
on:click={() => showPopup = true}
>
Plugins
{$t("plugin_manager.button")}
</button>
<svelte:window
@@ -212,11 +219,11 @@
}}
/>
<Popup show={showPopup} label="Manage plugins">
<button class="mr-2 my-1 float-right text-xl text-neutral-300" on:click={() => showPopup = false} aria-label="Close">✕</button>
<h2 class="m-2 font-semibold text-xl text-neutral-300">Manage plugins</h2>
<Popup show={showPopup} label={$t("plugin_manager.title")}>
<button class="mr-2 my-1 float-right text-xl text-neutral-300" on:click={() => showPopup = false} aria-label={$t("settings.close")}>✕</button>
<h2 class="m-2 font-semibold text-xl text-neutral-300">{$t("plugin_manager.title")}</h2>
<h2 class="mx-2 mt-6 mb-2 text-lg text-neutral-400">Installed plugins</h2>
<h2 class="mx-2 mt-6 mb-2 text-lg text-neutral-400">{$t("plugin_manager.installed")}</h2>
<div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<!-- deno-fmt-ignore -->
{#each installed.sort((a, b) =>
@@ -235,15 +242,15 @@
if ($settings?.developer) invoke("reload_plugin", { id: plugin.id });
else removePlugin(plugin);
}}
actionLabel={$settings?.developer ? "Reload" : "Remove"}
actionLabel={$settings?.developer ? $t("plugin_manager.reload") : $t("plugin_manager.remove")}}
secondaryAction={!plugin.registered ? () => invoke("open_log_directory") : plugin.has_settings_interface ? () => invoke("show_settings_interface", { plugin: plugin.id }) : undefined}
secondaryActionLabel={!plugin.registered ? "View logs" : "Settings"}
secondaryActionLabel={!plugin.registered ? $t("plugin_manager.view_logs") : $t("plugin_manager.plugin_settings")}}
>
<svelte:fragment slot="subtitle">
{plugin.version}
{#if availableUpdates[plugin.id]}
(<span class="text-yellow-400">
available:
{$t("plugin_manager.available")}
<button
class="font-semibold underline"
on:click={() => openDetailsView = plugin.id.endsWith(".sdPlugin") ? plugin.id.slice(0, -9) : plugin.id}
@@ -271,20 +278,20 @@
</div>
<div class="flex flex-row justify-between items-center mx-2 mt-6 mb-2">
<h2 class="text-lg text-neutral-400">Plugin store</h2>
<h2 class="text-lg text-neutral-400">{$t("plugin_manager.store")}</h2>
<button
class="flex flex-row items-center mt-2 px-1 py-0.5 text-sm text-neutral-300 bg-neutral-700 hover:bg-neutral-600 transition-colors border border-neutral-600 rounded-lg"
on:click={installPluginFile}
>
<FileArrowUp />
<span class="ml-1">Install from file</span>
<span class="ml-1">{$t("plugin_manager.install_from_file")}</span>
</button>
</div>
<div class="flex flex-row items-center mx-2 my-4 p-3 space-x-2 bg-yellow-900/20 border-l-4 border-yellow-500 rounded">
<WarningCircle size="20" class="mt-0.5 text-yellow-500" />
<div class="text-sm text-yellow-200">
If you are experiencing issues with a plugin, please reach out on one of the {PRODUCT_NAME} support channels before attempting to contact the plugin developer.
{$t("plugin_manager.warning", { PRODUCT_NAME })}
</div>
</div>
@@ -293,19 +300,19 @@
<input
bind:value={query}
class="w-full p-2 text-neutral-300"
placeholder="Search plugins"
aria-label="Search plugins"
placeholder={$t("plugin_manager.search")}
aria-label={$t("plugin_manager.search")}
type="search"
spellcheck="false"
/>
</div>
{#if !plugins}
<h2 class="mx-2 mt-6 mb-2 text-md text-neutral-400">Loading open-source plugin list...</h2>
<h2 class="mx-2 mt-6 mb-2 text-md text-neutral-400">{$t("plugin_manager.loading.open_source")}</h2>
{:else}
<div class="flex flex-row items-center ml-2 mt-6 mb-2 space-x-2">
<h2 class="font-semibold text-md text-neutral-400">Open-source plugins</h2>
<Tooltip>Open-source plugins downloaded from the author's releases</Tooltip>
<h2 class="font-semibold text-md text-neutral-400">{$t("plugin_manager.open_source")}</h2>
<Tooltip>{$t("plugin_manager.open_source.tooltip")}</Tooltip>
</div>
<div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{#each Object.entries(plugins) as [id, plugin]}
@@ -315,7 +322,7 @@
subtitle={plugin.author}
hidden={!plugin.name.toLowerCase().includes(query.toLowerCase())}
action={() => openDetailsView = id}
actionLabel="View details"
actionLabel={$t("plugin_manager.view_details")}
>
<ArrowSquareOut size="24" class="text-neutral-400" />
</ListedPlugin>
@@ -324,8 +331,8 @@
{/if}
<div class="flex flex-row items-center mt-6 mb-2">
<h2 class="mx-2 font-semibold text-md text-neutral-400">Elgato App Store archive</h2>
<Tooltip>Plugins archived from the Elgato App Store (now replaced by the Elgato Marketplace)</Tooltip>
<h2 class="mx-2 font-semibold text-md text-neutral-400">{$t("plugin_manager.elgato")}</h2>
<Tooltip>{$t("plugin_manager.elgato.tooltip")}</Tooltip>
</div>
{#if !showArchive}
<button
@@ -335,10 +342,10 @@
archivePlugins = await (await fetch("https://plugins.amankhanna.me/catalogue.json")).json();
}}
>
Load Elgato App Store archive
{$t("plugin_manager.elgato.load")}
</button>
{:else if !archivePlugins}
<h2 class="mx-2 mt-4 mb-2 text-md text-neutral-400">Loading Elgato App Store archive plugin list...</h2>
<h2 class="mx-2 mt-4 mb-2 text-md text-neutral-400">{$t("plugin_manager.loading.elgato")}</h2>
{:else}
<div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{#each archivePlugins as plugin}
@@ -348,7 +355,7 @@
subtitle={plugin.author}
hidden={!plugin.name.toLowerCase().includes(query.toLowerCase())}
action={() => installPluginElgato(plugin)}
actionLabel="Install"
actionLabel={$t("plugin_details.install")}
>
<CloudArrowDown size="24" class="text-neutral-400" />
</ListedPlugin>
@@ -359,7 +366,7 @@
{#if "Tacto Connect".toLowerCase().includes(query.toLowerCase())}
<div class="flex flex-row items-center mt-6 mb-2">
<h2 class="mx-2 font-semibold text-md text-neutral-400">Tacto</h2>
<Tooltip>Turn your phone or keyboard into a control centre for your computer</Tooltip>
<Tooltip>{$t("plugin_manager.tacto.tooltip")}</Tooltip>
</div>
<div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<ListedPlugin
@@ -374,9 +381,9 @@
download_url: undefined,
});
}}
actionLabel="Install"
actionLabel={$t("plugin_details.install")}
secondaryAction={() => window.open("https://tacto.live")}
secondaryActionLabel="Visit website"
secondaryActionLabel={$t("plugin_manager.visit_website")}
>
<svelte:fragment slot="secondary">
<ArrowSquareOut size="24" class="text-neutral-400" />
@@ -402,9 +409,9 @@
{#if choices}
<div class="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 mt-2 p-2 w-96 text-xs text-neutral-300 bg-neutral-700 border border-neutral-600 rounded-lg z-40">
<h3 class="mb-2 font-semibold text-lg text-center">Choose a release asset</h3>
<h3 class="mb-2 font-semibold text-lg text-center">{$t("plugin_manager.choose_asset")}</h3>
<div class="select-wrapper">
<select class="w-full bg-neutral-800!" bind:value={choice} aria-label="Release asset">
<select class="w-full bg-neutral-800!" bind:value={choice} aria-label={$t("plugin_manager.choose_asset.label")}>
{#each choices as choice, i}
<option value={i}>{choice.name}</option>
{/each}
@@ -414,7 +421,7 @@
class="mt-2 p-1 w-full text-sm text-neutral-300 bg-neutral-800 hover:bg-neutral-900 transition-colors border border-neutral-600 rounded-lg"
on:click={finishChoice}
>
Install
{$t("plugin_details.install")}
</button>
</div>
{/if}
+34 -29
View File
@@ -9,6 +9,7 @@
import Trash from "phosphor-svelte/lib/Trash";
import Popup from "./Popup.svelte";
import { t } from "$lib/i18n";
import { inspectedInstance } from "$lib/propertyInspector";
import { invoke } from "@tauri-apps/api/core";
@@ -85,14 +86,14 @@
// Check if a profile with the new ID already exists
const allProfiles = Object.values(folders).flat();
if (allProfiles.includes(newId)) {
message(`A profile with the ID "${newId}" already exists.`, { title: "Failed to rename profile" });
message($t("profile_manager.rename.exists", { id: newId }), { title: $t("profile_manager.rename.failed"), buttons: { ok: $t("dialog.ok") } });
return;
}
try {
await invoke("rename_profile", { device: device.id, oldId, newId, retain: false });
} catch (error: any) {
message(error, { title: "Failed to rename profile" });
message(error, { title: $t("profile_manager.rename.failed"), buttons: { ok: $t("dialog.ok") } });
console.error(error);
}
@@ -125,14 +126,14 @@
$: if (renameInput) renameInput.focus();
async function duplicateProfile(id: string) {
let newId = id + " Copy";
let newId = id + $t("profile_manager.duplicate.suffix");
// Check if a profile with the new ID already exists
const allProfiles = Object.values(folders).flat();
let counter = 1;
while (allProfiles.includes(newId)) {
counter++;
newId = `${id} Copy ${counter}`;
newId = `${id}${$t("profile_manager.duplicate.suffix")} ${counter}`;
}
await invoke("rename_profile", { device: device.id, oldId: id, newId, retain: true });
@@ -188,7 +189,7 @@
<div class="select-profile-wrapper">
<span bind:this={measure} class="invisible fixed whitespace-pre pointer-events-none" aria-hidden="true"></span>
<select bind:value style:width="{selectWidth}px" aria-label="Profile">
<select bind:value style:width="{selectWidth}px" aria-label={$t("profile_manager.label")}>
{#each Object.entries(folders).sort() as [id, profiles]}
{#if id && profiles.length}
<optgroup label={id}>
@@ -202,7 +203,7 @@
{/each}
{/if}
{/each}
<option value="opendeck_edit_profiles">Edit...</option>
<option value="opendeck_edit_profiles">{$t("profile_manager.edit")}</option>
</select>
</div>
@@ -216,8 +217,8 @@
}}
/>
<Popup show={showPopup} label="{device.name} profiles">
<button class="mr-1 float-right text-xl text-neutral-300" on:click={() => showPopup = false} aria-label="Close">✕</button>
<Popup show={showPopup} label="{device.name} {$t('profile_manager.profiles')}">
<button class="mr-1 float-right text-xl text-neutral-300" on:click={() => showPopup = false} aria-label={$t("settings.close")}>✕</button>
<h2 class="text-xl font-semibold text-neutral-300">{device.name}</h2>
<div class="flex flex-row mt-2 mb-1">
@@ -225,8 +226,8 @@
bind:this={nameInput}
pattern="[a-zA-Z0-9_ ]+(\/[a-zA-Z0-9_ ]+)?"
class="grow p-2 text-neutral-300 invalid:text-red-400 bg-neutral-700 border-l border-y border-neutral-600 rounded-l-lg"
placeholder='Profile name or "folder/name"'
aria-label="Profile name"
placeholder={$t("profile_manager.create.placeholder")}
aria-label={$t("profile_manager.create.label")}
/>
<button
@@ -239,13 +240,13 @@
}}
class="px-4 text-neutral-300 bg-neutral-900 hover:bg-neutral-800 transition-colors border-r border-y border-neutral-600 rounded-r-lg"
>
Create
{$t("profile_manager.create")}
</button>
<button
class="ml-2 px-4 flex items-center text-neutral-300 bg-neutral-900 hover:bg-neutral-800 transition-colors border border-neutral-600 rounded-lg"
on:click={() => showApplicationManager = true}
aria-label="Application profiles"
aria-label={$t("profile_manager.application_profiles")}
>
<Browsers size={24} />
</button>
@@ -277,23 +278,23 @@
if (e.key === "Enter") saveRenamedProfile(profile);
}}
/>
<button on:click={() => saveRenamedProfile(profile)} title="Save" aria-label="Save">
<button on:click={() => saveRenamedProfile(profile)} title={$t("profile_manager.save")} aria-label={$t("profile_manager.save")}>
<FloppyDisk size="20" class="text-green-500" />
</button>
{:else}
<label class="grow text-neutral-400" for={`profile-${encodeURIComponent(profile)}`}>{id ? profile.split("/")[1] : profile}</label>
<button on:click={() => duplicateProfile(profile)} title="Duplicate" aria-label="Duplicate">
<button on:click={() => duplicateProfile(profile)} title={$t("profile_manager.duplicate")} aria-label={$t("profile_manager.duplicate")}>
<Copy size="20" class="text-neutral-400" />
</button>
{#if profile != value}
<button
on:click={() => renamingProfile = newId = profile}
title="Rename"
aria-label="Rename"
title={$t("profile_manager.rename")}
aria-label={$t("profile_manager.rename")}
>
<Pencil size="20" class="text-neutral-400" />
</button>
<button on:click={() => deleteProfile(profile)} title="Delete" aria-label="Delete">
<button on:click={() => deleteProfile(profile)} title={$t("profile_manager.delete")} aria-label={$t("profile_manager.delete")}>
<Trash size="20" class="text-neutral-400" />
</button>
{/if}
@@ -304,19 +305,23 @@
</div>
</Popup>
<Popup show={showApplicationManager} label="Application profiles">
<button class="mr-1 float-right text-xl text-neutral-300" on:click={() => showApplicationManager = false} aria-label="Close">✕</button>
<Popup show={showApplicationManager} label={$t("profile_manager.application_profiles")}>
<button class="mr-1 float-right text-xl text-neutral-300" on:click={() => showApplicationManager = false} aria-label={$t("settings.close")}>✕</button>
<h2 class="text-xl font-semibold text-neutral-300">{device.name}</h2>
<span class="text-sm text-neutral-400">If your application isn't listed, try switching to it and back again.</span>
<span class="text-sm text-neutral-400">The 'default profile' will activate when the focussed application has no profile associated with it.</span>
<span class="text-sm text-neutral-400">{$t("profile_manager.application_profiles.hint.1")}</span>
<span class="text-sm text-neutral-400">{$t("profile_manager.application_profiles.hint.2")}</span>
<table class="w-full text-neutral-300 divide-y divide-neutral-500!">
{#each Object.entries(applicationProfiles).sort((a, b) => a[0] == "opendeck_default" ? -1 : b[0] == "opendeck_default" ? 1 : a[0].localeCompare(b[0])) as [appName, devices]}
{#if devices[device.id]}
<tr class="h-12">
<td>{appName == "opendeck_default" ? "Default profile" : appName}:</td>
<td>{appName == "opendeck_default" ? $t("profile_manager.default_profile") : appName}:</td>
<td class="select-wrapper">
<select bind:value={applicationProfiles[appName][device.id]} class="w-full" aria-label="{appName == 'opendeck_default' ? 'Default profile' : appName} profile">
<select
bind:value={applicationProfiles[appName][device.id]}
class="w-full"
aria-label={$t("profile_manager.application_profiles.aria", { name: appName == "opendeck_default" ? $t("profile_manager.default_profile") : appName })}
>
{#each Object.entries(folders) as [id, profiles]}
{#if id && profiles.length}
<optgroup label={id}>
@@ -331,7 +336,7 @@
{/if}
{/each}
<option disabled>──────────</option>
<option value={undefined}>Remove application</option>
<option value={undefined}>{$t("profile_manager.remove_application")}</option>
</select>
</td>
</tr>
@@ -339,10 +344,10 @@
{/each}
<tr class="h-12">
<td class="w-48 select-wrapper">
<select bind:value={applicationsAddAppName} class="w-full" aria-label="Select application">
<option selected disabled value="opendeck_select_application">Select application...</option>
<select bind:value={applicationsAddAppName} class="w-full" aria-label={$t("profile_manager.select_application")}>
<option selected disabled value="opendeck_select_application">{$t("profile_manager.select_application.placeholder")}</option>
{#if !applicationProfiles["opendeck_default"] || !applicationProfiles["opendeck_default"][device.id]}
<option value="opendeck_default">Default profile</option>
<option value="opendeck_default">{$t("profile_manager.default_profile")}</option>
{#if applications.filter((appName) => !applicationProfiles[appName] || !applicationProfiles[appName][device.id]).length > 0}
<option disabled>──────────</option>
{/if}
@@ -355,8 +360,8 @@
</select>
</td>
<td class="w-96 select-wrapper">
<select bind:value={applicationsAddProfile} class="w-full" aria-label="Select profile">
<option selected disabled value="opendeck_select_profile">Select profile...</option>
<select bind:value={applicationsAddProfile} class="w-full" aria-label={$t("profile_manager.select_profile")}>
<option selected disabled value="opendeck_select_profile">{$t("profile_manager.select_profile.placeholder")}</option>
{#each Object.entries(folders) as [id, profiles]}
{#if id && profiles.length}
<optgroup label={id}>
+2 -1
View File
@@ -3,6 +3,7 @@
import type { DeviceInfo } from "$lib/DeviceInfo";
import type { Profile } from "$lib/Profile";
import { t } from "$lib/i18n";
import { getWebserverUrl, getWebSocketPort } from "$lib/ports";
import { inspectedInstance } from "$lib/propertyInspector";
@@ -189,7 +190,7 @@
{#each instances as instance (instance.context)}
{#if instance.action.property_inspector}
<iframe
title="Property inspector"
title={$t("property_inspector.title")}
class="w-full h-full hidden"
class:block!={$inspectedInstance == instance.context}
src={getWebserverUrl(instance.action.property_inspector + "|opendeck_property_inspector")}
+42 -45
View File
@@ -8,6 +8,7 @@
import Popup from "./Popup.svelte";
import Tooltip from "./Tooltip.svelte";
import { t } from "$lib/i18n";
import { settings } from "$lib/settings";
import { PRODUCT_NAME } from "$lib/singletons";
@@ -38,22 +39,21 @@
async function backupConfig() {
await message(
"You will be prompted to select a location to save the backup to. The config directory will be compressed and saved there. This may take a while if you have many plugins or profiles.",
{ title: "Backing up configuration" },
$t("settings.backup_config.prompt"),
{ title: $t("settings.backup_config.title"), buttons: { ok: $t("dialog.ok") } },
);
if (await invoke("backup_config_directory")) {
await message(
"Successfully backed up the config directory.",
{ title: "Backup complete" },
$t("settings.backup_config.success.prompt"),
{ title: $t("settings.backup_config.success.title"), buttons: { ok: $t("dialog.ok") } },
);
}
}
async function restoreConfig() {
await message(
"You will be prompted to select a location to restore the backup from. This may take a while if you have many plugins or profiles. The application will restart after the restoration is complete.\n\n" +
"You may encounter issues if you attempt to restore a backup from a different operating system or architecture.",
{ title: "Restoring configuration" },
$t("settings.restore_config.prompt"),
{ title: $t("settings.restore_config.title"), buttons: { ok: $t("dialog.ok") } },
);
await invoke("restore_config_directory");
}
@@ -63,7 +63,7 @@
class="px-3 py-1 text-sm text-neutral-300 bg-neutral-700 hover:bg-neutral-600 transition-colors border border-neutral-600 rounded-lg"
on:click={() => showPopup = true}
>
Settings
{$t("settings.button")}
</button>
<svelte:window
@@ -72,12 +72,12 @@
}}
/>
<Popup show={showPopup} label="Settings">
<button class="mr-2 my-1 float-right text-xl text-neutral-300" on:click={() => showPopup = false} aria-label="Close">✕</button>
<h2 class="m-2 font-semibold text-xl text-neutral-300">Settings</h2>
<Popup show={showPopup} label={$t("settings.button")}>
<button class="mr-2 my-1 float-right text-xl text-neutral-300" on:click={() => showPopup = false} aria-label={$t("settings.close")}>✕</button>
<h2 class="m-2 font-semibold text-xl text-neutral-300">{$t("settings.button")}</h2>
{#if $settings}
<div class="flex flex-row items-center m-2 space-x-2">
<label for="settings-language" class="text-neutral-400">Language:</label>
<label for="settings-language" class="text-neutral-400">{$t("settings.language")}</label>
<div class="select-wrapper">
<select bind:value={$settings.language} class="w-32" id="settings-language">
<option value="en">English</option>
@@ -90,85 +90,83 @@
</select>
</div>
<Tooltip>
{PRODUCT_NAME} itself is not yet translated. Changing this setting will translate the text from installed plugins into your language for those that support it.
{$t("settings.language.tooltip", { PRODUCT_NAME })}
</Tooltip>
</div>
<div class="flex flex-row items-center m-2 space-x-2">
<label for="settings-brightness" class="text-neutral-400">Device brightness:</label>
<label for="settings-brightness" class="text-neutral-400">{$t("settings.brightness")}</label>
<input type="range" min="0" max="100" bind:value={$settings.brightness} id="settings-brightness" />
</div>
<div class="flex flex-row items-center m-2 space-x-2">
<label for="settings-sleep_timeout_minutes" class="text-neutral-400">Sleep after inactivity:</label>
<label for="settings-sleep_timeout_minutes" class="text-neutral-400">{$t("settings.sleep_timeout_minutes")}</label>
<input type="number" min="0" bind:value={$settings.sleep_timeout_minutes} class="w-12 px-1 text-neutral-300 border border-neutral-600 rounded-lg" id="settings-sleep_timeout_minutes" />
<span class="text-neutral-400">minutes</span>
<Tooltip> This option controls how many minutes of inactivity will cause devices to enter sleep mode, where a value of 0 disables sleeping automatically. </Tooltip>
<span class="text-neutral-400">{$t("settings.sleep_timeout_minutes.minutes")}</span>
<Tooltip> {$t("settings.sleep_timeout_minutes.tooltip")} </Tooltip>
</div>
<div class="flex flex-row items-center m-2 space-x-2">
<label for="settings-sleep_when_computer_locked" class="text-neutral-400">Sleep when computer is locked:</label>
<label for="settings-sleep_when_computer_locked" class="text-neutral-400">{$t("settings.sleep_when_computer_locked")}</label>
<input type="checkbox" bind:checked={$settings.sleep_when_computer_locked} id="settings-sleep_when_computer_locked" />
<Tooltip> If this option is enabled, devices will enter sleep mode when the computer locks and all key presses will be suppressed until it is unlocked. </Tooltip>
<Tooltip> {$t("settings.sleep_when_computer_locked.tooltip")} </Tooltip>
</div>
<div class="flex flex-row items-center m-2 space-x-2">
<label for="settings-rotation" class="text-neutral-400">Image rotation:</label>
<label for="settings-rotation" class="text-neutral-400">{$t("settings.rotation")}</label>
<input type="range" min="0" max="270" step="90" bind:value={$settings.rotation} id="settings-rotation" />
</div>
<div class="flex flex-row items-center m-2 space-x-2">
<label for="settings-background" class="text-neutral-400">Run in background:</label>
<label for="settings-background" class="text-neutral-400">{$t("settings.background")}</label>
<input type="checkbox" bind:checked={$settings.background} id="settings-background" />
<Tooltip> If this option is enabled, {PRODUCT_NAME} will minimise to the tray and run in the background. </Tooltip>
<Tooltip>{$t("settings.background.tooltip", { PRODUCT_NAME })}</Tooltip>
</div>
<div class="flex flex-row items-center m-2 space-x-2">
<label for="settings-autolaunch" class="text-neutral-400">Start at login:</label>
<label for="settings-autolaunch" class="text-neutral-400">{$t("settings.autolaunch")}</label>
<input type="checkbox" bind:checked={$settings.autolaunch} id="settings-autolaunch" />
<Tooltip>
If this option is enabled, {PRODUCT_NAME} will automatically start at login.
{$t("settings.autolaunch.tooltip.1", { PRODUCT_NAME })}
{#if buildInfo?.split("</summary>")[0]?.includes("linux")}
<br />
If you used Flatpak to install {PRODUCT_NAME}, this option may not function as intended.
{$t("settings.autolaunch.tooltip.2", { PRODUCT_NAME })}
{/if}
</Tooltip>
</div>
<div class="flex flex-row items-center m-2 space-x-2">
<label for="settings-updatecheck" class="text-neutral-400">Check for updates:</label>
<label for="settings-updatecheck" class="text-neutral-400">{$t("settings.updatecheck")}</label>
<input type="checkbox" bind:checked={$settings.updatecheck} id="settings-updatecheck" />
</div>
<div class="flex flex-row items-center m-2 space-x-2">
<label for="settings-statistics" class="text-neutral-400">Contribute statistics:</label>
<label for="settings-statistics" class="text-neutral-400">{$t("settings.statistics")}</label>
<input type="checkbox" bind:checked={$settings.statistics} id="settings-statistics" />
</div>
{#if !buildInfo?.split("</summary>")[0]?.includes("windows")}
<div class="flex flex-row items-center m-2 space-x-2">
<label for="settings-separatewine" class="text-neutral-400">Create separate Wine prefixes:</label>
<label for="settings-separatewine" class="text-neutral-400">{$t("settings.separatewine")}</label>
<input type="checkbox" bind:checked={$settings.separatewine} id="settings-separatewine" />
<Tooltip>
If this option is enabled, {PRODUCT_NAME} will create a separate Wine prefix for each plugin that runs under Wine. Please note that each Wine prefix is quite large - around 300MB when
initialised.
{$t("settings.separatewine.tooltip", { PRODUCT_NAME })}
</Tooltip>
</div>
{/if}
<div class="flex flex-row items-center m-2 space-x-2">
<label for="settings-developer" class="text-neutral-400">Enable developer mode:</label>
<label for="settings-developer" class="text-neutral-400">{$t("settings.developer")}</label>
<input type="checkbox" bind:checked={$settings.developer} id="settings-developer" />
<Tooltip>
This option enables features that make plugin development and debugging easier. Additionally, this option exposes all file paths on your device on the local webserver to allow symbolic linking
of plugins, so you should disable it if it is not in use.
{$t("settings.developer.tooltip")}
</Tooltip>
</div>
<div class="flex flex-row items-center m-2 space-x-2">
<label for="settings-disableelgato" class="text-neutral-400">Disable Elgato device discovery:</label>
<label for="settings-disableelgato" class="text-neutral-400">{$t("settings.disableelgato")}</label>
<input type="checkbox" bind:checked={$settings.disableelgato} id="settings-disableelgato" />
<Tooltip> This option disables discovery of Elgato devices so that they can be managed by other software. </Tooltip>
<Tooltip>{$t("settings.disableelgato.tooltip")}</Tooltip>
</div>
{/if}
@@ -179,47 +177,46 @@
on:click={() => backupConfig()}
>
<ClockCounterClockwise class="mr-1" />
Back up config
{$t("settings.backup_config.button")}
</button>
<button
class="flex flex-row items-center px-2 py-1 text-sm text-neutral-300 bg-neutral-700 hover:bg-neutral-600 transition-colors border border-neutral-600 rounded-lg"
on:click={() => restoreConfig()}
>
<ClockClockwise class="mr-1" />
Restore config
{$t("settings.restore_config.button")}
</button>
<button
class="flex flex-row items-center px-2 py-1 text-sm text-neutral-300 bg-neutral-700 hover:bg-neutral-600 transition-colors border border-neutral-600 rounded-lg"
on:click={() => invoke("open_config_directory")}
>
<Gear class="mr-1" />
Open config
{$t("settings.open_config")}
</button>
<button
class="flex flex-row items-center px-2 py-1 text-sm text-neutral-300 bg-neutral-700 hover:bg-neutral-600 transition-colors border border-neutral-600 rounded-lg"
on:click={() => invoke("open_log_directory")}
>
<Scroll class="mr-1" />
Open logs
{$t("settings.open_logs")}
</button>
</div>
<span class="text-xs text-neutral-400">
{@html buildInfo}
</span>
<div class="absolute bottom-6 flex flex-row items-center text-sm text-neutral-400">
<span class="mr-1">
Please leave a
<button on:click={() => invoke("open_url", { url: "https://github.com/nekename/OpenDeck" })} class="underline">star on GitHub</button>
{$t("settings.footer.1")}
<button on:click={() => invoke("open_url", { url: "https://github.com/nekename/OpenDeck" })} class="underline">{$t("settings.footer.2")}</button>
</span>
<Star weight="fill" fill="yellow" />
<span class="mx-1">
or
<button on:click={() => invoke("open_url", { url: "https://github.com/sponsors/nekename" })} class="underline">sponsor me</button>
{$t("settings.footer.3")}
<button on:click={() => invoke("open_url", { url: "https://github.com/sponsors/nekename" })} class="underline">{$t("settings.footer.4")}</button>
</span>
<Heart weight="fill" fill="fuchsia" />
<span class="ml-1">for my work :)</span>
<span class="ml-1">{$t("settings.footer.5")}</span>
</div>
</div>
</Popup>
+43
View File
@@ -0,0 +1,43 @@
import { derived, writable } from "svelte/store";
const FALLBACK_LOCALE = "en";
const translationModules = import.meta.glob("../../translations/*.json", {
eager: true,
import: "default",
}) as Record<string, Record<string, string>>;
const translations: Record<string, Record<string, string>> = Object.fromEntries(
Object.entries(translationModules).map(([path, translations]) => {
const locale = path.match(/\/([^/]+)\.json$/)?.[1];
if (!locale) {
throw new Error(`Could not determine locale from translation file path: ${path}`);
}
return [locale, translations];
}),
);
export const locale = writable("en");
function translate(locale: string, key: string, vars: { [x: string]: any }) {
if (!key) throw new Error("no key provided to $t()");
if (!locale) throw new Error("no locale provided to $t()");
// Grab the translation from the translation object.
const dictionary = translations as Record<string, Record<string, string>>;
let text = dictionary[locale]?.[key] ?? dictionary[FALLBACK_LOCALE]?.[key];
if (!text) throw new Error(`no translation found for ${locale}.${key} or ${FALLBACK_LOCALE}.${key}`);
// Replace any passed in variables in the translation string.
Object.keys(vars).map((k) => {
const regex = new RegExp(`{{${k}}}`, "g");
text = text.replace(regex, vars[k]);
});
return text;
}
export const t = derived(locale, ($locale) => (key: string, vars = {}) => translate($locale, key, vars));
+2
View File
@@ -16,6 +16,7 @@ export type Settings = {
import { invoke } from "@tauri-apps/api/core";
import { type Writable, writable } from "svelte/store";
import { locale } from "./i18n.ts";
export const settings: Writable<Settings | null> = writable(null);
(async () => settings.set(await invoke("get_settings")))();
@@ -23,6 +24,7 @@ export const localisations: Writable<{ [plugin: string]: any } | null> = writabl
settings.subscribe(async (value) => {
if (value) {
await invoke("set_settings", { settings: value });
locale.set(value.language);
localisations.set(await invoke("get_localisations", { locale: value.language }));
}
});
+151
View File
@@ -0,0 +1,151 @@
{
"action_list.hint": "Verwende die Pfeiltasten, um zwischen Aktionen innerhalb einer Kategorie zu navigieren.",
"action_list.search_placeholder": "Durchsuche Aktionen",
"action_view.drag_copy": "Ziehe eine neue Aktion hierher oder kopiere eine mit Strg+C und füge sie mit Strg+V ein.",
"action_view.drag_paste": "Ziehe oder füge Aktionen hier ein",
"device_selector.choose_device": "Wähle ein Gerät...",
"device_selector.device": "Gerät",
"device_view.encoder": "Drehknauf",
"device_view.grid_description": "Verwende die Pfeiltasten, um zwischen den Tasten zu navigieren. Bewegen auf eine Taste zeigt ihre Eigenschaften an.",
"device_view.key": "Taste",
"device_view.touchpoint": "Touchpunkt",
"dialog.no": "Nein",
"dialog.ok": "OK",
"dialog.yes": "Ja",
"instance_editor.alignment": "Ausrichtung",
"instance_editor.alignment.bottom": "Unten",
"instance_editor.alignment.middle": "Mitte",
"instance_editor.alignment.top": "Oben",
"instance_editor.background": "Hintergrund festlegen",
"instance_editor.colour": "Farbe",
"instance_editor.font": "Schriftart",
"instance_editor.font.placeholder": "Schriftfamilie",
"instance_editor.font.size": "Schriftgröße",
"instance_editor.image.hint": "Klicke zum Auswählen eines Bildes oder Rechtsklick zum Zurücksetzen auf das Standardbild.",
"instance_editor.image.n": "Bild für Zustand {{n}}",
"instance_editor.image.scale.decrease": "Bildgröße verringern",
"instance_editor.image.scale.increase": "Bildgröße vergrößern",
"instance_editor.outline": "Konturbreite",
"instance_editor.show": "Anzeige",
"instance_editor.solid_colour": "Einfärbig",
"instance_editor.state": "Zustand",
"instance_editor.state.n": "Zustand {{n}}",
"instance_editor.stroke": "Kontur",
"instance_editor.text": "Text",
"key.copy": "Kopieren",
"key.delete": "Entfernen",
"key.edit": "Bearbeiten",
"key.paste": "Einfügen",
"no_devices_detected.check_connection": "Stelle sicher, dass deine Geräte ordnungsgemäß verbunden sind und du Zugriff darauf hast.",
"no_devices_detected.check_udev": "Stelle sicher, dass du die korrekten udev-Subsystemregeln installiert hast.",
"no_devices_detected.install_plugin": "Du musst möglicherweise ein Plugin installieren, das Unterstützung für dein Gerät hinzufügt.",
"no_devices_detected.restart": "{{PRODUCT_NAME}} neu starten",
"no_devices_detected.title": "Keine Geräte gefunden",
"parent_action.children": "Unteraktionen",
"parent_action.multi": "Multiaktionen",
"parent_action.remove": "{{name}} entfernen",
"parent_action.toggle": "Umschaltaktion",
"plugin_details.by": "von",
"plugin_details.download_latest": "Neueste Version von GitHub herunterladen",
"plugin_details.install": "Installieren",
"plugin_details.loading": "Lade Plugin-Details...",
"plugin_details.readme.not_found": "**Plugin-README-Datei nicht gefunden**\n\n[Plugin auf GitHub ansehen](https://github.com/{{repo}})",
"plugin_details.title": "{details.name} Plugin-Details",
"plugin_manager.available": "verfügbar:",
"plugin_manager.button": "Plugins",
"plugin_manager.choose_asset": "Release-Asset auswählen",
"plugin_manager.choose_asset.label": "Release-Asset",
"plugin_manager.elgato": "Elgato App Store-Archiv",
"plugin_manager.elgato.load": "Elgato App Store-Archiv laden",
"plugin_manager.elgato.tooltip": "Archivierte Plugins aus dem Elgato App Store (mittlerweile ersetzt durch den Elgato Marketplace)",
"plugin_manager.install_from_file": "Aus Datei installieren",
"plugin_manager.install.error": "Installation von \"{{name}}\" fehlgeschlagen",
"plugin_manager.install.prompt": "Die Installation des Plugins kann eine Weile dauern.",
"plugin_manager.install.success": "\"{{name}}\" wurde erfolgreich installiert.",
"plugin_manager.install.success.title": "\"{{name}}\" installiert",
"plugin_manager.install.title": "\"{{name}}\" installieren?",
"plugin_manager.installed": "Installierte Plugins",
"plugin_manager.loading.elgato": "Lade Elgato App-Store-Archiv Plugin Liste...",
"plugin_manager.loading.open_source": "Lade Open-Source-Plugin-Liste...",
"plugin_manager.open_source": "Open-Source-Plugins",
"plugin_manager.open_source.tooltip": "Open-Source-Plugins, die aus den Releases des Autors heruntergeladen werden",
"plugin_manager.plugin_settings": "Einstellungen",
"plugin_manager.reload": "Neu laden",
"plugin_manager.remove": "Entfernen",
"plugin_manager.remove.error": "Entfernen von \"{{name}}\" fehlgeschlagen",
"plugin_manager.remove.prompt": "Bist du dir sicher, dass du \"{{name}}\" entfernen möchtest?",
"plugin_manager.remove.success": "\"{{name}}\" wurde erfolgreich entfernt.",
"plugin_manager.remove.success.title": "\"{{name}}\" entfernt",
"plugin_manager.remove.title": "\"{{name}}\" entfernen?",
"plugin_manager.search": "Plugins durchsuchen",
"plugin_manager.store": "Plugin-Store",
"plugin_manager.tacto.tooltip": "Verwandle dein Telefon oder deine Tastatur in ein Steuerzentrum für deinen Computer",
"plugin_manager.title": "Plugins verwalten",
"plugin_manager.view_details": "Details anzeigen",
"plugin_manager.view_logs": "Protokolle anzeigen",
"plugin_manager.visit_website": "Website besuchen",
"plugin_manager.warning": "Wenn du Probleme mit einem Plugin hast, wende dich bitte zuerst an einen der {{PRODUCT_NAME}}-Supportkanäle, bevor du den Plugin-Entwickler kontaktierst.",
"profile_manager.application_profiles": "Anwendungsprofile",
"profile_manager.application_profiles.aria": "{{name}} Profil",
"profile_manager.application_profiles.hint.1": "Wenn deine Anwendung nicht aufgelistet wird, versuche zu ihr und zurück zu wechseln.",
"profile_manager.application_profiles.hint.2": "Das \"Standardprofil\" wird aktiviert, wenn die aktuell aktive Anwendung kein zugeordnetes Profil hat.",
"profile_manager.create": "Erstellen",
"profile_manager.create.label": "Profilname",
"profile_manager.create.placeholder": "Profilname oder \"Ordner/Name\"",
"profile_manager.default_profile": "Standardprofil",
"profile_manager.delete": "Löschen",
"profile_manager.duplicate": "Duplizieren",
"profile_manager.duplicate.suffix": " Kopie",
"profile_manager.edit": "Bearbeiten...",
"profile_manager.label": "Profile",
"profile_manager.profiles": "Profile",
"profile_manager.remove_application": "Anwendung entfernen",
"profile_manager.rename": "Umbenennen",
"profile_manager.rename.exists": "Ein Profil mit der ID \"{{id}}\" existiert bereits.",
"profile_manager.rename.failed": "Profil umbenennen fehlgeschlagen",
"profile_manager.save": "Speichern",
"profile_manager.select_application": "Anwendung auswählen",
"profile_manager.select_application.placeholder": "Anwendung auswählen...",
"profile_manager.select_profile": "Profil auswählen",
"profile_manager.select_profile.placeholder": "Profil auswählen...",
"property_inspector.title": "Eigenschaftsanzeige",
"settings.autolaunch": "Beim Anmelden starten:",
"settings.autolaunch.tooltip.1": "Wenn diese Option aktiviert ist, wird {{PRODUCT_NAME}} automatisch beim Anmelden gestartet.",
"settings.autolaunch.tooltip.2": "Wenn du {{PRODUCT_NAME}} mit Flatpak installiert hast, funktioniert diese Option möglicherweise nicht wie vorgesehen",
"settings.background": "Im Hintergrund weiterlaufen lassen:",
"settings.background.tooltip": "Wenn diese Option aktiviert ist, wird {{PRODUCT_NAME}} in das System-Tray minimiert und im Hintergrund ausgeführt.",
"settings.backup_config.button": "Konfiguration sichern",
"settings.backup_config.prompt": "Du wirst aufgefordert, einen Speicherort zum Speichern der Sicherung auszuwählen. Der Konfigurationsordner wird komprimiert und dort gespeichert. Dies kann eine Weile dauern, wenn du viele Plugins oder Profile hast.",
"settings.backup_config.success.prompt": "Die Konfiguration wurde erfolgreich gesichert.",
"settings.backup_config.success.title": "Sicherung abgeschlossen",
"settings.backup_config.title": "Sichern der Konfiguration",
"settings.brightness": "Gerätehelligkeit:",
"settings.button": "Einstellungen",
"settings.close": "Schließen",
"settings.developer": "Entwicklermodus aktivieren:",
"settings.developer.tooltip": "Diese Option aktiviert Funktionen, die die Entwicklung und das Debugging von Plugins erleichtern. Zusätzlich macht diese Option alle Dateipfade auf Ihrem Gerät im lokalen Webserver verfügbar, um symbolische Verknüpfungen von Plugins zu ermöglichen. Deaktiviere sie, wenn sie nicht verwendet wird.",
"settings.disableelgato": "Elgato-Geräteerkennung deaktivieren:",
"settings.disableelgato.tooltip": "Diese Option deaktiviert die Erkennung von Elgato-Geräten, sodass sie von anderer Software verwaltet werden können.",
"settings.footer.1": "Bitte hinterlasse einen ",
"settings.footer.2": "Stern auf GitHub ",
"settings.footer.3": "oder ",
"settings.footer.4": "unterstütze mich ",
"settings.footer.5": "für meine Arbeit :)",
"settings.language": "Sprache:",
"settings.language.tooltip": "{{PRODUCT_NAME}} selbst ist noch nicht komplett übersetzt. Wenn du diese Einstellung änderst, wird der Text installierter Plugins in deine Sprache übersetzt, sofern diese dies unterstützen.",
"settings.open_config": "Konfigurationsordner öffnen",
"settings.open_logs": "Protokollordner öffnen",
"settings.restore_config.button": "Konfiguration wiederherstellen",
"settings.restore_config.prompt": "Du wirst aufgefordert, einen Speicherort zum Wiederherstellen der Sicherung auszuwählen. Dies kann eine Weile dauern, wenn du viele Plugins oder Profile hast. Die Anwendung wird nach der Wiederherstellung neu gestartet.\n\nDu kannst Probleme haben, wenn du versuchst, eine Sicherung von einem anderen Betriebssystem oder Architektur zu wiederherstellen.",
"settings.restore_config.title": "Wiederherstellen der Konfiguration",
"settings.rotation": "Bildrotation:",
"settings.separatewine": "Separate Wine Präfixe erstellen:",
"settings.separatewine.tooltip": "Wenn diese Option aktiviert ist, erstellt {{PRODUCT_NAME}} für jedes Plugin, das unter Wine läuft, ein separates Wine-Präfix. Bitte beachte, dass jedes Wine-Präfix recht groß ist – im initialisierten Zustand etwa 300 MB.",
"settings.sleep_timeout_minutes": "Ruhezustand nach Inaktivität:",
"settings.sleep_timeout_minutes.minutes": "Minuten",
"settings.sleep_timeout_minutes.tooltip": "Diese Option legt fest, nach wie vielen Minuten ohne Aktivität Geräte in den Schlafmodus wechseln; ein Wert von 0 deaktiviert den automatischen Schlafmodus.",
"settings.sleep_when_computer_locked": "Ruhezustand, wenn der Computer gesperrt ist:",
"settings.sleep_when_computer_locked.tooltip": "Wenn diese Option aktiviert ist, wechseln die Geräte beim Sperren des Computers in den Ruhezustand, und alle Tastendrücke werden bis zum Entsperren des Computers unterdrückt.",
"settings.statistics": "Statistiken übermitteln:",
"settings.updatecheck": "Automatisch auf Updates prüfen:"
}
+151
View File
@@ -0,0 +1,151 @@
{
"action_list.hint": "Use arrow keys to navigate between actions within a category.",
"action_list.search_placeholder": "Search actions",
"action_view.drag_copy": "Drag a new action here or copy one with Control+C and paste with Control+V.",
"action_view.drag_paste": "Drop or paste actions here",
"device_selector.choose_device": "Choose a device...",
"device_selector.device": "Device",
"device_view.encoder": "Encoder",
"device_view.grid_description": "Use arrow keys to navigate between keys. Moving to a key will display its property inspector.",
"device_view.key": "Key",
"device_view.touchpoint": "Touch point",
"dialog.no": "No",
"dialog.ok": "OK",
"dialog.yes": "Yes",
"instance_editor.alignment": "Alignment",
"instance_editor.alignment.bottom": "Bottom",
"instance_editor.alignment.middle": "Middle",
"instance_editor.alignment.top": "Top",
"instance_editor.background": "Set background",
"instance_editor.colour": "Colour",
"instance_editor.font": "Font",
"instance_editor.font.placeholder": "Font family",
"instance_editor.font.size": "Size",
"instance_editor.image.hint": "Click to select an image, or right-click to reset to the default image.",
"instance_editor.image.n": "State {{n}} image",
"instance_editor.image.scale.decrease": "Decrease image scale",
"instance_editor.image.scale.increase": "Increase image scale",
"instance_editor.outline": "Outline",
"instance_editor.show": "Show",
"instance_editor.solid_colour": "Use solid colour",
"instance_editor.state": "State",
"instance_editor.state.n": "State {{n}}",
"instance_editor.stroke": "Stroke",
"instance_editor.text": "Text",
"key.copy": "Copy",
"key.delete": "Delete",
"key.edit": "Edit",
"key.paste": "Paste",
"no_devices_detected.check_connection": "Make sure your devices are connected properly and you have permission to access them.",
"no_devices_detected.check_udev": "Ensure you have the correct udev subsystem rules installed.",
"no_devices_detected.install_plugin": "You may need to install a plugin that adds support for your device.",
"no_devices_detected.restart": "Restart {{PRODUCT_NAME}}",
"no_devices_detected.title": "No devices detected",
"parent_action.children": "children",
"parent_action.multi": "Multi Action",
"parent_action.remove": "Remove {{name}}",
"parent_action.toggle": "Toggle Action",
"plugin_details.by": "by",
"plugin_details.download_latest": "Download latest release from GitHub",
"plugin_details.install": "Install",
"plugin_details.loading": "Loading plugin details...",
"plugin_details.readme.not_found": "**Plugin README file not found**\n\n[View plugin on GitHub](https://github.com/{{repo}})",
"plugin_details.title": "{details.name} plugin details",
"plugin_manager.available": "available:",
"plugin_manager.button": "Plugins",
"plugin_manager.choose_asset": "Choose a release asset",
"plugin_manager.choose_asset.label": "Release asset",
"plugin_manager.elgato": "Elgato App Store archive",
"plugin_manager.elgato.load": "Load Elgato App Store archive",
"plugin_manager.elgato.tooltip": "Plugins archived from the Elgato App Store (now replaced by the Elgato Marketplace)",
"plugin_manager.install_from_file": "Install from file",
"plugin_manager.install.error": "Failed to install \"{{name}}\"",
"plugin_manager.install.prompt": "It may take a while to install the plugin.",
"plugin_manager.install.success": "Successfully installed \"{{name}}\".",
"plugin_manager.install.success.title": "Installed \"{{name}}\"",
"plugin_manager.install.title": "Install \"{{name}}\"?",
"plugin_manager.installed": "Installed plugins",
"plugin_manager.loading.elgato": "Loading Elgato App Store archive plugin list...",
"plugin_manager.loading.open_source": "Loading open-source plugin list...",
"plugin_manager.open_source": "Open-source plugins",
"plugin_manager.open_source.tooltip": "Open-source plugins downloaded from the author's releases",
"plugin_manager.plugin_settings": "Settings",
"plugin_manager.reload": "Reload",
"plugin_manager.remove": "Remove",
"plugin_manager.remove.error": "Failed to remove \"{{name}}\"",
"plugin_manager.remove.prompt": "Are you sure you want to remove \"{{name}}\"?",
"plugin_manager.remove.success": "Successfully removed \"{{name}}\".",
"plugin_manager.remove.success.title": "Removed \"{{name}}\"",
"plugin_manager.remove.title": "Remove \"{{name}}\"?",
"plugin_manager.search": "Search plugins",
"plugin_manager.store": "Plugin store",
"plugin_manager.tacto.tooltip": "Turn your phone or keyboard into a control centre for your computer",
"plugin_manager.title": "Manage plugins",
"plugin_manager.view_details": "View details",
"plugin_manager.view_logs": "View logs",
"plugin_manager.visit_website": "Visit website",
"plugin_manager.warning": "If you are experiencing issues with a plugin, please reach out on one of the {{PRODUCT_NAME}} support channels before attempting to contact the plugin developer.",
"profile_manager.application_profiles": "Application profiles",
"profile_manager.application_profiles.aria": "{{name}} profile",
"profile_manager.application_profiles.hint.1": "If your application isn't listed, try switching to it and back again.",
"profile_manager.application_profiles.hint.2": "The 'default profile' will activate when the focussed application has no profile associated with it.",
"profile_manager.create": "Create",
"profile_manager.create.label": "Profile name",
"profile_manager.create.placeholder": "Profile name or \"folder/name\"",
"profile_manager.default_profile": "Default profile",
"profile_manager.delete": "Delete",
"profile_manager.duplicate": "Duplicate",
"profile_manager.duplicate.suffix": " Copy",
"profile_manager.edit": "Edit...",
"profile_manager.label": "Profile",
"profile_manager.profiles": "profiles",
"profile_manager.remove_application": "Remove application",
"profile_manager.rename": "Rename",
"profile_manager.rename.exists": "A profile with the ID \"{{id}}\" already exists.",
"profile_manager.rename.failed": "Failed to rename profile",
"profile_manager.save": "Save",
"profile_manager.select_application": "Select application",
"profile_manager.select_application.placeholder": "Select application...",
"profile_manager.select_profile": "Select profile",
"profile_manager.select_profile.placeholder": "Select profile...",
"property_inspector.title": "Property inspector",
"settings.autolaunch": "Start at login:",
"settings.autolaunch.tooltip.1": "If this option is enabled, {{PRODUCT_NAME}} will automatically start at login.",
"settings.autolaunch.tooltip.2": "If you used Flatpak to install {{PRODUCT_NAME}}, this option may not function as intended.",
"settings.background": "Run in background:",
"settings.background.tooltip": "If this option is enabled, {{PRODUCT_NAME}} will minimise to the tray and run in the background.",
"settings.backup_config.button": "Back up config",
"settings.backup_config.prompt": "You will be prompted to select a location to save the backup to. The config directory will be compressed and saved there. This may take a while if you have many plugins or profiles.",
"settings.backup_config.success.prompt": "Successfully backed up the config directory.",
"settings.backup_config.success.title": "Backup complete",
"settings.backup_config.title": "Backing up configuration",
"settings.brightness": "Device brightness:",
"settings.button": "Settings",
"settings.close": "Close",
"settings.developer": "Enable developer mode:",
"settings.developer.tooltip": "This option enables features that make plugin development and debugging easier. Additionally, this option exposes all file paths on your device on the local webserver to allow symbolic linking of plugins, so you should disable it if it is not in use.",
"settings.disableelgato": "Disable Elgato device discovery:",
"settings.disableelgato.tooltip": "This option disables discovery of Elgato devices so that they can be managed by other software.",
"settings.footer.1": "Please leave a ",
"settings.footer.2": "star on GitHub ",
"settings.footer.3": "or ",
"settings.footer.4": "sponsor me ",
"settings.footer.5": "for my work :)",
"settings.language": "Language:",
"settings.language.tooltip": "{{PRODUCT_NAME}} itself is not yet completely translated. Changing this setting will translate the text from installed plugins into your language for those that support it.",
"settings.open_config": "Open config",
"settings.open_logs": "Open logs",
"settings.restore_config.button": "Restore config",
"settings.restore_config.prompt": "You will be prompted to select a location to restore the backup from. This may take a while if you have many plugins or profiles. The application will restart after the restoration is complete.\n\nYou may encounter issues if you attempt to restore a backup from a different operating system or architecture.",
"settings.restore_config.title": "Restoring configuration",
"settings.rotation": "Image rotation:",
"settings.separatewine": "Create separate Wine prefixes:",
"settings.separatewine.tooltip": "If this option is enabled, {{PRODUCT_NAME}} will create a separate Wine prefix for each plugin that runs under Wine. Please note that each Wine prefix is quite large - around 300MB when initialised.",
"settings.sleep_timeout_minutes": "Sleep after inactivity:",
"settings.sleep_timeout_minutes.minutes": "minutes",
"settings.sleep_timeout_minutes.tooltip": "This option controls how many minutes of inactivity will cause devices to enter sleep mode, where a value of 0 disables sleeping automatically.",
"settings.sleep_when_computer_locked": "Sleep when computer is locked:",
"settings.sleep_when_computer_locked.tooltip": "If this option is enabled, devices will enter sleep mode when the computer locks and all key presses will be suppressed until it is unlocked.",
"settings.statistics": "Contribute statistics:",
"settings.updatecheck": "Check for updates:"
}
+3
View File
@@ -13,5 +13,8 @@ export default defineConfig({
watch: {
ignored: ["**/src-tauri/**", "**/target/**"],
},
fs: {
allow: ["./translations"],
},
},
});