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:
Nikolai Oakfield
2025-05-29 08:56:30 -04:00
committed by GitHub
parent e6f7397b30
commit 3a4fae5eb8
27 changed files with 478 additions and 233 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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";
});

View File

@@ -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>

View File

@@ -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

View File

@@ -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 => {

View File

@@ -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) {

View File

@@ -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];
};

View File

@@ -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();

View File

@@ -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)">

View File

@@ -3,7 +3,7 @@
<template #default>
{{ formattedValue }}
</template>
<template #fallback> Loading... </template>
<template #fallback> {{ $t("global.loading") }} </template>
</Suspense>
</template>

View File

@@ -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;
}