diff --git a/backend/endpoints/user.py b/backend/endpoints/user.py index 0e53b0e04..a05d15083 100644 --- a/backend/endpoints/user.py +++ b/backend/endpoints/user.py @@ -125,7 +125,7 @@ def get_user(request: Request, id: int) -> UserSchema: return UserSchema.model_validate(user) -@protected_route(router.put, "/users/{id}", [Scope.USERS_WRITE]) +@protected_route(router.put, "/users/{id}", [Scope.ME_WRITE]) async def update_user( request: Request, id: int, form_data: Annotated[UserForm, Depends()] ) -> UserSchema: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fb60cd9b4..4a1eeb86e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,6 +28,7 @@ "semver": "^7.6.2", "socket.io-client": "^4.7.5", "vue": "^3.4.27", + "vue-i18n": "^10.0.5", "vue-router": "^4.3.2", "vuetify": "^3.7.4", "webfontloader": "^1.6.28" @@ -2217,6 +2218,47 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@intlify/core-base": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.5.tgz", + "integrity": "sha512-F3snDTQs0MdvnnyzTDTVkOYVAZOE/MHwRvF7mn7Jw1yuih4NrFYLNYIymGlLmq4HU2iIdzYsZ7f47bOcwY73XQ==", + "dependencies": { + "@intlify/message-compiler": "10.0.5", + "@intlify/shared": "10.0.5" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.5.tgz", + "integrity": "sha512-6GT1BJ852gZ0gItNZN2krX5QAmea+cmdjMvsWohArAZ3GmHdnNANEcF9JjPXAMRtQ6Ux5E269ymamg/+WU6tQA==", + "dependencies": { + "@intlify/shared": "10.0.5", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.5.tgz", + "integrity": "sha512-bmsP4L2HqBF6i6uaMqJMcFBONVjKt+siGluRq4Ca4C0q7W2eMaVZr8iCgF9dKbcVXutftkC7D6z2SaSMmLiDyA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "devOptional": true, @@ -7323,6 +7365,25 @@ "eslint": ">=6.0.0" } }, + "node_modules/vue-i18n": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.5.tgz", + "integrity": "sha512-9/gmDlCblz3i8ypu/afiIc/SUIfTTE1mr0mZhb9pk70xo2csHAM9mp2gdQ3KD2O0AM3Hz/5ypb+FycTj/lHlPQ==", + "dependencies": { + "@intlify/core-base": "10.0.5", + "@intlify/shared": "10.0.5", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/vue-router": { "version": "4.3.2", "license": "MIT", diff --git a/frontend/package.json b/frontend/package.json index ff79db188..2d5beb5bb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -46,6 +46,7 @@ "semver": "^7.6.2", "socket.io-client": "^4.7.5", "vue": "^3.4.27", + "vue-i18n": "^10.0.5", "vue-router": "^4.3.2", "vuetify": "^3.7.4", "webfontloader": "^1.6.28" diff --git a/frontend/src/RomM.vue b/frontend/src/RomM.vue index 231a7d8f8..fe4cf2c4b 100644 --- a/frontend/src/RomM.vue +++ b/frontend/src/RomM.vue @@ -5,12 +5,25 @@ import storeHeartbeat from "@/stores/heartbeat"; import api from "@/services/api/index"; import userApi from "@/services/api/user"; import router from "@/plugins/router"; -import { onBeforeMount } from "vue"; +import languageStore from "@/stores/language"; +import { storeToRefs } from "pinia"; +import { onBeforeMount, ref } from "vue"; +import { useI18n } from "vue-i18n"; // Props const heartbeat = storeHeartbeat(); const auth = storeAuth(); const configStore = storeConfig(); +const { locale } = useI18n(); +const storeLanguage = languageStore(); +const { defaultLanguage, languages } = storeToRefs(storeLanguage); +const selectedLanguage = ref( + languages.value.find( + (lang) => lang.value === localStorage.getItem("settings.locale"), + ) || defaultLanguage.value, +); +locale.value = selectedLanguage.value.value; +storeLanguage.setLanguage(selectedLanguage.value); // Functions onBeforeMount(async () => { diff --git a/frontend/src/components/Details/Info/FileInfo.vue b/frontend/src/components/Details/Info/FileInfo.vue index e737ec339..72b0cd701 100644 --- a/frontend/src/components/Details/Info/FileInfo.vue +++ b/frontend/src/components/Details/Info/FileInfo.vue @@ -6,8 +6,10 @@ import storeDownload from "@/stores/download"; import type { DetailedRom } from "@/stores/roms"; import { formatBytes } from "@/utils"; import { ref, watch } from "vue"; +import { useI18n } from "vue-i18n"; // Props +const { t } = useI18n(); const props = defineProps<{ rom: DetailedRom }>(); const downloadStore = storeDownload(); const romUser = ref(props.rom.rom_user); @@ -39,8 +41,8 @@ watch( class="align-center my-3" no-gutters > - - Version + + {{ t("rom.version") }} @@ -49,7 +51,7 @@ watch( location="top" class="tooltip" transition="fade-transition" - text="Set as default version" + :text="t('rom.set-as-default')" open-delay="300" > @@ -76,16 +77,16 @@ watch( - - File + + {{ t("rom.file") }} {{ rom.file_name }} - - Files + + {{ t("rom.files") }} - - Info + + {{ t("rom.info") }} - Size{{ t("rom.size") }}{{ formatBytes(rom.file_size_bytes) }} @@ -132,8 +133,8 @@ watch( - - Tags + + {{ t("rom.tags") }} (); const { xs } = useDisplay(); const show = ref(false); const carousel = ref(0); const router = useRouter(); -const filters = ["genres", "franchises", "collections", "companies"] as const; +const filters = [ + { value: "genres", name: t("rom.genres") }, + { value: "franchises", name: t("rom.franchises") }, + { value: "collections", name: t("rom.collections") }, + { value: "companies", name: t("rom.companies") }, +] as const; const galleryViewStore = storeGalleryView(); const { defaultAspectRatioScreenshot } = storeToRefs(galleryViewStore); @@ -36,7 +43,7 @@ function onFilterClick(filter: FilterType, value: string) { no-gutters class="align-center my-3" > - + RomM Collections @@ -62,18 +69,18 @@ function onFilterClick(filter: FilterType, value: string) { {{ t("rom.no-states-found") }} - Scan platform + {{ t("scan.scan") }} diff --git a/frontend/src/components/Gallery/AppBar/common/FilterTextField.vue b/frontend/src/components/Gallery/AppBar/common/FilterTextField.vue index 7aa27fc84..e1933eb29 100644 --- a/frontend/src/components/Gallery/AppBar/common/FilterTextField.vue +++ b/frontend/src/components/Gallery/AppBar/common/FilterTextField.vue @@ -5,8 +5,10 @@ import { debounce } from "lodash"; import type { Emitter } from "mitt"; import { storeToRefs } from "pinia"; import { inject, nextTick } from "vue"; +import { useI18n } from "vue-i18n"; // Props +const { t } = useI18n(); const emitter = inject>("emitter"); const galleryFilterStore = storeGalleryFilter(); const { filterSearch } = storeToRefs(galleryFilterStore); @@ -24,7 +26,7 @@ function clear() { import storeGalleryView from "@/stores/galleryView"; import { views } from "@/utils"; +import { useI18n } from "vue-i18n"; // Props +const { t } = useI18n(); const galleryView = storeGalleryView(); @@ -11,7 +13,7 @@ const galleryView = storeGalleryView(); location="bottom" class="tooltip" transition="fade-transition" - text="Change view" + :text="t('platform.change-view')" open-delay="1000" >