mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-27 16:24:31 -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:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						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", | ||||
|     "site-settings": "Site Settings", | ||||
|     "tags": "Tags", | ||||
|     "toolbox": "Toolbox" | ||||
|     "toolbox": "Toolbox", | ||||
|     "language": "Language" | ||||
|   }, | ||||
|   "signup": { | ||||
|     "error-signing-up": "Error Signing Up", | ||||
| @@ -494,5 +495,11 @@ | ||||
|     "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-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-menu> | ||||
|       <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-icon> | ||||
|             <v-icon> | ||||
| @@ -64,12 +73,13 @@ | ||||
| import { computed, defineComponent, onMounted, ref, useContext } from "@nuxtjs/composition-api"; | ||||
| import AppHeader from "@/components/Layout/AppHeader.vue"; | ||||
| import AppSidebar from "@/components/Layout/AppSidebar.vue"; | ||||
| import LanguageDialog from "~/components/global/LanguageDialog.vue"; | ||||
| import TheSnackbar from "@/components/Layout/TheSnackbar.vue"; | ||||
| import { useCookbooks } from "~/composables/use-group-cookbooks"; | ||||
| import { useToggleDarkMode } from "~/composables/use-utils"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { AppHeader, AppSidebar, TheSnackbar }, | ||||
|   components: { AppHeader, AppSidebar, LanguageDialog, TheSnackbar }, | ||||
|   middleware: "auth", | ||||
|   setup() { | ||||
|     const { cookbooks } = useCookbooks(); | ||||
| @@ -79,6 +89,8 @@ export default defineComponent({ | ||||
|  | ||||
|     const toggleDark = useToggleDarkMode(); | ||||
|  | ||||
|     const languageDialog = ref<boolean>(false); | ||||
|  | ||||
|     const sidebar = ref<boolean | null>(null); | ||||
|  | ||||
|     onMounted(() => { | ||||
| @@ -95,7 +107,7 @@ export default defineComponent({ | ||||
|         }; | ||||
|       }); | ||||
|     }); | ||||
|     return { cookbookLinks, isAdmin, toggleDark, sidebar }; | ||||
|     return { cookbookLinks, isAdmin, languageDialog, toggleDark, sidebar }; | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|   | ||||
| @@ -170,7 +170,12 @@ export default { | ||||
|       // END: MESSAGE_LOCALES | ||||
|     ], | ||||
|     lazy: true, | ||||
|     strategy: "no_prefix", | ||||
|     langDir: "lang/messages", | ||||
|     detectBrowserLanguage: { | ||||
|       useCookie: true, | ||||
|       alwaysRedirect: true, | ||||
|     }, | ||||
|     defaultLocale: "en-US", | ||||
|     vueI18n: { | ||||
|       dateTimeFormats: { | ||||
| @@ -208,8 +213,8 @@ export default { | ||||
|         "vi-VN": require("./lang/dateTimeFormats/vi-VN.json"), | ||||
|         // END: DATE_LOCALES | ||||
|       }, | ||||
|       fallbackLocale: "en-US", | ||||
|     }, | ||||
|     fallbackLocale: "es", | ||||
|   }, | ||||
|  | ||||
|   // 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 InputQuantity from "@/components/global/InputQuantity.vue"; | ||||
|  import ToggleState from "@/components/global/ToggleState.vue"; | ||||
|  import LanguageDialog from "~/components/global/LanguageDialog.vue"; | ||||
|  import AppButtonCopy from "@/components/global/AppButtonCopy.vue"; | ||||
|  import CrudTable from "@/components/global/CrudTable.vue"; | ||||
|  import InputColor from "@/components/global/InputColor.vue"; | ||||
| @@ -49,6 +50,7 @@ declare module "vue" { | ||||
|      DevDumpJson: typeof DevDumpJson; | ||||
|      InputQuantity: typeof InputQuantity; | ||||
|      ToggleState: typeof ToggleState; | ||||
|      LanguageDialog: typeof LanguageDialog; | ||||
|      AppButtonCopy: typeof AppButtonCopy; | ||||
|      CrudTable: typeof CrudTable; | ||||
|      InputColor: typeof InputColor; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user