fix(front): translations

This commit is contained in:
jon r 2025-04-03 12:49:38 +02:00
parent 02daeab46e
commit 60f38a3117
20 changed files with 69 additions and 138 deletions

View File

@ -1,105 +0,0 @@
<script setup lang="ts">
import { useIntervalFn, useStyleTag, useToggle, useWindowSize } from '@vueuse/core'
import { computed, defineAsyncComponent, nextTick, onMounted } from 'vue'
import { useQueue } from '~/composables/audio/queue'
import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
const { t } = useI18n()
const ChannelUploadModal = defineAsyncComponent(() => import('~/components/channels/UploadModal.vue'))
const PlaylistModal = defineAsyncComponent(() => import('~/components/playlists/PlaylistModal.vue'))
const FilterModal = defineAsyncComponent(() => import('~/components/moderation/FilterModal.vue'))
const ReportModal = defineAsyncComponent(() => import('~/components/moderation/ReportModal.vue'))
const ServiceMessages = defineAsyncComponent(() => import('~/components/ServiceMessages.vue'))
const ShortcutsModal = defineAsyncComponent(() => import('~/components/ShortcutsModal.vue'))
const AudioPlayer = defineAsyncComponent(() => import('~/components/audio/Player.vue'))
const Sidebar = defineAsyncComponent(() => import('~/components/Sidebar.vue'))
const Queue = defineAsyncComponent(() => import('~/components/Queue.vue'))
const store = useStore()
// Tracks
const { tracks } = useQueue()
// Fake content
onMounted(async () => {
await nextTick()
document.getElementById('fake-content')?.classList.add('loaded')
})
// Styles
const customStylesheets = computed(() => {
return store.state.instance.frontSettings.additionalStylesheets ?? []
})
useStyleTag(computed(() => store.state.instance.settings.ui.custom_css.value))
// Time ago
useIntervalFn(() => {
// used to redraw ago dates every minute
store.commit('ui/computeLastDate')
}, 1000 * 60)
// Shortcuts
const [showShortcutsModal, toggleShortcutsModal] = useToggle(false)
onKeyboardShortcut('h', () => toggleShortcutsModal())
const { width } = useWindowSize()
</script>
<template>
<div
:key="store.state.instance.instanceUrl"
:class="{
'has-bottom-player': tracks.length > 0,
'queue-focused': store.state.ui.queueFocused
}"
>
<!-- here, we display custom stylesheets, if any -->
<link
v-for="url in customStylesheets"
:key="url"
rel="stylesheet"
property="stylesheet"
:href="url"
>
<sidebar
:width="width"
@show:shortcuts-modal="toggleShortcutsModal"
/>
<service-messages />
<transition name="queue">
<queue v-show="store.state.ui.queueFocused" />
</transition>
<router-view v-slot="{ Component }">
<template v-if="Component">
<keep-alive :max="1">
<Suspense>
<component :is="Component" />
<template #fallback>
<!-- TODO (wvffle): Add loader -->
{{ t('App.loading') }}
</template>
</Suspense>
</keep-alive>
</template>
<template v-else>
<!-- Display a proper 404 page or error message -->
<h1>404 - Page Not Found</h1>
</template>
</router-view>
<audio-player />
<playlist-modal v-if="store.state.auth.authenticated" />
<channel-upload-modal v-if="store.state.auth.authenticated" />
<filter-modal v-if="store.state.auth.authenticated" />
<report-modal />
<shortcuts-modal v-model:show="showShortcutsModal" />
</div>
</template>

View File

@ -7,6 +7,8 @@ import { ref, computed, watch, nextTick } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
// TODO: Delete this file?
const { t } = useI18n() const { t } = useI18n()
interface Props { interface Props {
@ -65,6 +67,7 @@ const checkAndSwitch = async (url: string) => {
</script> </script>
<template> <template>
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
<Modal <Modal
v-model="show" v-model="show"
:title="t('views.ChooseInstance.header.chooseInstance')" :title="t('views.ChooseInstance.header.chooseInstance')"
@ -181,4 +184,5 @@ const checkAndSwitch = async (url: string) => {
</button> </button>
</div> </div>
</Modal> </Modal>
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
</template> </template>

View File

@ -250,8 +250,7 @@ const save = async () => {
red red
> >
<h4 class="header"> <h4 class="header">
{{ group.label }}: {{ t('components.admin.SettingsGroup.header.error', {label: group.label}) }}
{{ t('components.admin.SettingsGroup.header.error') }}
</h4> </h4>
<ul class="list"> <ul class="list">
<li <li

View File

@ -71,7 +71,7 @@ watch(albums, (value) => {
icon="bi-plus" icon="bi-plus"
:to="useModal('album').to" :to="useModal('album').to"
> >
Add Album {{ t('components.channels.AlbumSelect.add') }}
<AlbumModal <AlbumModal
v-model="model.channel" v-model="model.channel"
@created="fetchAlbums" @created="fetchAlbums"

View File

@ -6,6 +6,7 @@ import { computed, reactive, ref } from 'vue'
import { isEqual, clone } from 'lodash-es' import { isEqual, clone } from 'lodash-es'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useRoute } from 'vue-router'
import axios from 'axios' import axios from 'axios'
@ -36,6 +37,7 @@ const props = withDefaults(defineProps<Props>(), {
const { t } = useI18n() const { t } = useI18n()
const configs = useEditConfigs() const configs = useEditConfigs()
const store = useStore() const store = useStore()
const route = useRoute()
const config = computed(() => configs[props.objectType]) const config = computed(() => configs[props.objectType])
const currentState = computed(() => config.value.fields.reduce((state: ReviewState, field) => { const currentState = computed(() => config.value.fields.reduce((state: ReviewState, field) => {
@ -157,13 +159,15 @@ const resetField = (fieldId: string) => {
{{ t('components.library.EditForm.button.new') }} {{ t('components.library.EditForm.button.new') }}
</Button> </Button>
<!-- TODO: Implement link back to all types of object -->
<Link <Link
v-if="route.path.includes('album')"
solid solid
secondary secondary
raised raised
:to="{ name: 'library.albums.detail', params: { id: object.id } }" :to="{ name: 'library.albums.detail', params: { id: object.id } }"
> >
Back to Album {{ t('components.library.EditForm.button.backToAlbum') }}
</Link> </Link>
</Alert> </Alert>
<Layout <Layout

View File

@ -426,7 +426,7 @@ const isServerDisclosureOpen = ref(false)
'yellow': files.length > uploadedFilesCount + erroredFilesCount 'yellow': files.length > uploadedFilesCount + erroredFilesCount
}" }"
> >
{{ uploadedFilesCount + erroredFilesCount }} / {{ files.length }} {{ t('components.library.FileUpload.table.upload.progressNum', {current: uploadedFilesCount + erroredFilesCount, total: files.length}) }}
</Pill> </Pill>
</Layout> </Layout>
<Layout <Layout
@ -435,7 +435,7 @@ const isServerDisclosureOpen = ref(false)
> >
<label>{{ t('components.library.FileUpload.link.processing') }}</label> <label>{{ t('components.library.FileUpload.link.processing') }}</label>
<Pill> <Pill>
{{ processedFilesCount }} / {{ processableFiles }} {{ t('components.library.FileUpload.table.upload.progressNum', {current: processedFilesCount, total: processableFiles}) }}
</Pill> </Pill>
</Layout> </Layout>
</Layout> </Layout>

View File

@ -330,7 +330,7 @@
"save": "Save" "save": "Save"
}, },
"header": { "header": {
"error": "Error while saving settings.", "error": "{label}: Error while saving settings.",
"image": "Current image" "image": "Current image"
}, },
"message": { "message": {
@ -1027,6 +1027,7 @@
} }
}, },
"AlbumSelect": { "AlbumSelect": {
"add": "Add Album",
"label": { "label": {
"album": "Album", "album": "Album",
"series": "Series" "series": "Series"
@ -1642,6 +1643,7 @@
}, },
"EditForm": { "EditForm": {
"button": { "button": {
"backToAlbum": "Back to Album",
"cancel": "Cancel", "cancel": "Cancel",
"clear": "Clear", "clear": "Clear",
"new": "Submit another edit", "new": "Submit another edit",
@ -1716,6 +1718,7 @@
"size": "Size", "size": "Size",
"status": "Status" "status": "Status"
}, },
"progressNum": "{current} / {total}",
"progress": "{percent}%", "progress": "{percent}%",
"status": { "status": {
"pending": "Pending", "pending": "Pending",
@ -3253,6 +3256,16 @@
"newAppVersion": "A new version of the app is available." "newAppVersion": "A new version of the app is available."
} }
}, },
"modals": {
"search": {
"tryAgain": "If the following link does not work, wait a few seconds and try again"
},
"upload": {
"library": "Host music you listen to",
"musicChannel": "Publish music you make",
"podcastChannel": "Publish podcasts you make"
}
},
"views": { "views": {
"ChooseInstance": { "ChooseInstance": {
"button": { "button": {
@ -4371,7 +4384,7 @@
"currentUsage": "Current usage" "currentUsage": "Current usage"
}, },
"label": { "label": {
"currentUsage": "{amount} used on {max} allowed", "currentUsage": "{currentAmount} used on {max} allowed",
"errored": "Errored files", "errored": "Errored files",
"pending": "Pending files", "pending": "Pending files",
"percentUsed": "{progress}%", "percentUsed": "{progress}%",
@ -4583,7 +4596,9 @@
"tracks": "Tracks" "tracks": "Tracks"
}, },
"meta": { "meta": {
"tracks": "Playlist containing {n} track, by {username} | Playlist containing {n} tracks, by {username}" "attribution": "by",
"tracks": "Playlist containing {n} track, by {username} | Playlist containing {n} tracks, by {username}",
"updated": "updated"
}, },
"modal": { "modal": {
"delete": { "delete": {

View File

@ -355,19 +355,6 @@ const moderationNotifications = computed(() =>
{{ t('components.Sidebar.link.about') }} {{ t('components.Sidebar.link.about') }}
</Link> </Link>
<Spacer shrink /> <Spacer shrink />
<Link
thin-font
to="/privacy"
>
Privacy
</Link>
<Spacer shrink />
<Link
thin-font
to="/legal"
>
Legal
</Link>
</Layout> </Layout>
</Layout> </Layout>
</Layout> </Layout>

View File

@ -6,6 +6,8 @@ import UploadList from '~/ui/components/UploadList.vue'
import { UseTimeAgo } from '@vueuse/components' import { UseTimeAgo } from '@vueuse/components'
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
// TODO: Delete this file, please.
defineProps<{ groups: UploadGroup[], isUploading?: boolean }>() defineProps<{ groups: UploadGroup[], isUploading?: boolean }>()
const openUploadGroup = ref<UploadGroup>() const openUploadGroup = ref<UploadGroup>()
@ -45,6 +47,7 @@ const getDescription = (group: UploadGroup) => {
</script> </script>
<template> <template>
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
<div> <div>
<div <div
v-for="group of groups" v-for="group of groups"
@ -161,6 +164,7 @@ const getDescription = (group: UploadGroup) => {
</VerticalCollapse> </VerticalCollapse>
</div> </div>
</div> </div>
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -10,9 +10,12 @@ defineProps<{
wide?: boolean wide?: boolean
}>() }>()
// TODO: Delete this file, please.
</script> </script>
<template> <template>
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
<div class="file-list"> <div class="file-list">
<div <div
v-for="track in uploads" v-for="track in uploads"
@ -106,6 +109,7 @@ defineProps<{
/> />
</div> </div>
</div> </div>
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -63,7 +63,7 @@ const continueInBackground = () => {
return router.push('/upload/running') return router.push('/upload/running')
} }
// TODO (whole file): Translations // TODO (whole file): Delete this file, please.
// Sorting // Sorting
const sortItems = reactive([ const sortItems = reactive([
@ -94,6 +94,7 @@ const isOpen = computed({
</script> </script>
<template> <template>
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
<Modal <Modal
v-model="isOpen" v-model="isOpen"
title="Upload..." title="Upload..."
@ -162,6 +163,7 @@ const isOpen = computed({
</Button> </Button>
</template> </template>
</Modal> </Modal>
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -492,7 +492,7 @@ watch(queryDebounced, search, { immediate: true })
<!-- If response has "url": "webfinger://node1@node1.funkwhale.test" -> Link to go directly to the federation page --> <!-- If response has "url": "webfinger://node1@node1.funkwhale.test" -> Link to go directly to the federation page -->
<span v-if="category.type === 'rss' && count(category) > 0"> <span v-if="category.type === 'rss' && count(category) > 0">
<Alert>If the following link does not work, wait a few seconds and try again</Alert> <Alert>{{ t('modals.search.tryAgain') }}</Alert>
<Link <Link
v-for="channel in resultsPerCategory(category)" v-for="channel in resultsPerCategory(category)"
:key="channel.artist.fid" :key="channel.artist.fid"
@ -504,7 +504,8 @@ watch(queryDebounced, search, { immediate: true })
</span> </span>
<span v-else-if="category.type === 'federation'"> <span v-else-if="category.type === 'federation'">
TODO: {{ resultsPerCategory(category) }} <!-- TODO: Federation search: backend adapter + display, fix results_per_category query -->
<!-- {{ resultsPerCategory(category) }} -->
</span> </span>
<EmptyState <EmptyState

View File

@ -110,7 +110,7 @@ const channelUpload = ref()
:class="$style.icon" :class="$style.icon"
/> />
</template> </template>
{{ "Host music you listen to" /* TODO: Translate */ }} {{ t('modals.upload.library') }}
</Card> </Card>
<Card <Card
small small
@ -125,7 +125,7 @@ const channelUpload = ref()
:class="$style.icon" :class="$style.icon"
/> />
</template> </template>
{{ "Publish music you make" /* TODO: Translate */ }} {{ t('modals.upload.musicChannel') }}
</Card> </Card>
<Card <Card
small small
@ -140,7 +140,7 @@ const channelUpload = ref()
:class="$style.icon" :class="$style.icon"
/> />
</template> </template>
{{ "Publish podcasts you make" /* TODO: Translate */ }} {{ t('modals.upload.podcastChannel') }}
</Card> </Card>
</Layout> </Layout>

View File

@ -4,6 +4,8 @@ import { useUploadsStore } from '~/ui/stores/upload'
import { bytesToHumanSize } from '~/ui/composables/bytes' import { bytesToHumanSize } from '~/ui/composables/bytes'
import UploadModal from '~/ui/components/UploadModal.vue' import UploadModal from '~/ui/components/UploadModal.vue'
// TODO: Delete this file?
const filesystemStats = reactive({ const filesystemStats = reactive({
total: 10737418240, total: 10737418240,
used: 3e9 used: 3e9
@ -40,6 +42,7 @@ const tabs = computed(() => [
</script> </script>
<template> <template>
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
<div class="flex items-center"> <div class="flex items-center">
<h1 class="mr-auto"> <h1 class="mr-auto">
Upload Upload
@ -82,6 +85,7 @@ const tabs = computed(() => [
<RouterView /> <RouterView />
<UploadModal /> <UploadModal />
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -5,6 +5,8 @@ import { bytesToHumanSize } from '~/ui/composables/bytes'
import { useUploadsStore, type UploadGroupEntry } from '~/ui/stores/upload' import { useUploadsStore, type UploadGroupEntry } from '~/ui/stores/upload'
import CoverArt from '~/ui/components/CoverArt.vue' import CoverArt from '~/ui/components/CoverArt.vue'
// TODO: Delete this file?
interface Recording { interface Recording {
guid: string guid: string
title: string title: string
@ -49,6 +51,7 @@ const columns = [
</script> </script>
<template> <template>
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
<div <div
v-if="allTracks.length === 0" v-if="allTracks.length === 0"
class="flex flex-col items-center py-32" class="flex flex-col items-center py-32"
@ -80,6 +83,7 @@ const columns = [
{{ intl.format(value) }} {{ intl.format(value) }}
</template> </template>
</FwTable> </FwTable>
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</template> </template>
<style scoped> <style scoped>

View File

@ -5,6 +5,8 @@ import { ref } from 'vue'
import axios from 'axios' import axios from 'axios'
import { useAsyncState } from '@vueuse/core' import { useAsyncState } from '@vueuse/core'
// TODO: Delete this file?
interface Tab { interface Tab {
label: string label: string
icon: string icon: string
@ -49,6 +51,7 @@ const { state: items } = useAsyncState(
</script> </script>
<template> <template>
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
<div class="upload"> <div class="upload">
<p> Select a destination for your audio files: </p> <p> Select a destination for your audio files: </p>
@ -98,6 +101,7 @@ const { state: items } = useAsyncState(
Open library Open library
</FwButton> </FwButton>
</div> </div>
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -22,6 +22,8 @@ import Button from '~/components/ui/Button.vue'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
// TODO: Depreciate & refactor essential functionality to new ui/modals/Search.vue
type QueryType = 'artists' | 'albums' | 'tracks' | 'playlists' | 'tags' | 'radios' | 'podcasts' | 'series' | 'rss' type QueryType = 'artists' | 'albums' | 'tracks' | 'playlists' | 'tags' | 'radios' | 'podcasts' | 'series' | 'rss'
const type = useRouteQuery<QueryType>('type', 'artists') const type = useRouteQuery<QueryType>('type', 'artists')
@ -226,7 +228,6 @@ const radioConfig = computed(() => {
class="main" class="main"
> >
<section class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
/front/src/components/audio/Search.vue
<div <div
v-if="id" v-if="id"
class="ui small text container" class="ui small text container"

View File

@ -84,7 +84,7 @@ const purgeErroredFiles = () => purge('errored')
v-if="quotaStatus" v-if="quotaStatus"
class="label" class="label"
> >
{{ t('views.content.libraries.Quota.label.currentUsage', {max: humanSize(quotaStatus.max * 1000 * 1000), current: humanSize(quotaStatus.current * 1000 * 1000)}) }} {{ t('views.content.libraries.Quota.label.currentUsage', {max: humanSize(quotaStatus.max * 1000 * 1000), currentAmount: humanSize(quotaStatus.current * 1000 * 1000)}) }}
</div> </div>
</div> </div>
<div class="ui hidden divider" /> <div class="ui hidden divider" />

View File

@ -21,6 +21,8 @@ import Upload from '~/ui/pages/upload.vue'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
// TODO: Delete this file.
const { t } = useI18n() const { t } = useI18n()
const router = useRouter() const router = useRouter()
@ -103,6 +105,7 @@ const openModal = (object_: Library | Channel) => {
<template> <template>
<!-- TODO: Remove this module --> <!-- TODO: Remove this module -->
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
<section <section
v-title="labels.title" v-title="labels.title"
class="ui vertical aligned stripe segment" class="ui vertical aligned stripe segment"
@ -160,4 +163,5 @@ const openModal = (object_: Library | Channel) => {
<upload /> <upload />
</fw-modal> </fw-modal>
<!-- <channel-upload-modal v-if="store.state.auth.authenticated" /> --> <!-- <channel-upload-modal v-if="store.state.auth.authenticated" /> -->
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</template> </template>

View File

@ -162,15 +162,14 @@ const deletePlaylist = async () => {
flex flex
gap-8 gap-8
> >
<!-- TODO: Translations --> {{ t('views.playlists.Detail.meta.attribution') }}
by
<ActorLink <ActorLink
:actor="playlist.actor" :actor="playlist.actor"
:avatar="false" :avatar="false"
:discrete="true" :discrete="true"
/> />
<i class="bi bi-dot" /> <i class="bi bi-dot" />
updated {{ t('views.playlists.Detail.meta.updated') }}
<HumanDate <HumanDate
:date="playlist.modification_date" :date="playlist.modification_date"
/> />