Merge branch 'main' into label-report-prefill

This commit is contained in:
zak
2024-08-11 19:03:05 -07:00
committed by GitHub
27 changed files with 8469 additions and 5431 deletions

View File

@@ -49,6 +49,11 @@ Contributions are what make the open source community such an amazing place to l
If you are not a coder, you can still contribute financially. Financial contributions help me prioritize working on this project over others and helps me know that there is a real demand for project development.
## Help us Translate
We want to make sure that Homebox is available in as many languages as possible. If you are interested in helping us translate Homebox, please help us via our [Weblate instance](https://translate.sysadminsmedia.com/projects/homebox/).
[![Translation status](http://translate.sysadminsmedia.com/widget/homebox/multi-auto.svg)](http://translate.sysadminsmedia.com/engage/homebox/)
## Credits
- Original project by [@hay-kot](https://github.com/hay-kot)

View File

@@ -4,12 +4,6 @@ import (
"embed"
"errors"
"fmt"
"io"
"mime"
"net/http"
"path"
"path/filepath"
"github.com/go-chi/chi/v5"
"github.com/hay-kot/httpkit/errchain"
httpSwagger "github.com/swaggo/http-swagger/v2" // http-swagger middleware
@@ -19,6 +13,11 @@ import (
_ "github.com/sysadminsmedia/homebox/backend/app/api/static/docs"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/authroles"
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
"io"
"mime"
"net/http"
"path"
"path/filepath"
)
const prefix = "/api"

View File

@@ -29,9 +29,9 @@ func (tp *TemplateProps) Set(key, value string) {
func DefaultTemplateData() TemplateProps {
return TemplateProps{
Defaults: TemplateDefaults{
CompanyName: "Haybytes.com",
CompanyName: "sysadminsmedia.com",
CompanyAddress: "123 Main St, Anytown, CA 12345",
CompanyURL: "https://haybytes.com",
CompanyURL: "https://sysadminsmedia.com",
ActivateAccountURL: "https://google.com",
UnsubscribeURL: "https://google.com",
},

View File

@@ -57,6 +57,11 @@ For documentation contributions, you only need Node.js and PNPM.
- Redirects can also be configured per language by adding `Language=` after the redirect code
:::
## Translations
We use our own [Weblate instance](https://translate.sysadminsmedia.com/projects/homebox/) for translations. If you would like to help translate Homebox, please visit the Weblate instance and help us translate the project.
[![Translation status](http://translate.sysadminsmedia.com/widget/homebox/multi-auto.svg)](http://translate.sysadminsmedia.com/engage/homebox/)
## Branch Flow
We use the `main` branch as the development branch. All PRs should be made to the `main` branch from a feature branch.
To create a pull request you can use the following steps:

View File

@@ -1,9 +1,8 @@
<template>
<BaseModal v-model="dialog">
<template #title> Import CSV File </template>
<template #title> {{ $t("components.app.import_dialog.title") }} </template>
<p>
Import a CSV file containing your items, labels, and locations. See documentation for more information on the
required format.
{{ $t("components.app.import_dialog.description") }}
</p>
<div class="alert alert-warning shadow-lg mt-4">
<div>
@@ -21,8 +20,7 @@
/>
</svg>
<span class="text-sm">
Behavior for imports with existing import_refs has changed. If an import_ref is present in the CSV file, the
item will be updated with the values in the CSV file.
{{ $t("components.app.import_dialog.change_warning") }}
</span>
</div>
</div>
@@ -33,7 +31,7 @@
<BaseButton type="button" @click="uploadCsv">
<MdiUpload class="h-5 w-5 mr-2" />
Upload
{{ $t("components.app.import_dialog.upload") }}
</BaseButton>
<p class="text-center pt-4 -mb-5">
{{ importCsv?.name }}
@@ -41,7 +39,7 @@
</div>
<div class="modal-action">
<BaseButton type="submit" :disabled="!importCsv"> Submit </BaseButton>
<BaseButton type="submit" :disabled="!importCsv"> {{ $t("global.submit") }} </BaseButton>
</div>
</form>
</BaseModal>

View File

@@ -1,6 +1,6 @@
<template>
<BaseModal v-model="modal">
<template #title> Create Item </template>
<template #title> {{ $t("components.item.create_modal.title") }} </template>
<form @submit.prevent="create()">
<LocationSelector v-model="form.location" />
<FormTextField ref="nameInput" v-model="form.name" :trigger-focus="focused" :autofocus="true" label="Item Name" />
@@ -13,7 +13,7 @@
<MdiPackageVariant class="swap-off h-5 w-5" />
<MdiPackageVariantClosed class="swap-on h-5 w-5" />
</template>
Create
{{ $t("global.create") }}
</BaseButton>
<div class="dropdown dropdown-top">
<label tabindex="0" class="btn rounded-l-none rounded-r-xl">
@@ -21,7 +21,7 @@
</label>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-64 right-0">
<li>
<button type="button" @click="create(false)">Create and Add Another</button>
<button type="button" @click="create(false)">{{ $t("global.create_and_add") }}</button>
</li>
</ul>
</div>

View File

@@ -29,7 +29,7 @@
<template>
<section>
<BaseSectionHeader class="mb-2 flex justify-between items-center">
Items
{{ $t("components.item.view.selectable.items") }}
<template #description>
<div v-if="!viewSet" class="dropdown dropdown-hover dropdown-left">
<label tabindex="0" class="btn btn-ghost m-1">
@@ -39,13 +39,13 @@
<li>
<button @click="setViewPreference('card')">
<MdiCardTextOutline class="h-5 w-5" />
Card
{{ $t("components.item.view.selectable.card") }}
</button>
</li>
<li>
<button @click="setViewPreference('table')">
<MdiTable class="h-5 w-5" />
Table
{{ $t("components.item.view.selectable.table") }}
</button>
</li>
</ul>
@@ -59,7 +59,7 @@
<template v-else>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<ItemCard v-for="item in items" :key="item.id" :item="item" />
<div class="first:block hidden text-lg">No Items to Display</div>
<div class="first:block hidden text-lg">{{ $t("components.item.view.selectable.no_items") }}</div>
</div>
</template>
</section>

View File

@@ -1,6 +1,6 @@
<template>
<BaseModal v-model="modal">
<template #title> Create Label </template>
<template #title>{{ $t("components.label.create_modal.title") }}</template>
<form @submit.prevent="create()">
<FormTextField
ref="locationNameRef"
@@ -12,14 +12,14 @@
<FormTextArea v-model="form.description" label="Label Description" />
<div class="modal-action">
<div class="flex justify-center">
<BaseButton class="rounded-r-none" :loading="loading" type="submit"> Create </BaseButton>
<BaseButton class="rounded-r-none" :loading="loading" type="submit"> {{ $t("global.create") }} </BaseButton>
<div class="dropdown dropdown-top">
<label tabindex="0" class="btn rounded-l-none rounded-r-xl">
<MdiChevronDown class="h-5 w-5" />
</label>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-64 right-0">
<li>
<button type="button" @click="create(false)">Create and Add Another</button>
<button type="button" @click="create(false)">{{ $t("global.create_and_add") }}</button>
</li>
</ul>
</div>

View File

@@ -1,6 +1,6 @@
<template>
<BaseModal v-model="modal">
<template #title> Create Location </template>
<template #title>{{ $t("components.location.create_modal.title") }}</template>
<form @submit.prevent="create()">
<FormTextField
ref="locationNameRef"
@@ -13,14 +13,14 @@
<LocationSelector v-model="form.parent" />
<div class="modal-action">
<div class="flex justify-center">
<BaseButton class="rounded-r-none" type="submit" :loading="loading"> Create </BaseButton>
<BaseButton class="rounded-r-none" type="submit" :loading="loading">{{ $t("global.create") }}</BaseButton>
<div class="dropdown dropdown-top">
<label tabindex="0" class="btn rounded-l-none rounded-r-xl">
<MdiChevronDown class="h-5 w-5" />
</label>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-64 right-0">
<li>
<button type="button" @click="create(false)">Create and Add Another</button>
<button type="button" @click="create(false)">{{ $t("global.create_and_add") }}</button>
</li>
</ul>
</div>

View File

@@ -1,11 +1,11 @@
<template>
<BaseModal v-model="isRevealed" readonly @cancel="cancel(false)">
<template #title> Confirm </template>
<template #title> {{ $t("global.confirm") }} </template>
<div>
<p>{{ text }}</p>
</div>
<div class="modal-action">
<BaseButton type="submit" @click="confirm(true)"> Confirm </BaseButton>
<BaseButton type="submit" @click="confirm(true)"> {{ $t("global.confirm") }} </BaseButton>
</div>
</BaseModal>
</template>

View File

@@ -7,7 +7,7 @@
</slot>
<div tabindex="0" class="card compact dropdown-content shadow-lg bg-base-100 rounded-box w-64">
<div class="card-body">
<h2 class="text-center">Page URL</h2>
<h2 class="text-center">{{ $t("components.global.page_qr_code.page_url") }}</h2>
<img :src="getQRCodeUrl()" />
</div>
</div>

View File

@@ -1,6 +1,6 @@
<template>
<div class="py-4">
<p class="text-sm">Password Strength: {{ message }}</p>
<p class="text-sm">{{ $t("components.global.password_score.password_strength") }}: {{ message }}</p>
<progress
class="progress w-full progress-bar"
:value="score"

View File

@@ -29,6 +29,12 @@
</div>
<slot></slot>
<footer v-if="status" class="text-center w-full bottom-0 pb-4 bg-base-300 text-secondary-content">
<p class="text-center text-sm">
{{ $t("global.version", { version: status.build.version }) }} ~
{{ $t("global.build", { build: status.build.commit }) }}
</p>
</footer>
</div>
<!-- Sidebar -->
@@ -39,7 +45,7 @@
<div class="w-60 py-5 md:py-10 bg-base-200 flex flex-grow-1 flex-col">
<div class="space-y-8">
<div class="flex flex-col items-center gap-4">
<p>Welcome, {{ username }}</p>
<p>{{ $t("global.welcome", { username: username }) }}</p>
<NuxtLink class="avatar placeholder" to="/home">
<div class="bg-base-300 text-neutral-content rounded-full w-24 p-4">
<AppLogo />
@@ -53,7 +59,7 @@
<span>
<MdiPlus class="mr-1 -ml-1" />
</span>
Create
{{ $t("global.create") }}
</label>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-40">
<li v-for="btn in dropdown" :key="btn.name">
@@ -83,7 +89,9 @@
</div>
<!-- Bottom -->
<button class="mt-auto mx-2 hover:bg-base-300 p-3 rounded-btn" @click="logout">Sign Out</button>
<button class="mt-auto mx-2 hover:bg-base-300 p-3 rounded-btn" @click="logout">
{{ $t("global.sign_out") }}
</button>
</div>
</div>
</div>
@@ -101,12 +109,17 @@
import MdiMagnify from "~icons/mdi/magnify";
import MdiAccount from "~icons/mdi/account";
import MdiCog from "~icons/mdi/cog";
const username = computed(() => authCtx.user?.name || "User");
const pubApi = usePublicApi();
const { data: status } = useAsyncData(async () => {
const { data } = await pubApi.status();
return data;
});
// Preload currency format
useFormatCurrency();
const modals = reactive({
item: false,
location: false,

127
frontend/locales/de.json Normal file
View File

@@ -0,0 +1,127 @@
{
"global": {
"github": "GitHub Projekt",
"join_discord": "Discord beitreten",
"password": "Passwort",
"version": "Version: { version }",
"build": "Build: { build }",
"email": "E-Mail",
"confirm": "Bestätigen",
"create": "Erstellen",
"create_and_add": "Erstellen und ein weiteres hinzufügen",
"follow_dev": "Dem Entwickler folgen",
"read_docs": "Lese die Dokumentation",
"submit": "Absenden",
"created": "Erstellt",
"welcome": "Willkommen,{username}",
"sign_out": "Abmelden",
"items": "",
"labels": "",
"locations": "",
"name": "",
"search": ""
},
"index": {
"login": "Anmelden",
"register": "Registrieren",
"tagline": "Verfolgen, Organisieren und Verwalten deiner Sachen.",
"disabled_registration": "Registrierung deaktiviert",
"joining_group": "Du trittst einer bereits bestehenden Gruppe bei!",
"remember_me": "Angemeldet bleiben",
"set_email": "Was ist deine E-Mail?",
"set_password": "Setze dein Passwort",
"set_name": "Wie heißt du?",
"dont_join_group": "Möchtest du nicht einer Gruppe beitreten?"
},
"components": {
"global": {
"password_score": {
"password_strength": "Passwortstärke"
},
"page_qr_code": {
"page_url": "Seiten-URL"
}
},
"item": {
"view": {
"selectable": {
"items": "Gegenstände",
"card": "Karte",
"table": "Tabelle",
"no_items": "Keine Gegenstände anzuzeigen"
}
},
"create_modal": {
"title": "Erstelle Gegenstand"
}
},
"app": {
"import_dialog": {
"description": "Importiere eine CSV-Datei, die deine Gegenstände, Etiketten und Standorte enthält. Siehe Dokumentation für weitere Informationen zum \nerforderlichen Format.",
"title": "Importiere CSV-Datei",
"change_warning": "Das Verhalten bei Importen mit vorhandenen import_refs hat sich geändert. Wenn ein import_ref in der CSV-Datei vorhanden ist, wird\n der Gegenstand mit den Werten in der CSV-Datei aktualisiert.",
"upload": "Hochladen"
}
},
"label": {
"create_modal": {
"title": "Erstelle Etikette"
}
},
"location": {
"create_modal": {
"title": "Erstelle Standort"
}
}
},
"profile": {
"change_password": "Passwort ändern",
"current_password": "Aktuelles Passwort",
"new_password": "Neues Passwort",
"notifiers": "Melder",
"notifiers_sub": "Benachrichtigungen über anstehende Wartungserinnerungen erhalten",
"url": "URL",
"enabled": "Aktiviert",
"test": "Test",
"gen_invite": "Einladungslink generieren",
"user_profile": "Benutzerprofil",
"user_profile_sub": "Benutzer einladen und Ihr Konto verwalten.",
"active": "Aktiv",
"inactive": "Inaktiv",
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Melder",
"theme_settings": "",
"theme_settings_sub": "",
"update_group": "",
"currency_format": "",
"delete_account": "",
"delete_account_sub": "",
"group_settings": "",
"group_settings_sub": ""
},
"items": {
"results": "",
"add": "",
"created_at": "",
"custom_fields": "",
"field_selector": "",
"field_value": "",
"first": "",
"include_archive": "",
"last": "",
"negate_labels": "",
"next_page": "",
"no_results": "",
"options": "",
"order_by": "",
"pages": "",
"prev_page": "",
"query_id": "",
"reset_search": "",
"tip_1": "",
"tip_2": "",
"tip_3": "",
"tips": "",
"tips_sub": "",
"updated_at": ""
}
}

127
frontend/locales/en.json Normal file
View File

@@ -0,0 +1,127 @@
{
"components": {
"app": {
"import_dialog": {
"change_warning": "Behavior for imports with existing import_refs has changed. If an import_ref is present in the CSV file, the \nitem will be updated with the values in the CSV file.",
"description": "Import a CSV file containing your items, labels, and locations. See documentation for more information on the \nrequired format.",
"title": "Import CSV File",
"upload": "Upload"
}
},
"global": {
"page_qr_code": {
"page_url": "Page URL"
},
"password_score": {
"password_strength": "Password Strength"
}
},
"item": {
"create_modal": {
"title": "Create Item"
},
"view": {
"selectable": {
"card": "Card",
"items": "Items",
"no_items": "No Items to Display",
"table": "Table"
}
}
},
"label": {
"create_modal": {
"title": "Create Label"
}
},
"location": {
"create_modal": {
"title": "Create Location"
}
}
},
"global": {
"build": "Build: { build }",
"confirm": "Confirm",
"create": "Create",
"create_and_add": "Create and Add Another",
"created": "Created",
"email": "Email",
"follow_dev": "Follow the Developer",
"github": "GitHub Project",
"items": "Items",
"join_discord": "Join the Discord",
"labels": "Labels",
"locations": "Locations",
"name": "Name",
"password": "Password",
"read_docs": "Read the Docs",
"search": "Search",
"sign_out": "Sign Out",
"submit": "Submit",
"version": "Version: { version }",
"welcome": "Welcome, { username }"
},
"index": {
"disabled_registration": "Registration Disabled",
"dont_join_group": "Don't want to join a group?",
"joining_group": "You're Joining an Existing Group!",
"login": "Login",
"register": "Register",
"remember_me": "Remember Me",
"set_email": "What's your email?",
"set_name": "What's your name?",
"set_password": "Set your password",
"tagline": "Track, Organize, and Manage your Things."
},
"items": {
"add": "Add",
"created_at": "Created At",
"custom_fields": "Custom Fields",
"field_selector": "Field Selector",
"field_value": "Field Value",
"first": "First",
"include_archive": "Include Archived Items",
"last": "Last",
"negate_labels": "Negate Selected Labels",
"next_page": "Next Page",
"no_results": "No Items Found",
"options": "Options",
"order_by": "Order By",
"pages": "Page { page } of { totalPages }",
"prev_page": "Previous Page",
"query_id": "Querying Asset ID Number: { id }",
"reset_search": "Reset Search",
"results": "{ total } Results",
"tip_1": "Location and label filters use the 'OR' operation. If more than one is selected only one will be\n required for a match.",
"tip_2": "Searches prefixed with '#'' will query for a asset ID (example '#000-001')",
"tip_3": "Field filters use the 'OR' operation. If more than one is selected only one will be required for a\n match.",
"tips": "Tips",
"tips_sub": "Search Tips",
"updated_at": "Updated At"
},
"profile": {
"active": "Active",
"change_password": "Change Password",
"currency_format": "Currency Format",
"current_password": "Current Password",
"delete_account": "Delete Account",
"delete_account_sub": "Delete your account and all its associated data. This can not be undone.",
"enabled": "Enabled",
"gen_invite": "Generate Invite Link",
"group_settings": "Group Settings",
"group_settings_sub": "Shared Group Settings. You may need to refresh your browser for some settings to apply.",
"inactive": "Inactive",
"new_password": "New Password",
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Notifier",
"notifiers": "Notifiers",
"notifiers_sub": "Get notifications for upcoming maintenance reminders",
"test": "Test",
"theme_settings": "Theme Settings",
"theme_settings_sub": "Theme settings are stored in your browser's local storage. You can change the theme at any time. If you're\n having trouble setting your theme try refreshing your browser.",
"update_group": "Update Group",
"url": "URL",
"user_profile": "User Profile",
"user_profile_sub": "Invite users, and manage your account."
}
}

127
frontend/locales/fr.json Normal file
View File

@@ -0,0 +1,127 @@
{
"global": {
"email": "Courriel",
"read_docs": "Lire la documentation",
"password": "Mot de passe",
"github": "Projet GitHub",
"join_discord": "Rejoindre le Discord",
"follow_dev": "Suivre le développeur",
"version": "Version : { version }",
"build": "Assemblage : { build }",
"create": "Créer",
"submit": "Soumettre",
"create_and_add": "Créer et en ajouter un autre",
"confirm": "Confirmer",
"sign_out": "Déconnexion",
"welcome": "Bienvenue,{ username }",
"created": "Créé",
"labels": "",
"locations": "",
"name": "",
"search": "",
"items": ""
},
"index": {
"set_email": "Quel est vôtre courriel?",
"set_name": "Quel est vôtre nom?",
"login": "Se connecter",
"register": "S'enregistrer",
"remember_me": "Se souvenir de moi",
"set_password": "Définir votre mot de passe",
"dont_join_group": "Vous ne voulez pas joindre un groupe ?",
"tagline": "Traquez, organisez et gérez vos affaires.",
"disabled_registration": "Les inscriptions sont désactivées",
"joining_group": "Vous rejoignez un groupe existant!"
},
"components": {
"label": {
"create_modal": {
"title": "Créer une étiquette"
}
},
"item": {
"view": {
"selectable": {
"items": "Items",
"no_items": "Nb. ditems à afficher",
"table": "Tableau",
"card": "Carte"
}
},
"create_modal": {
"title": "Créer un item"
}
},
"location": {
"create_modal": {
"title": "Créer un emplacement"
}
},
"global": {
"password_score": {
"password_strength": "Solidité du mot de passe"
},
"page_qr_code": {
"page_url": "URL de la page"
}
},
"app": {
"import_dialog": {
"title": "Importer un fichier CSV",
"upload": "Téléverser",
"description": "Importer un fichier CSV contenant vos items, étiquettes, et emplacements. Voir la documentation pour plus d'informations sur le format requis.",
"change_warning": "Le comportement lors dimportations avec des import_ref existants a changé. Si une valeur pour import_ref est présente dans le fichier CSV, alors l'item sera mis à jour avec celle-ci."
}
}
},
"profile": {
"notifiers_sub": "Recevez des notifications pour vous prévenir des prochaines maintenances",
"active": "Actif",
"enabled": "Activé",
"test": "Test",
"current_password": "Mot de passe actuel",
"new_password": "Nouveau mot de passe",
"change_password": "Changer de mot de passe",
"inactive": "Inactif",
"notifiers": "Notifications",
"gen_invite": "Générer un lien dinvitation",
"user_profile": "Profil Utilisateur",
"user_profile_sub": "Invitez des utilisateurs, et gérez votre compte.",
"url": "URL",
"delete_account": "Effacer le compte",
"delete_account_sub": "Effacer le compte et toutes les données. Aucune réccupération possible.",
"group_settings": "Paramètres du groupe",
"group_settings_sub": "Paramètres partagés du groupe. Il peut être ne2cessaire de recharger la page.",
"currency_format": "Format de la devise",
"update_group": "Mettre à jour le groupe",
"theme_settings": "Paramètres du thème",
"theme_settings_sub": "Les paramètres du thème sont stockés dans le navigateur. Vous pouvez les changer à tout moment. Si vous\nrencontrez des problèmes, il est conseillé de rafraichir la page.",
"notifier_modal": ""
},
"items": {
"add": "",
"created_at": "",
"custom_fields": "",
"field_selector": "",
"field_value": "",
"first": "",
"include_archive": "",
"last": "",
"negate_labels": "",
"next_page": "",
"no_results": "",
"options": "",
"order_by": "",
"pages": "",
"prev_page": "",
"query_id": "",
"reset_search": "",
"tip_1": "",
"tip_2": "",
"tip_3": "",
"tips": "",
"tips_sub": "",
"updated_at": "",
"results": ""
}
}

127
frontend/locales/it.json Normal file
View File

@@ -0,0 +1,127 @@
{
"global": {
"build": "Versione: { build }",
"version": "Versione: { version }",
"github": "Progetto GitHub",
"join_discord": "Unisciti a Discord",
"follow_dev": "Segui lo Sviluppatore",
"read_docs": "Leggi la Documentazione",
"password": "Password",
"email": "Email",
"submit": "Invia",
"confirm": "Conferma",
"create": "Crea",
"create_and_add": "Crea e aggiungi un altro",
"created": "",
"items": "",
"labels": "",
"locations": "",
"name": "",
"search": "",
"sign_out": "",
"welcome": ""
},
"index": {
"disabled_registration": "Registrazione Disabilitata",
"login": "Accedi",
"register": "Registrati",
"remember_me": "Ricordami",
"set_email": "Qual è la tua email?",
"set_password": "Imposta la tua password",
"set_name": "Come ti chiami?",
"dont_join_group": "Non vuoi unirti a un gruppo?",
"tagline": "Tieni traccia, Organizza e Gestisci le tue Cose.",
"joining_group": "Stai unendoti a un gruppo esistente!"
},
"components": {
"global": {
"password_score": {
"password_strength": "Complessità della Password"
},
"page_qr_code": {
"page_url": "URL della Pagina"
}
},
"app": {
"import_dialog": {
"title": "Importa File CSV",
"change_warning": "Il comportamento per le importazioni con import_ref esistenti è cambiato. Se un import_ref è presente nel file CSV,\n l'elemento verrà aggiornato con i valori presenti nel file CSV.",
"upload": "Carica",
"description": "Importa un file CSV contenente gli oggetti, le etichette e le posizioni. Vedi la documentazione per ulteriori informazioni sul \nformato richiesto."
}
},
"item": {
"create_modal": {
"title": "Crea Oggetto"
},
"view": {
"selectable": {
"items": "Oggetti",
"card": "Scheda",
"table": "Tabella",
"no_items": "Nessun Oggetto da Visualizzare"
}
}
},
"label": {
"create_modal": {
"title": "Crea Etichetta"
}
},
"location": {
"create_modal": {
"title": "Crea Posizione"
}
}
},
"profile": {
"notifiers_sub": "",
"theme_settings_sub": "",
"url": "",
"user_profile": "",
"user_profile_sub": "",
"active": "",
"change_password": "",
"currency_format": "",
"current_password": "",
"delete_account": "",
"delete_account_sub": "",
"enabled": "",
"gen_invite": "",
"group_settings": "",
"group_settings_sub": "",
"inactive": "",
"new_password": "",
"notifier_modal": "",
"notifiers": "",
"test": "",
"theme_settings": "",
"update_group": ""
},
"items": {
"add": "",
"custom_fields": "",
"field_value": "",
"first": "",
"last": "",
"next_page": "",
"no_results": "",
"order_by": "",
"created_at": "",
"field_selector": "",
"include_archive": "",
"negate_labels": "",
"options": "",
"pages": "",
"prev_page": "",
"query_id": "",
"reset_search": "",
"tip_1": "",
"tip_2": "",
"tip_3": "",
"tips": "",
"tips_sub": "",
"updated_at": "",
"results": ""
}
}

127
frontend/locales/nl.json Normal file
View File

@@ -0,0 +1,127 @@
{
"global": {
"version": "Versie: { version }",
"github": "GitHub Project",
"join_discord": "Sluit je aan bij de Discord",
"follow_dev": "Volg de ontwikkelaar",
"read_docs": "Lees de documentatie",
"email": "E-mail",
"submit": "Indienen",
"confirm": "Bevestigen",
"create": "Maken",
"create_and_add": "Maak en voeg nog een toe",
"password": "Wachtwoord",
"build": "Bouw: { build }",
"created": "Gemaakt",
"welcome": "Welkom, { username }",
"sign_out": "Log uit",
"labels": "",
"locations": "",
"name": "",
"items": "",
"search": ""
},
"index": {
"disabled_registration": "Registratie uitgeschakeld",
"login": "Log in",
"register": "Registreer",
"remember_me": "Onthoud mij",
"set_email": "Wat is je mailadres?",
"set_password": "Stel je wachtwoord in",
"set_name": "Wat is je naam?",
"joining_group": "Je neemt deel aan een bestaande groep!",
"tagline": "Volg, Organiseer en beheer je dingen.",
"dont_join_group": "Wil je niet aan een groep deelnemen?"
},
"components": {
"global": {
"password_score": {
"password_strength": "Wachtwoord sterkte"
},
"page_qr_code": {
"page_url": "Pagina URL"
}
},
"app": {
"import_dialog": {
"title": "Importeer CSV bestand",
"change_warning": "Gedrag voor importeren met bestaande import_refs is veranderd. Als een import_refs reeds bestaat in het CSV bestand, het\nobject zal worden aangepast met de waardes van het CSV bestand.",
"upload": "Upload",
"description": "Importeer een CSV bestand met je objecten, labels en locaties. Zie documentatie voor meer informatie m.b.t.\n vereist formaat."
}
},
"item": {
"create_modal": {
"title": "Maak object"
},
"view": {
"selectable": {
"items": "Objecten",
"card": "Kaart",
"table": "Tabel",
"no_items": "Geen objecten om te tonen"
}
}
},
"label": {
"create_modal": {
"title": "Maak label"
}
},
"location": {
"create_modal": {
"title": "Maak locatie"
}
}
},
"profile": {
"gen_invite": "Genereer Uitnodigingslink",
"notifier_modal": "{ type, selecteer, waar {Bewerk} onwaar {Creëer} overig {overig}} Notifier",
"change_password": "Verander Wachtwoord",
"current_password": "Huidig Wachtwoord",
"new_password": "Nieuw Wachtwoord",
"url": "URL",
"enabled": "ingeschakeld",
"test": "Test",
"user_profile": "Gebruikers Profiel",
"user_profile_sub": "Nodig gebruikers uit, en beheer je account.",
"active": "Actief",
"inactive": "Inactief",
"notifiers_sub": "Krijg notificaties voor opkomende onderhouds herinneringen",
"group_settings_sub": "Gedeelde groepsinstellingen",
"group_settings": "Groeps Instellingen",
"notifiers": "Notificatie",
"currency_format": "Valutanotatie",
"update_group": "Groep bijwerken",
"theme_settings": "Theme instellingen",
"theme_settings_sub": "",
"delete_account": "",
"delete_account_sub": ""
},
"items": {
"tip_1": "",
"tip_2": "",
"tip_3": "",
"tips_sub": "",
"updated_at": "",
"pages": "",
"prev_page": "",
"query_id": "",
"reset_search": "",
"tips": "",
"no_results": "",
"options": "",
"order_by": "",
"results": "",
"add": "",
"created_at": "",
"custom_fields": "",
"field_selector": "",
"field_value": "",
"first": "",
"include_archive": "",
"last": "",
"negate_labels": "",
"next_page": ""
}
}

127
frontend/locales/sv.json Normal file
View File

@@ -0,0 +1,127 @@
{
"profile": {
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Anmälare",
"change_password": "Ändra Lösenord",
"current_password": "Nuvarande lösenord",
"new_password": "Nytt lösenord",
"notifiers_sub": "Få aviseringar om kommande underhållspåminnelser",
"url": "URL",
"test": "Test",
"gen_invite": "Skapa inbjudningslänk",
"user_profile": "Användarprofil",
"user_profile_sub": "Bjud in användare och hantera ditt konto.",
"active": "Aktiv",
"inactive": "Inaktiv",
"notifiers": "Notiser",
"enabled": "Aktiverad",
"currency_format": "",
"delete_account": "",
"delete_account_sub": "",
"group_settings": "",
"group_settings_sub": "",
"theme_settings": "",
"theme_settings_sub": "",
"update_group": ""
},
"index": {
"set_name": "Vad heter du?",
"joining_group": "Du går med i en befintlig grupp!",
"dont_join_group": "Vill du inte gå med i en grupp?",
"tagline": "Spåra, organisera och hantera dina saker.",
"disabled_registration": "Registrering avaktiverad",
"login": "Logga in",
"register": "Registrera",
"remember_me": "Kom ihåg mig",
"set_email": "Vad är din e-post?",
"set_password": "Ställ in ditt lösenord"
},
"components": {
"app": {
"import_dialog": {
"upload": "Ladda upp",
"title": "Importera CSV fil",
"description": "Importera en CSV-fil som innehåller dina föremål, etiketter och platser. Se dokumentationen för mer information om \nönskat format.",
"change_warning": "Beteendet för importer med befintliga import_refs har ändrats. Om en import_ref finns i CSV-filen, \nobjektet kommer att uppdateras med värdena i CSV-filen."
}
},
"item": {
"view": {
"selectable": {
"card": "Kort",
"items": "Föremål",
"table": "Tabell",
"no_items": "Inga föremål att visa"
}
},
"create_modal": {
"title": "Skapa föremål"
}
},
"label": {
"create_modal": {
"title": "Skapa etikett"
}
},
"location": {
"create_modal": {
"title": "Skapa plats"
}
},
"global": {
"password_score": {
"password_strength": "Lösenordsstyrka"
},
"page_qr_code": {
"page_url": "Sidans URL"
}
}
},
"global": {
"build": "Byggd: { build }",
"github": "GitHub Projekt",
"join_discord": "Gå med i Discord",
"follow_dev": "Följ utvecklaren",
"read_docs": "Läs dokumenten",
"password": "Lösenord",
"email": "Epost",
"submit": "Skicka",
"confirm": "Godkänn",
"create": "Skapa",
"created": "Skapad",
"welcome": "Välkommen, { username }",
"sign_out": "Logga ut",
"create_and_add": "Skapa och lägg till en annan",
"version": "Version: { version }",
"items": "",
"labels": "",
"locations": "",
"name": "",
"search": ""
},
"items": {
"add": "",
"created_at": "",
"custom_fields": "",
"field_selector": "",
"field_value": "",
"first": "",
"include_archive": "",
"last": "",
"negate_labels": "",
"next_page": "",
"no_results": "",
"options": "",
"order_by": "",
"pages": "",
"prev_page": "",
"query_id": "",
"reset_search": "",
"tip_1": "",
"tip_2": "",
"tip_3": "",
"tips": "",
"tips_sub": "",
"updated_at": "",
"results": ""
}
}

127
frontend/locales/tr.json Normal file
View File

@@ -0,0 +1,127 @@
{
"global": {
"version": "Versiyon:{ version }",
"password": "Şifre",
"create": "Oluştur",
"github": "GitHub projesi",
"join_discord": "Discord'a Katılın",
"follow_dev": "Geliştiriciyi takip edin",
"read_docs": "Dokümanları okuyun",
"email": "Elektronik posta",
"submit": "Gönder",
"confirm": "Onaylayın",
"build": "Sürüm: { build }",
"create_and_add": "Oluştur ve Bir Tane Daha Ekle",
"created": "",
"items": "",
"labels": "",
"locations": "",
"name": "",
"search": "",
"sign_out": "",
"welcome": ""
},
"components": {
"app": {
"import_dialog": {
"change_warning": "Mevcut import_refs ile içe aktarmaların davranışı değişti. CSV dosyasında bir import_ref varsa, \nöğe CSV dosyasındaki değerlerle güncellenecektir.",
"title": "CSV dosyasını içeri aktar",
"upload": "Yükle",
"description": "Öğelerinizi, etiketlerinizi ve konumlarınızı içeren bir CSV dosyasını içe aktarın. Daha fazla\nbilgi için dökümanları okuyun."
}
},
"global": {
"password_score": {
"password_strength": "Şifre güvenlik seviyesi"
},
"page_qr_code": {
"page_url": "Sayfa URL'si"
}
},
"item": {
"create_modal": {
"title": "Eşya Oluştur"
},
"view": {
"selectable": {
"items": "Öğeler",
"card": "Kart",
"table": "Tablo",
"no_items": "Görüntülecek Öge Yok"
}
}
},
"label": {
"create_modal": {
"title": "Etiket oluştur"
}
},
"location": {
"create_modal": {
"title": "Konum oluştur"
}
}
},
"index": {
"remember_me": "Beni Hatırla",
"tagline": "Eşyalarınızı Takip Edin, Düzenleyin ve Yönetin.",
"disabled_registration": "Kayıt olma devre dışı",
"login": "Oturum Aç",
"register": "Kaydolun",
"set_email": "E-posta adresiniz nedir?",
"set_password": "Şifrenizi belirleyin",
"set_name": "Adın ne?",
"joining_group": "Mevcut bir gruba katılıyorsunuz!",
"dont_join_group": "Bir gruba katılmak istemiyor musunuz?"
},
"items": {
"add": "",
"created_at": "",
"custom_fields": "",
"field_selector": "",
"field_value": "",
"first": "",
"include_archive": "",
"last": "",
"negate_labels": "",
"next_page": "",
"no_results": "",
"options": "",
"order_by": "",
"pages": "",
"prev_page": "",
"query_id": "",
"tip_1": "",
"reset_search": "",
"tips": "",
"results": "",
"tip_2": "",
"tip_3": "",
"tips_sub": "",
"updated_at": ""
},
"profile": {
"url": "",
"user_profile": "",
"user_profile_sub": "",
"active": "",
"change_password": "",
"currency_format": "",
"current_password": "",
"delete_account": "",
"delete_account_sub": "",
"enabled": "",
"gen_invite": "",
"group_settings": "",
"group_settings_sub": "",
"inactive": "",
"new_password": "",
"notifier_modal": "",
"notifiers": "",
"test": "",
"theme_settings": "",
"theme_settings_sub": "",
"update_group": "",
"notifiers_sub": ""
}
}

View File

@@ -3,6 +3,9 @@ import { defineNuxtConfig } from "nuxt/config";
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
ssr: false,
build: {
transpile: ["vue-i18n"],
},
modules: [
"@nuxtjs/tailwindcss",
"@pinia/nuxt",

View File

@@ -16,6 +16,7 @@
"devDependencies": {
"@faker-js/faker": "^8.0.0",
"@iconify-json/mdi": "^1.1.64",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@nuxtjs/eslint-config-typescript": "^12.0.0",
"@types/dompurify": "^3.0.0",
"@types/markdown-it": "^13.0.0",
@@ -27,13 +28,15 @@
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.4.0",
"h3": "^1.7.1",
"intl-messageformat": "^10.5.14",
"isomorphic-fetch": "^3.0.0",
"nuxt": "3.6.5",
"prettier": "^3.2.5",
"typescript": "^5.0.0",
"unplugin-icons": "^0.18.5",
"vite-plugin-eslint": "^1.8.1",
"vitest": "^1.0.0"
"vitest": "^1.0.0",
"vue-i18n": "^9.13.1"
},
"dependencies": {
"@headlessui/vue": "^1.7.9",

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import MdiGithub from "~icons/mdi/github";
import MdiTwitter from "~icons/mdi/twitter";
import MdiDiscord from "~icons/mdi/discord";
import MdiFolder from "~icons/mdi/folder";
import MdiAccount from "~icons/mdi/account";
@@ -8,6 +7,7 @@
import MdiLogin from "~icons/mdi/login";
import MdiArrowRight from "~icons/mdi/arrow-right";
import MdiLock from "~icons/mdi/lock";
import MdiMastodon from '~icons/mdi/mastodon';
useHead({
title: "Homebox | Organize and Tag Your Stuff",
@@ -149,19 +149,19 @@
<AppLogo class="w-12 -mb-4" />
x
</h2>
<p class="ml-1 text-lg text-base-content/50">Track, Organize, and Manage your Things.</p>
<p class="ml-1 text-lg text-base-content/50">{{ $t("index.tagline") }}</p>
</div>
<div class="flex mt-6 sm:mt-0 gap-4 ml-auto text-neutral-content">
<a class="tooltip" data-tip="Project Github" href="https://github.com/sysadminsmedia/homebox" target="_blank">
<a class="tooltip" :data-tip="$t('global.github')" href="https://github.com/sysadminsmedia/homebox" target="_blank">
<MdiGithub class="h-8 w-8" />
</a>
<a href="https://twitter.com/haybytes" class="tooltip" data-tip="Follow The Developer" target="_blank">
<MdiTwitter class="h-8 w-8" />
<a href="https://noc.social/@sysadminsmedia" class="tooltip" :data-tip="$t('global.follow_dev')" target="_blank">
<MdiMastodon class="h-8 w-8" />
</a>
<a href="https://discord.gg/aY4DCkpNA9" class="tooltip" data-tip="Join The Discord" target="_blank">
<a href="https://discord.gg/aY4DCkpNA9" class="tooltip" :data-tip="$t('global.join_discord')" target="_blank">
<MdiDiscord class="h-8 w-8" />
</a>
<a href="https://homebox.sysadminsmedia.com/en/" class="tooltip" data-tip="Read The Docs" target="_blank">
<a href="https://homebox.sysadminsmedia.com/en/" class="tooltip" :data-tip="$t('global.read_docs')" target="_blank">
<MdiFolder class="h-8 w-8" />
</a>
</div>
@@ -174,17 +174,17 @@
<div class="card-body">
<h2 class="card-title text-2xl align-center">
<MdiAccount class="mr-1 w-7 h-7" />
Register
{{ $t("index.register") }}
</h2>
<FormTextField v-model="email" label="Set your email?" />
<FormTextField v-model="username" label="What's your name?" />
<FormTextField v-model="email" :label="$t('index.set_email')" />
<FormTextField v-model="username" :label="$t('index.set_name')" />
<div v-if="!(groupToken == '')" class="pt-4 pb-1 text-center">
<p>You're Joining an Existing Group!</p>
<p>{{ $t("index.joining_group") }}</p>
<button type="button" class="text-xs underline" @click="groupToken = ''">
Don't Want To Join a Group?
{{ $t("index.dont_join_group") }}
</button>
</div>
<FormPassword v-model="password" label="Set your password" />
<FormPassword v-model="password" :label="$t('index.set_password')" />
<PasswordScore v-model:valid="canRegister" :password="password" />
<div class="card-actions justify-end">
<button
@@ -193,7 +193,7 @@
:class="loading ? 'loading' : ''"
:disabled="loading || !canRegister"
>
Register
{{ $t("index.register") }}
</button>
</div>
</div>
@@ -204,17 +204,17 @@
<div class="card-body">
<h2 class="card-title text-2xl align-center">
<MdiAccount class="mr-1 w-7 h-7" />
Login
{{ $t("index.login") }}
</h2>
<template v-if="status && status.demo">
<p class="text-xs italic text-center">This is a demo instance</p>
<p class="text-xs text-center"><b>Email</b> demo@example.com</p>
<p class="text-xs text-center"><b>Password</b> demo</p>
<p class="text-xs text-center"><b>{{ $t("global.email") }}</b> demo@example.com</p>
<p class="text-xs text-center"><b>{{ $t("global.password") }}</b> demo</p>
</template>
<FormTextField v-model="email" label="Email" />
<FormPassword v-model="loginPassword" label="Password" />
<FormTextField v-model="email" :label="$t('global.email')" />
<FormPassword v-model="loginPassword" :label="$t('global.password')" />
<div class="max-w-[140px]">
<FormCheckbox v-model="remember" label="Remember Me" />
<FormCheckbox v-model="remember" :label="$t('index.remember_me')" />
</div>
<div class="card-actions justify-end">
<button
@@ -223,7 +223,7 @@
:class="loading ? 'loading' : ''"
:disabled="loading"
>
Login
{{ $t("index.login") }}
</button>
</div>
</div>
@@ -241,18 +241,21 @@
<MdiLogin v-else class="w-5 h-5 swap-off" />
<MdiArrowRight class="w-5 h-5 swap-on" />
</template>
{{ registerForm ? "Login" : "Register" }}
{{ registerForm ? $t("index.login") : $t("index.register") }}
</BaseButton>
<p v-else class="text-base-content italic text-sm inline-flex items-center gap-2">
<MdiLock class="w-4 h-4 inline-block" />
Registration Disabled
{{ $t("disabled_registration") }}
</p>
</div>
</div>
</div>
</div>
<footer v-if="status" class="mt-auto text-center w-full bottom-0 pb-4">
<p class="text-center text-sm">Version: {{ status.build.version }} ~ Build: {{ status.build.commit }}</p>
<p class="text-center text-sm">
{{ $t("global.version", { version: status.build.version }) }} ~
{{ $t("global.build", { build: status.build.commit }) }}
</p>
</footer>
</div>
</template>

View File

@@ -338,9 +338,9 @@
<div v-if="locations && labels">
<div class="flex flex-wrap md:flex-nowrap gap-4 items-end">
<div class="w-full">
<FormTextField v-model="query" placeholder="Search" />
<FormTextField v-model="query" :placeholder="$t('global.search')" />
<div v-if="byAssetId" class="text-sm pl-2 pt-2">
<p>Querying Asset ID Number: {{ parsedAssetId }}</p>
<p>{{ $t("items.query_id", { id: parsedAssetId }) }}</p>
</div>
</div>
<BaseButton class="btn-block md:w-auto" @click.prevent="submit">
@@ -348,12 +348,12 @@
<MdiLoading v-if="loading" class="animate-spin" />
<MdiMagnify v-else />
</template>
Search
{{ $t("global.search") }}
</BaseButton>
</div>
<div class="flex flex-wrap md:flex-nowrap gap-2 w-full py-2">
<SearchFilter v-model="selectedLocations" label="Locations" :options="locationFlatTree">
<SearchFilter v-model="selectedLocations" :label="$t('global.locations')" :options="locationFlatTree">
<template #display="{ item }">
<div>
<div class="flex w-full">
@@ -365,60 +365,60 @@
</div>
</template>
</SearchFilter>
<SearchFilter v-model="selectedLabels" label="Labels" :options="labels" />
<SearchFilter v-model="selectedLabels" :label="$t('global.labels')" :options="labels" />
<div class="dropdown">
<label tabindex="0" class="btn btn-xs">Options</label>
<label tabindex="0" class="btn btn-xs">{{ $t("items.options") }}</label>
<div
tabindex="0"
class="dropdown-content mt-1 max-h-72 p-4 w-64 overflow-auto shadow bg-base-100 rounded-md -translate-x-24"
>
<label class="label cursor-pointer mr-auto">
<input v-model="includeArchived" type="checkbox" class="toggle toggle-sm toggle-primary" />
<span class="label-text ml-4"> Include Archived Items </span>
<span class="label-text ml-4"> {{ $t("items.include_archive") }} </span>
</label>
<label class="label cursor-pointer mr-auto">
<input v-model="fieldSelector" type="checkbox" class="toggle toggle-sm toggle-primary" />
<span class="label-text ml-4"> Field Selector </span>
<span class="label-text ml-4"> {{ $t("items.field_selector") }} </span>
</label>
<label class="label cursor-pointer mr-auto">
<input v-model="negateLabels" type="checkbox" class="toggle toggle-sm toggle-primary" />
<span class="label-text ml-4"> Negate selected labels </span>
<span class="label-text ml-4"> {{ $t("items.negate_labels") }} </span>
</label>
<label class="label cursor-pointer mr-auto">
<select v-model="orderBy" class="select select-bordered select-sm">
<option value="name" selected>Name</option>
<option value="createdAt">Created At</option>
<option value="updatedAt">Updated At</option>
<option value="name" selected>{{ $t("global.name") }}</option>
<option value="createdAt">{{ $t("items.created_at") }}</option>
<option value="updatedAt">{{ $t("items.updated_at") }}</option>
</select>
<span class="label-text ml-4"> Order By </span>
<span class="label-text ml-4"> {{ $t("items.order_by") }} </span>
</label>
<hr class="my-2" />
<BaseButton class="btn-block btn-sm" @click="reset"> Reset Search</BaseButton>
<BaseButton class="btn-block btn-sm" @click="reset"> {{ $t("items.reset_search") }} </BaseButton>
</div>
</div>
<div class="dropdown ml-auto dropdown-end">
<label tabindex="0" class="btn btn-xs">Tips</label>
<label tabindex="0" class="btn btn-xs">{{ $t("items.tips") }}</label>
<div
tabindex="0"
class="dropdown-content mt-1 p-4 w-[325px] text-sm overflow-auto shadow bg-base-100 rounded-md"
>
<p class="text-base">Search Tips</p>
<p class="text-base">{{ $t("items.tips_sub") }}</p>
<ul class="mt-1 list-disc pl-6">
<li>
Location and label filters use the 'OR' operation. If more than one is selected only one will be
required for a match.
{{ $t("items.tip_1") }}
</li>
<li>Searches prefixed with '#'' will query for a asset ID (example '#000-001')</li>
<li>
Field filters use the 'OR' operation. If more than one is selected only one will be required for a
match.
{{ $t("items.tip_2") }}
</li>
<li>
{{ $t("items.tip_3") }}
</li>
</ul>
</div>
</div>
</div>
<div v-if="fieldSelector" class="py-4 space-y-2">
<p>Custom Fields</p>
<p>{{ $t("items.custom_fields") }}</p>
<div v-for="(f, idx) in fieldTuples" :key="idx" class="flex flex-wrap gap-2">
<div class="form-control w-full max-w-xs">
<label class="label">
@@ -435,7 +435,7 @@
</div>
<div class="form-control w-full max-w-xs">
<label class="label">
<span class="label-text">Field Value</span>
<span class="label-text">{{ $t("items.field_value") }}</span>
</label>
<select v-model="fieldTuples[idx][1]" class="select-bordered select" :items="fieldValuesCache[f[0]]">
<option v-for="v in fieldValuesCache[f[0]]" :key="v" :value="v">{{ v }}</option>
@@ -449,38 +449,42 @@
<MdiDelete class="w-5 h-5" />
</button>
</div>
<BaseButton type="button" class="btn-sm mt-2" @click="() => fieldTuples.push(['', ''])"> Add</BaseButton>
<BaseButton type="button" class="btn-sm mt-2" @click="() => fieldTuples.push(['', ''])">
{{ $t("items.add") }}
</BaseButton>
</div>
</div>
<section class="mt-10">
<BaseSectionHeader ref="itemsTitle"> Items </BaseSectionHeader>
<BaseSectionHeader ref="itemsTitle"> {{ $t("global.items") }} </BaseSectionHeader>
<p class="text-base font-medium flex items-center">
{{ total }} Results
<span class="text-base ml-auto"> Page {{ page }} of {{ totalPages }}</span>
{{ $t("items.results", { total: total }) }}
<span class="text-base ml-auto"> {{ $t("items.pages", { page: page, totalPages: totalPages }) }} </span>
</p>
<div ref="cardgrid" class="grid mt-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<ItemCard v-for="item in items" :key="item.id" :item="item" />
<div class="hidden first:inline text-xl">No Items Found</div>
<div class="hidden first:inline text-xl">{{ $t("items.no_results") }}</div>
</div>
<div v-if="items.length > 0 && (hasNext || hasPrev)" class="mt-10 flex gap-2 flex-col items-center">
<div class="flex">
<div class="btn-group">
<button :disabled="!hasPrev" class="btn text-no-transform" @click="prev">
<MdiChevronLeft class="mr-1 h-6 w-6" name="mdi-chevron-left" />
Prev
{{ $t("items.prev_page") }}
</button>
<button v-if="hasPrev" class="btn text-no-transform" @click="page = 1">{{ $t("items.first") }}</button>
<button v-if="hasNext" class="btn text-no-transform" @click="page = totalPages">
{{ $t("items.last") }}
</button>
<button v-if="hasPrev" class="btn text-no-transform" @click="page = 1">First</button>
<button v-if="hasNext" class="btn text-no-transform" @click="page = totalPages">Last</button>
<button :disabled="!hasNext" class="btn text-no-transform" @click="next">
Next
{{ $t("items.next_page") }}
<MdiChevronRight class="ml-1 h-6 w-6" name="mdi-chevron-right" />
</button>
</div>
</div>
<p class="text-sm font-bold">Page {{ page }} of {{ totalPages }}</p>
<p class="text-sm font-bold">{{ $t("items.pages", { page: page, totalPages: totalPages }) }}</p>
</div>
</section>
</BaseContainer>

View File

@@ -89,13 +89,6 @@
notify.success("Group updated");
}
const pubApi = usePublicApi();
const { data: status } = useAsyncData(async () => {
const { data } = await pubApi.status();
return data;
});
const { setTheme } = useTheme();
const auth = useAuthContext();
@@ -305,11 +298,11 @@
<template>
<div>
<BaseModal v-model="passwordChange.dialog">
<template #title> Change Password </template>
<template #title> {{ $t("profile.change_password") }} </template>
<form @submit.prevent="changePassword">
<FormPassword v-model="passwordChange.current" label="Current Password" placeholder="" />
<FormPassword v-model="passwordChange.new" label="New Password" placeholder="" />
<FormPassword v-model="passwordChange.current" :label="$t('profile.current_password')" placeholder="" />
<FormPassword v-model="passwordChange.new" :label="$t('profile.new_password')" placeholder="" />
<PasswordScore v-model:valid="passwordChange.isValid" :password="passwordChange.new" />
<div class="flex">
@@ -319,26 +312,26 @@
:disabled="!passwordChange.isValid"
type="submit"
>
Submit
{{ $t("global.submit") }}
</BaseButton>
</div>
</form>
</BaseModal>
<BaseModal v-model="notifierDialog">
<template #title> {{ notifier ? "Edit" : "Create" }} Notifier </template>
<template #title> {{ $t("profile.notifier_modal", {type: (notifier != null)}) }} </template>
<form @submit.prevent="createNotifier">
<template v-if="notifier">
<FormTextField v-model="notifier.name" label="Name" />
<FormTextField v-model="notifier.url" label="URL" />
<FormTextField v-model="notifier.name" :label="$t('global.name')" />
<FormTextField v-model="notifier.url" :label="$t('profile.url')" />
<div class="max-w-[100px]">
<FormCheckbox v-model="notifier.isActive" label="Enabled" />
<FormCheckbox v-model="notifier.isActive" :label="$t('profile.enabled')" />
</div>
</template>
<div class="flex gap-2 justify-between mt-4">
<BaseButton :disabled="!(notifier && notifier.url)" type="button" @click="testNotifier"> Test </BaseButton>
<BaseButton type="submit"> Submit </BaseButton>
<BaseButton :disabled="!(notifier && notifier.url)" type="button" @click="testNotifier"> {{ $t("profile.test") }} </BaseButton>
<BaseButton type="submit"> {{ $t("global.submit") }} </BaseButton>
</div>
</form>
</BaseModal>
@@ -348,8 +341,8 @@
<template #title>
<BaseSectionHeader>
<MdiAccount class="mr-2 -mt-1 text-base-600" />
<span class="text-base-600"> User Profile </span>
<template #description> Invite users, and manage your account. </template>
<span class="text-base-600"> {{ $t("profile.user_profile") }} </span>
<template #description> {{ $t("profile.user_profile_sub") }} </template>
</BaseSectionHeader>
</template>
@@ -357,8 +350,8 @@
<div class="p-4">
<div class="flex gap-2">
<BaseButton size="sm" @click="openPassChange"> Change Password </BaseButton>
<BaseButton size="sm" @click="generateToken"> Generate Invite Link </BaseButton>
<BaseButton size="sm" @click="openPassChange"> {{ $t("profile.change_password") }} </BaseButton>
<BaseButton size="sm" @click="generateToken"> {{ $t("profile.gen_invite") }} </BaseButton>
</div>
<div v-if="token" class="pt-4 flex items-center pl-1">
<CopyText class="mr-2 btn-primary btn btn-outline btn-square btn-sm" :text="tokenUrl" />
@@ -375,8 +368,8 @@
<template #title>
<BaseSectionHeader>
<MdiMegaphone class="mr-2 -mt-1 text-base-600" />
<span class="text-base-600"> Notifiers </span>
<template #description> Get notifications for up coming maintenance reminders </template>
<span class="text-base-600"> {{ $t("profile.notifiers") }} </span>
<template #description> {{ $t("profile.notifiers_sub") }} </template>
</BaseSectionHeader>
</template>
@@ -399,11 +392,11 @@
</div>
<div class="flex justify-between py-1 flex-wrap text-sm">
<p>
<span v-if="n.isActive" class="badge badge-success"> Active </span>
<span v-else class="badge badge-error"> Inactive</span>
<span v-if="n.isActive" class="badge badge-success"> {{ $t("profile.active") }} </span>
<span v-else class="badge badge-error"> {{ $t("profile.inactive") }} </span>
</p>
<p>
Created
{{ $t("global.created") }}
<DateTime format="relative" datetime-type="time" :date="n.createdAt" />
</p>
</div>
@@ -411,7 +404,7 @@
</div>
<div class="p-4">
<BaseButton size="sm" @click="openNotifierDialog"> Create </BaseButton>
<BaseButton size="sm" @click="openNotifierDialog"> {{ $t("global.create") }} </BaseButton>
</div>
</BaseCard>
@@ -419,19 +412,19 @@
<template #title>
<BaseSectionHeader class="pb-0">
<MdiAccountMultiple class="mr-2 -mt-1 text-base-600" />
<span class="text-base-600"> Group Settings </span>
<span class="text-base-600"> {{ $t("profile.group_settings") }} </span>
<template #description>
Shared Group Settings. You may need to refresh your browser for some settings to apply.
{{ $t("profile.group_settings_sub") }}
</template>
</BaseSectionHeader>
</template>
<div v-if="group && currencies && currencies.length > 0" class="p-5 pt-0">
<FormSelect v-model="currency" label="Currency Format" :items="currencies" />
<FormSelect v-model="currency" :label="$t('profile.currency_format')" :items="currencies" />
<p class="m-2 text-sm">Example: {{ currencyExample }}</p>
<div class="mt-4">
<BaseButton size="sm" @click="updateGroup"> Update Group </BaseButton>
<BaseButton size="sm" @click="updateGroup"> {{ $t("profile.update_group") }} </BaseButton>
</div>
</div>
</BaseCard>
@@ -440,10 +433,9 @@
<template #title>
<BaseSectionHeader>
<MdiFill class="mr-2 text-base-600" />
<span class="text-base-600"> Theme Settings </span>
<span class="text-base-600"> {{ $t("profile.theme_settings") }} </span>
<template #description>
Theme settings are stored in your browser's local storage. You can change the theme at any time. If you're
having trouble setting your theme try refreshing your browser.
{{ $t("profile.theme_settings_sub") }}
</template>
</BaseSectionHeader>
</template>
@@ -491,18 +483,15 @@
<template #title>
<BaseSectionHeader>
<MdiDelete class="mr-2 -mt-1 text-base-600" />
<span class="text-base-600"> Delete Account</span>
<template #description> Delete your account and all its associated data. </template>
<span class="text-base-600"> {{ $t("profile.delete_account") }} </span>
<template #description> {{ $t("profile.delete_account_sub") }} </template>
</BaseSectionHeader>
</template>
<div class="p-4 px-6 border-t-2 border-gray-300">
<BaseButton size="sm" class="btn-error" @click="deleteProfile"> Delete Account </BaseButton>
<BaseButton size="sm" class="btn-error" @click="deleteProfile"> {{ $t("profile.delete_account") }} </BaseButton>
</div>
</BaseCard>
</BaseContainer>
<footer v-if="status" class="text-center w-full bottom-0 pb-4">
<p class="text-center text-sm">Version: {{ status.build.version }} ~ Build: {{ status.build.commit }}</p>
</footer>
</div>
</template>

63
frontend/plugins/i18n.ts Normal file
View File

@@ -0,0 +1,63 @@
import type { CompileError, MessageCompiler, MessageContext } from "vue-i18n";
import { createI18n } from "vue-i18n";
import { IntlMessageFormat } from "intl-messageformat";
export default defineNuxtPlugin(({ vueApp }) => {
function checkDefaultLanguage() {
let matched = null;
const languages = Object.getOwnPropertyNames(messages())
languages.forEach(lang => {
if (lang === navigator.language) {
matched = lang;
}
});
if (!matched) {
languages.forEach(lang => {
const languagePartials = navigator.language.split('-')[0]
if (lang === languagePartials) {
matched = lang;
}
});
}
return matched;
}
const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: checkDefaultLanguage() || "en",
fallbackLocale: "en",
messageCompiler,
messages: messages(),
});
vueApp.use(i18n);
});
export const messageCompiler: MessageCompiler = (message, { locale, key, onError }) => {
if (typeof message === "string") {
/**
* You can tune your message compiler performance more with your cache strategy or also memoization at here
*/
const formatter = new IntlMessageFormat(message, locale);
return (ctx: MessageContext) => {
return formatter.format(ctx.values);
};
} else {
/**
* for AST.
* If you would like to support it,
* You need to transform locale messages such as `json`, `yaml`, etc. with the bundle plugin.
*/
onError && onError(new Error("not support for AST") as CompileError);
return () => key;
}
};
export const messages: Object = () => {
let messages = {};
const modules = import.meta.glob('~//locales/**.json', { eager: true });
for (const path in modules) {
const key = path.slice(9, -5);
messages[key] = modules[path];
}
return messages;
};

12656
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff