mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-06-02 21:25:45 -04:00
Enable localization based on browser settings, add language selector (#925)
* Enable localization based on browser settings, add language selector * Add dialog for language selection
This commit is contained in:
parent
8f569509bf
commit
022cbd1616
194
frontend/components/global/LanguageDialog.vue
Normal file
194
frontend/components/global/LanguageDialog.vue
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
<template>
|
||||||
|
<BaseDialog v-model="dialog" :icon="$globals.icons.translate" :title="$t('language-dialog.choose-language')">
|
||||||
|
<v-card-text>
|
||||||
|
{{ $t('language-dialog.select-description') }}
|
||||||
|
<v-select
|
||||||
|
v-model="locale"
|
||||||
|
:items="locales"
|
||||||
|
item-text="name"
|
||||||
|
menu-props="auto"
|
||||||
|
outlined
|
||||||
|
></v-select>
|
||||||
|
<i18n path="language-dialog.how-to-contribute-description">
|
||||||
|
<template #read-the-docs-link>
|
||||||
|
<a href="https://docs.mealie.io/contributors/translating/" target="_blank">{{ $t("language-dialog.read-the-docs") }}</a>
|
||||||
|
</template>
|
||||||
|
</i18n>
|
||||||
|
</v-card-text>
|
||||||
|
</BaseDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||||
|
import type { LocaleObject } from "@nuxtjs/i18n";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props, context) {
|
||||||
|
const dialog = computed<boolean>({
|
||||||
|
get() {
|
||||||
|
return props.value;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
context.emit("input", val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { i18n } = useContext();
|
||||||
|
|
||||||
|
const locales = [
|
||||||
|
{
|
||||||
|
name: "American English",
|
||||||
|
value: "en-US",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "British English",
|
||||||
|
value: "en-GB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Afrikaans (Afrikaans)",
|
||||||
|
value: "af-ZA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "العربية (Arabic)",
|
||||||
|
value: "ar-SA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Català (Catalan)",
|
||||||
|
value: "ca-ES",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Čeština (Czech)",
|
||||||
|
value: "cs-CZ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Dansk (Danish)",
|
||||||
|
value: "da-DK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Deutsch (German)",
|
||||||
|
value: "de-DE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ελληνικά (Greek)",
|
||||||
|
value: "el-GR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Español (Spanish)",
|
||||||
|
value: "es-ES",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Suomi (Finnish)",
|
||||||
|
value: "fi-FI",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Français (French)",
|
||||||
|
value: "fr-FR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "עברית (Hebrew)",
|
||||||
|
value: "he-IL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Magyar (Hungarian)",
|
||||||
|
value: "hu-HU",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Italiano (Italian)",
|
||||||
|
value: "it-IT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "日本語 (Japanese)",
|
||||||
|
value: "ja-JP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "한국어 (Korean)",
|
||||||
|
value: "ko-KR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Norsk (Norwegian)",
|
||||||
|
value: "no-NO",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nederlands (Dutch)",
|
||||||
|
value: "nl-NL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Polski (Polish)",
|
||||||
|
value: "pl-PL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Português do Brasil (Brazilian Portugese)",
|
||||||
|
value: "pt-BR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Português (Portugese)",
|
||||||
|
value: "pt-PT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Română (Romanian)",
|
||||||
|
value: "ro-RO",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pусский (Russian)",
|
||||||
|
value: "ru-RU",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "српски (Serbian)",
|
||||||
|
value: "sr-SP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Svenska (Swedish)",
|
||||||
|
value: "sv-SE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Türkçe (Turkish)",
|
||||||
|
value: "tr-TR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Українська (Ukrainian)",
|
||||||
|
value: "uk-UA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tiếng Việt (Vietnamese)",
|
||||||
|
value: "vi-VN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "简体中文 (Chinese simplified)",
|
||||||
|
value: "zh-CN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "繁體中文 (Chinese traditional)",
|
||||||
|
value: "zh-TW",
|
||||||
|
},
|
||||||
|
].filter(locale => (i18n.locales as LocaleObject[]).map(i18nLocale => i18nLocale.code).includes(locale.value));
|
||||||
|
|
||||||
|
const locale = computed<string>({
|
||||||
|
get() {
|
||||||
|
return i18n.locale;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
i18n.setLocale(value);
|
||||||
|
// Reload the page to update the language - not all strings are reactive
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
dialog,
|
||||||
|
i18n,
|
||||||
|
locales,
|
||||||
|
locale,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
@ -415,7 +415,8 @@
|
|||||||
"search": "Search",
|
"search": "Search",
|
||||||
"site-settings": "Site Settings",
|
"site-settings": "Site Settings",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"toolbox": "Toolbox"
|
"toolbox": "Toolbox",
|
||||||
|
"language": "Language"
|
||||||
},
|
},
|
||||||
"signup": {
|
"signup": {
|
||||||
"error-signing-up": "Error Signing Up",
|
"error-signing-up": "Error Signing Up",
|
||||||
@ -494,5 +495,11 @@
|
|||||||
"webhooks-enabled": "Webhooks Enabled",
|
"webhooks-enabled": "Webhooks Enabled",
|
||||||
"you-are-not-allowed-to-create-a-user": "You are not allowed to create a user",
|
"you-are-not-allowed-to-create-a-user": "You are not allowed to create a user",
|
||||||
"you-are-not-allowed-to-delete-this-user": "You are not allowed to delete this user"
|
"you-are-not-allowed-to-delete-this-user": "You are not allowed to delete this user"
|
||||||
|
},
|
||||||
|
"language-dialog": {
|
||||||
|
"choose-language": "Choose language",
|
||||||
|
"select-description": "Choose the language for the Mealie UI. The setting only applies to you, not other users.",
|
||||||
|
"how-to-contribute-description": "Is something not translated yet, mistranslated, or your language missing from the list? {read-the-docs-link} on how to contribute!",
|
||||||
|
"read-the-docs": "Read the docs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,15 @@
|
|||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
<template #bottom>
|
<template #bottom>
|
||||||
|
<v-list-item @click.stop="languageDialog = true">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>{{ $globals.icons.translate }}</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>{{ $t('sidebar.language') }}</v-list-item-title>
|
||||||
|
<LanguageDialog v-model="languageDialog" />
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
<v-list-item @click="toggleDark">
|
<v-list-item @click="toggleDark">
|
||||||
<v-list-item-icon>
|
<v-list-item-icon>
|
||||||
<v-icon>
|
<v-icon>
|
||||||
@ -64,12 +73,13 @@
|
|||||||
import { computed, defineComponent, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
import { computed, defineComponent, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
||||||
import AppHeader from "@/components/Layout/AppHeader.vue";
|
import AppHeader from "@/components/Layout/AppHeader.vue";
|
||||||
import AppSidebar from "@/components/Layout/AppSidebar.vue";
|
import AppSidebar from "@/components/Layout/AppSidebar.vue";
|
||||||
|
import LanguageDialog from "~/components/global/LanguageDialog.vue";
|
||||||
import TheSnackbar from "@/components/Layout/TheSnackbar.vue";
|
import TheSnackbar from "@/components/Layout/TheSnackbar.vue";
|
||||||
import { useCookbooks } from "~/composables/use-group-cookbooks";
|
import { useCookbooks } from "~/composables/use-group-cookbooks";
|
||||||
import { useToggleDarkMode } from "~/composables/use-utils";
|
import { useToggleDarkMode } from "~/composables/use-utils";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { AppHeader, AppSidebar, TheSnackbar },
|
components: { AppHeader, AppSidebar, LanguageDialog, TheSnackbar },
|
||||||
middleware: "auth",
|
middleware: "auth",
|
||||||
setup() {
|
setup() {
|
||||||
const { cookbooks } = useCookbooks();
|
const { cookbooks } = useCookbooks();
|
||||||
@ -79,6 +89,8 @@ export default defineComponent({
|
|||||||
|
|
||||||
const toggleDark = useToggleDarkMode();
|
const toggleDark = useToggleDarkMode();
|
||||||
|
|
||||||
|
const languageDialog = ref<boolean>(false);
|
||||||
|
|
||||||
const sidebar = ref<boolean | null>(null);
|
const sidebar = ref<boolean | null>(null);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -95,7 +107,7 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return { cookbookLinks, isAdmin, toggleDark, sidebar };
|
return { cookbookLinks, isAdmin, languageDialog, toggleDark, sidebar };
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -170,7 +170,12 @@ export default {
|
|||||||
// END: MESSAGE_LOCALES
|
// END: MESSAGE_LOCALES
|
||||||
],
|
],
|
||||||
lazy: true,
|
lazy: true,
|
||||||
|
strategy: "no_prefix",
|
||||||
langDir: "lang/messages",
|
langDir: "lang/messages",
|
||||||
|
detectBrowserLanguage: {
|
||||||
|
useCookie: true,
|
||||||
|
alwaysRedirect: true,
|
||||||
|
},
|
||||||
defaultLocale: "en-US",
|
defaultLocale: "en-US",
|
||||||
vueI18n: {
|
vueI18n: {
|
||||||
dateTimeFormats: {
|
dateTimeFormats: {
|
||||||
@ -208,8 +213,8 @@ export default {
|
|||||||
"vi-VN": require("./lang/dateTimeFormats/vi-VN.json"),
|
"vi-VN": require("./lang/dateTimeFormats/vi-VN.json"),
|
||||||
// END: DATE_LOCALES
|
// END: DATE_LOCALES
|
||||||
},
|
},
|
||||||
|
fallbackLocale: "en-US",
|
||||||
},
|
},
|
||||||
fallbackLocale: "es",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Axios module configuration: https://go.nuxtjs.dev/config-axios
|
// Axios module configuration: https://go.nuxtjs.dev/config-axios
|
||||||
|
2
frontend/types/components.d.ts
vendored
2
frontend/types/components.d.ts
vendored
@ -15,6 +15,7 @@
|
|||||||
import DevDumpJson from "@/components/global/DevDumpJson.vue";
|
import DevDumpJson from "@/components/global/DevDumpJson.vue";
|
||||||
import InputQuantity from "@/components/global/InputQuantity.vue";
|
import InputQuantity from "@/components/global/InputQuantity.vue";
|
||||||
import ToggleState from "@/components/global/ToggleState.vue";
|
import ToggleState from "@/components/global/ToggleState.vue";
|
||||||
|
import LanguageDialog from "~/components/global/LanguageDialog.vue";
|
||||||
import AppButtonCopy from "@/components/global/AppButtonCopy.vue";
|
import AppButtonCopy from "@/components/global/AppButtonCopy.vue";
|
||||||
import CrudTable from "@/components/global/CrudTable.vue";
|
import CrudTable from "@/components/global/CrudTable.vue";
|
||||||
import InputColor from "@/components/global/InputColor.vue";
|
import InputColor from "@/components/global/InputColor.vue";
|
||||||
@ -49,6 +50,7 @@ declare module "vue" {
|
|||||||
DevDumpJson: typeof DevDumpJson;
|
DevDumpJson: typeof DevDumpJson;
|
||||||
InputQuantity: typeof InputQuantity;
|
InputQuantity: typeof InputQuantity;
|
||||||
ToggleState: typeof ToggleState;
|
ToggleState: typeof ToggleState;
|
||||||
|
LanguageDialog: typeof LanguageDialog;
|
||||||
AppButtonCopy: typeof AppButtonCopy;
|
AppButtonCopy: typeof AppButtonCopy;
|
||||||
CrudTable: typeof CrudTable;
|
CrudTable: typeof CrudTable;
|
||||||
InputColor: typeof InputColor;
|
InputColor: typeof InputColor;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user