mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-25 14:59:21 +01:00
Fix untranslated strings (#756)
* add missing translations and translate page titles * fix: actually use the declared localized variables * lint and prettier fixes * add missing translations for toasts and confirms * use components for shift/enter keys, add pluralization for photos, and fix primary photo conditional * remove prop defaults since we're computing these anyways
This commit is contained in:
@@ -8,10 +8,18 @@
|
||||
<slot />
|
||||
|
||||
<DialogFooter>
|
||||
<span class="flex items-center gap-1 text-sm">
|
||||
Use <Shortcut size="sm" :keys="['Shift']" /> + <Shortcut size="sm" :keys="['Enter']" /> to create and add
|
||||
another.
|
||||
</span>
|
||||
<i18n-t
|
||||
keypath="components.app.create_modal.createAndAddAnother"
|
||||
tag="span"
|
||||
class="flex items-center gap-1 text-sm"
|
||||
>
|
||||
<template #shiftKey>
|
||||
<Shortcut size="sm" :keys="[$t('components.app.create_modal.shift')]" />
|
||||
</template>
|
||||
<template #enterKey>
|
||||
<Shortcut size="sm" :keys="[$t('components.app.create_modal.enter')]" />
|
||||
</template>
|
||||
</i18n-t>
|
||||
</DialogFooter>
|
||||
</DialogScrollContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -52,6 +53,8 @@
|
||||
modelValue: boolean;
|
||||
};
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
});
|
||||
@@ -83,7 +86,7 @@
|
||||
|
||||
async function submitCsvFile() {
|
||||
if (!importCsv.value) {
|
||||
toast.error("Please select a file to import.");
|
||||
toast.error(t("components.app.import_dialog.toast.please_select_file"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -92,7 +95,7 @@
|
||||
const { error } = await api.items.import(importCsv.value);
|
||||
|
||||
if (error) {
|
||||
toast.error("Import failed. Please try again later.");
|
||||
toast.error(t("components.app.import_dialog.toast.import_failed"));
|
||||
}
|
||||
|
||||
// Reset
|
||||
@@ -104,6 +107,6 @@
|
||||
importRef.value.value = "";
|
||||
}
|
||||
|
||||
toast.success("Import successful!");
|
||||
toast.success(t("components.app.import_dialog.toast.import_success"));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<FormTextField v-model="value" placeholder="Password" :label="label" :type="inputType"> </FormTextField>
|
||||
<FormTextField v-model="value" :placeholder="localizedPlaceholder" :label="localizedLabel" :type="inputType">
|
||||
</FormTextField>
|
||||
<TooltipProvider :delay-duration="0">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
@@ -12,29 +13,31 @@
|
||||
<MdiEye name="mdi-eye" class="size-5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Toggle Password Show</TooltipContent>
|
||||
<TooltipContent>{{ $t("components.form.password.toggle_show") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import MdiEye from "~icons/mdi/eye";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
|
||||
|
||||
const { t } = useI18n();
|
||||
type Props = {
|
||||
modelValue: string;
|
||||
placeholder?: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
placeholder: "Password",
|
||||
label: "Password",
|
||||
});
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const [hide, toggle] = useToggle(true);
|
||||
|
||||
const localizedPlaceholder = computed(() => props.placeholder ?? t("global.password"));
|
||||
const localizedLabel = computed(() => props.label ?? t("global.password"));
|
||||
|
||||
const inputType = computed(() => {
|
||||
return hide.value ? "password" : "text";
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<MdiDownload />
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent> Download </TooltipContent>
|
||||
<TooltipContent> {{ $t("components.item.attachments_list.download") }} </TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
@@ -29,7 +29,7 @@
|
||||
<MdiOpenInNew />
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent> Open in new tab </TooltipContent>
|
||||
<TooltipContent> {{ $t("components.item.attachments_list.open_new_tab") }} </TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,11 @@
|
||||
<div v-if="form.photos.length > 0" class="mt-4 border-t px-4 pb-4">
|
||||
<div v-for="(photo, index) in form.photos" :key="index">
|
||||
<div class="mt-8 w-full">
|
||||
<img :src="photo.fileBase64" class="w-full rounded object-fill shadow-sm" alt="Uploaded Photo" />
|
||||
<img
|
||||
:src="photo.fileBase64"
|
||||
class="w-full rounded object-fill shadow-sm"
|
||||
:alt="$t('components.item.create_modal.uploaded')"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<TooltipProvider class="flex gap-2" :delay-duration="0">
|
||||
@@ -77,11 +81,11 @@
|
||||
<TooltipTrigger>
|
||||
<Button size="icon" type="button" variant="destructive" @click.prevent="deleteImage(index)">
|
||||
<MdiDelete />
|
||||
<div class="sr-only">Delete photo</div>
|
||||
<div class="sr-only">{{ $t("components.item.create_modal.delete_photo") }}</div>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Delete photo</p>
|
||||
<p>{{ $t("components.item.create_modal.delete_photo") }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
@@ -97,11 +101,11 @@
|
||||
"
|
||||
>
|
||||
<MdiRotateClockwise />
|
||||
<div class="sr-only">Rotate photo</div>
|
||||
<div class="sr-only">{{ $t("components.item.create_modal.rotate_photo") }}</div>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Rotate photo</p>
|
||||
<p>{{ $t("components.item.create_modal.rotate_photo") }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
@@ -114,11 +118,15 @@
|
||||
>
|
||||
<MdiStar v-if="photo.primary" />
|
||||
<MdiStarOutline v-else />
|
||||
<div class="sr-only">Set as {{ photo.primary ? "non" : "" }} primary photo</div>
|
||||
<div class="sr-only">
|
||||
{{ $t("components.item.create_modal.set_as_primary_photo", { isPrimary: photo.primary }) }}
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Set as {{ photo.primary ? "non" : "" }} primary photo</p>
|
||||
<p>
|
||||
{{ $t("components.item.create_modal.set_as_primary_photo", { isPrimary: photo.primary }) }}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -131,6 +139,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import { Button, ButtonGroup } from "~/components/ui/button";
|
||||
import BaseModal from "@/components/App/CreateModal.vue";
|
||||
@@ -157,6 +166,7 @@
|
||||
primary: boolean;
|
||||
}
|
||||
|
||||
const { t } = useI18n();
|
||||
const { activeDialog, closeDialog } = useDialog();
|
||||
|
||||
useDialogHotkey("create-item", { code: "Digit1", shift: true });
|
||||
@@ -269,7 +279,7 @@
|
||||
const itemIdRead = typeof itemId.value === "string" ? (itemId.value as string) : itemId.value[0];
|
||||
const { data, error } = await api.items.get(itemIdRead);
|
||||
if (error || !data) {
|
||||
toast.error("Failed to load parent item - please select manually");
|
||||
toast.error(t("components.item.create_modal.toast.failed_load_parent"));
|
||||
console.error("Parent item fetch error:", error);
|
||||
}
|
||||
|
||||
@@ -309,12 +319,12 @@
|
||||
|
||||
async function create(close = true) {
|
||||
if (!form.location?.id) {
|
||||
toast.error("Please select a location.");
|
||||
toast.error(t("components.item.create_modal.toast.please_select_location"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (loading.value) {
|
||||
toast.error("Already creating an item");
|
||||
toast.error(t("components.item.create_modal.toast.already_creating"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -335,14 +345,14 @@
|
||||
|
||||
if (error) {
|
||||
loading.value = false;
|
||||
toast.error("Couldn't create item");
|
||||
toast.error(t("components.item.create_modal.toast.create_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Item created");
|
||||
toast.success(t("components.item.create_modal.toast.create_success"));
|
||||
|
||||
if (form.photos.length > 0) {
|
||||
toast.info(`Uploading ${form.photos.length} photo(s)...`);
|
||||
toast.info(t("components.item.create_modal.toast.uploading_photos", { count: form.photos.length }));
|
||||
let uploadError = false;
|
||||
for (const photo of form.photos) {
|
||||
const { error: attachError } = await api.items.attachments.add(
|
||||
@@ -355,14 +365,14 @@
|
||||
|
||||
if (attachError) {
|
||||
uploadError = true;
|
||||
toast.error(`Failed to upload Photo: ${photo.photoName}`);
|
||||
toast.error(t("components.item.create_modal.toast.upload_failed", { photoName: photo.photoName }));
|
||||
console.error(attachError);
|
||||
}
|
||||
}
|
||||
if (uploadError) {
|
||||
toast.warning("Some photos failed to upload.");
|
||||
toast.warning(t("components.item.create_modal.toast.some_photos_failed", { count: form.photos.length }));
|
||||
} else {
|
||||
toast.success("All photos uploaded successfully.");
|
||||
toast.success(t("components.item.create_modal.toast.upload_success", { count: form.photos.length }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,7 +425,7 @@
|
||||
const offScreenCanvasCtx = offScreenCanvas.getContext("2d");
|
||||
|
||||
if (!offScreenCanvasCtx) {
|
||||
toast.error("Your browser doesn't support canvas operations");
|
||||
toast.error(t("components.item.create_modal.toast.no_canvas_support"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -428,7 +438,7 @@
|
||||
img.onerror = () => reject(new Error("Failed to load image"));
|
||||
img.src = base64Image;
|
||||
}).catch(error => {
|
||||
toast.error("Failed to rotate image: " + error.message);
|
||||
toast.error(t("components.item.create_modal.toast.rotate_failed", { error: error.message }));
|
||||
});
|
||||
|
||||
// Set its dimensions to rotated size
|
||||
@@ -447,7 +457,7 @@
|
||||
form.photos[index].fileBase64 = offScreenCanvas.toDataURL(imageType, 100);
|
||||
form.photos[index].file = dataURLtoFile(form.photos[index].fileBase64, form.photos[index].photoName);
|
||||
} catch (error) {
|
||||
toast.error("Failed to process rotated image");
|
||||
toast.error(t("components.item.create_modal.toast.rotate_process_failed"));
|
||||
console.error(error);
|
||||
} finally {
|
||||
// Clean up resources
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<Button :id="id" variant="outline" role="combobox" :aria-expanded="open" class="w-full justify-between">
|
||||
<span>
|
||||
<slot name="display" v-bind="{ item: value }">
|
||||
{{ displayValue(value) || placeholder }}
|
||||
{{ displayValue(value) || localizedPlaceholder }}
|
||||
</slot>
|
||||
</span>
|
||||
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||
@@ -16,9 +16,9 @@
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[--reka-popper-anchor-width] p-0">
|
||||
<Command :ignore-filter="true">
|
||||
<CommandInput v-model="search" :placeholder="searchPlaceholder" :display-value="_ => ''" />
|
||||
<CommandInput v-model="search" :placeholder="localizedSearchPlaceholder" :display-value="_ => ''" />
|
||||
<CommandEmpty>
|
||||
{{ noResultsText }}
|
||||
{{ localizedNoResultsText }}
|
||||
</CommandEmpty>
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
@@ -41,6 +41,7 @@
|
||||
import { Check, ChevronsUpDown } from "lucide-vue-next";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "~/components/ui/command";
|
||||
import { Label } from "~/components/ui/label";
|
||||
@@ -48,6 +49,8 @@
|
||||
import { cn } from "~/lib/utils";
|
||||
import { useId } from "#imports";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
type ItemsObject = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
@@ -72,9 +75,9 @@
|
||||
itemText: "text",
|
||||
itemValue: "value",
|
||||
search: "",
|
||||
searchPlaceholder: "Type to search...",
|
||||
noResultsText: "No Results Found",
|
||||
placeholder: "Select...",
|
||||
searchPlaceholder: undefined,
|
||||
noResultsText: undefined,
|
||||
placeholder: undefined,
|
||||
});
|
||||
|
||||
const id = useId();
|
||||
@@ -82,6 +85,12 @@
|
||||
const search = ref(props.search);
|
||||
const value = useVModel(props, "modelValue", emit);
|
||||
|
||||
const localizedSearchPlaceholder = computed(
|
||||
() => props.searchPlaceholder ?? t("components.item.selector.search_placeholder")
|
||||
);
|
||||
const localizedNoResultsText = computed(() => props.noResultsText ?? t("components.item.selector.no_results"));
|
||||
const localizedPlaceholder = computed(() => props.placeholder ?? t("components.item.selector.placeholder"));
|
||||
|
||||
watch(
|
||||
() => props.search,
|
||||
val => {
|
||||
|
||||
@@ -27,10 +27,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import BaseModal from "@/components/App/CreateModal.vue";
|
||||
import { useDialog, useDialogHotkey } from "~/components/ui/dialog-provider";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { closeDialog } = useDialog();
|
||||
|
||||
useDialogHotkey("create-label", { code: "Digit2", shift: true });
|
||||
@@ -56,11 +59,11 @@
|
||||
|
||||
async function create(close = true) {
|
||||
if (loading.value) {
|
||||
toast.error("Already creating a label");
|
||||
toast.error(t("components.label.create_modal.toast.already_creating"));
|
||||
return;
|
||||
}
|
||||
if (form.name.length > 50) {
|
||||
toast.error("Label name must not be longer than 50 characters");
|
||||
toast.error(t("components.label.create_modal.toast.label_name_too_long"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,12 +74,12 @@
|
||||
const { error, data } = await api.labels.create(form);
|
||||
|
||||
if (error) {
|
||||
toast.error("Couldn't create label");
|
||||
toast.error(t("components.label.create_modal.toast.create_failed"));
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Label created");
|
||||
toast.success(t("components.label.create_modal.toast.create_success"));
|
||||
reset();
|
||||
|
||||
if (close) {
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ComboboxAnchor, ComboboxContent, ComboboxInput, ComboboxPortal, ComboboxRoot } from "reka-ui";
|
||||
import { computed, ref } from "vue";
|
||||
import fuzzysort from "fuzzysort";
|
||||
@@ -80,6 +81,8 @@
|
||||
} from "@/components/ui/tags-input";
|
||||
import type { LabelOut } from "~/lib/api/types/data-contracts";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const id = useId();
|
||||
|
||||
const api = useUserApi();
|
||||
@@ -118,7 +121,7 @@
|
||||
.filter(i => !modelValue.value.includes(i.value));
|
||||
|
||||
if (searchTerm.value.trim() !== "") {
|
||||
filtered.push({ value: "create-item", label: `Create ${searchTerm.value}` });
|
||||
filtered.push({ value: "create-item", label: `${t("global.create")} ${searchTerm.value}` });
|
||||
}
|
||||
|
||||
return filtered;
|
||||
@@ -126,7 +129,7 @@
|
||||
|
||||
const createAndAdd = async (name: string) => {
|
||||
if (name.length > 50) {
|
||||
toast.error("Label name must not be longer than 50 characters");
|
||||
toast.error(t("components.label.create_modal.toast.label_name_too_long"));
|
||||
return;
|
||||
}
|
||||
const { error, data } = await api.labels.create({
|
||||
@@ -136,11 +139,11 @@
|
||||
});
|
||||
|
||||
if (error) {
|
||||
toast.error("Couldn't create label");
|
||||
toast.error(t("components.label.create_modal.toast.create_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Label created");
|
||||
toast.success(t("components.label.create_modal.toast.create_success"));
|
||||
|
||||
modelValue.value = [...modelValue.value, data.id];
|
||||
};
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import { Button, ButtonGroup } from "~/components/ui/button";
|
||||
import BaseModal from "@/components/App/CreateModal.vue";
|
||||
import type { LocationSummary } from "~~/lib/api/types/data-contracts";
|
||||
import { useDialog, useDialogHotkey } from "~/components/ui/dialog-provider";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { activeDialog, closeDialog } = useDialog();
|
||||
|
||||
useDialogHotkey("create-location", { code: "Digit3", shift: true });
|
||||
@@ -94,7 +97,7 @@
|
||||
|
||||
async function create(close = true) {
|
||||
if (loading.value) {
|
||||
toast.error("Already creating a location");
|
||||
toast.error(t("components.location.create_modal.toast.already_creating"));
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
@@ -109,11 +112,11 @@
|
||||
|
||||
if (error) {
|
||||
loading.value = false;
|
||||
toast.error("Couldn't create location");
|
||||
toast.error(t("components.location.create_modal.toast.create_failed"));
|
||||
}
|
||||
|
||||
if (data) {
|
||||
toast.success("Location created");
|
||||
toast.success(t("components.location.create_modal.toast.create_success"));
|
||||
}
|
||||
reset();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ $t("global.confirm") }}</AlertDialogTitle>
|
||||
<AlertDialogDescription> {{ text }} </AlertDialogDescription>
|
||||
<AlertDialogDescription> {{ text || $t("global.delete_confirm") }} </AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel @click="cancel(false)">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<template #default>
|
||||
{{ formattedValue }}
|
||||
</template>
|
||||
<template #fallback> Loading... </template>
|
||||
<template #fallback> {{ $t("global.loading") }} </template>
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { route } from "../../lib/api/base";
|
||||
import PageQRCode from "./PageQRCode.vue";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
@@ -17,6 +18,7 @@
|
||||
import { Button, ButtonGroup } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -29,7 +31,7 @@
|
||||
const { data: status } = useAsyncData(async () => {
|
||||
const { data, error } = await pubApi.status();
|
||||
if (error) {
|
||||
toast.error("Failed to load status");
|
||||
toast.error(t("components.global.label_maker.toast.load_status_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,11 +57,11 @@
|
||||
} catch (err) {
|
||||
console.error("Failed to print labels:", err);
|
||||
serverPrinting.value = false;
|
||||
toast.error("Failed to print label");
|
||||
toast.error(t("components.global.label_maker.toast.print_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Label printed");
|
||||
toast.success(t("components.global.label_maker.toast.print_success"));
|
||||
closeDialog("print-label");
|
||||
serverPrinting.value = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user