i18n support

This commit is contained in:
Gauthier Roebroeck 2025-05-28 17:19:19 +08:00
parent 45cbd5d8e4
commit 27336233cb
13 changed files with 351 additions and 44 deletions

1
next-ui/.gitignore vendored
View file

@ -22,4 +22,5 @@ pnpm-debug.log*
*.sw?
# FormatJS compiled translation files
!/src/i18n/README.md
src/i18n

View file

@ -46,6 +46,7 @@ export default defineConfigWithVueTs(
'error',
{
idInterpolationPattern: '[sha512:contenthash:base64:6]',
idWhitelist: ['app.*']
},
],
},

View file

@ -1,12 +1,56 @@
{
"/bathK": {
"defaultMessage": "Allow only under",
"description": "User creation/edit dialog: Age restriction field possible option"
},
"02SRax": {
"defaultMessage": "Sign in",
"description": "Login screen: Sign In button"
},
"0YG9GQ": {
"defaultMessage": "Remember Me",
"description": "Login screen: Remember Me checkbox"
},
"2Bh8F2": {
"defaultMessage": "Latest",
"description": "Updates view: badge showing next to the latest release number"
},
"2g7iOx": {
"defaultMessage": "Metrics",
"description": "Drawer menu for Server > Metrics"
},
"3W0jUi": {
"defaultMessage": "Exclude labels",
"description": "User creation/edit dialog: Exclude labels field label"
},
"3jrAF6": {
"defaultMessage": "Currently installed",
"description": "Updates view: badge showing next to the currently installed release number"
},
"3rSWpQ": {
"defaultMessage": "Help translate",
"description": "Translations pop-up menu footer"
},
"5AAGkA": {
"defaultMessage": "Password",
"description": "Login screen: password field label"
},
"AeA9Ka": {
"defaultMessage": "No restriction",
"description": "User creation/edit dialog: Age restriction field possible option"
},
"CUxhzL": {
"defaultMessage": "Roles",
"description": "User creation/edit dialog: Roles field"
},
"DxtDpt": {
"defaultMessage": "Media Analysis",
"description": "Drawer menu for Media > Media Analysis"
},
"G/T8/2": {
"defaultMessage": "Cancel",
"description": "ConfirmEdit dialog: Cancel button"
},
"G7quju": {
"defaultMessage": "Announcements",
"description": "Drawer menu for Server > Announcements"
@ -19,6 +63,10 @@
"defaultMessage": "Media",
"description": "Drawer menu for Media"
},
"InW6ko": {
"defaultMessage": "Translations",
"description": "Translations pop-up menu header"
},
"IpvWiZ": {
"defaultMessage": "Server",
"description": "Drawer menu for Server"
@ -27,6 +75,10 @@
"defaultMessage": "Users",
"description": "Drawer menu for Server > Users"
},
"LaxrEO": {
"defaultMessage": "Passwords must be identical",
"description": "User password change dialog: Error message if passwords differ"
},
"MvwDsn": {
"defaultMessage": "Known",
"description": "Drawer menu for Media > Duplicate Pages > Known"
@ -35,10 +87,34 @@
"defaultMessage": "Import",
"description": "Drawer menu for Import"
},
"N9WFH4": {
"defaultMessage": "Save",
"description": "ConfirmEdit dialog: Save button"
},
"Nb0V0p": {
"defaultMessage": "Missing Posters",
"description": "Drawer menu for Media > Missing Posters"
},
"QIr0z7": {
"defaultMessage": "Email",
"description": "Login screen: email field label"
},
"Sj0HXz": {
"defaultMessage": "Allow only labels",
"description": "User creation/edit dialog: Allow only labels field label"
},
"UvhIIT": {
"defaultMessage": "Shared Libraries",
"description": "User creation/edit dialog: Shared Libraries field"
},
"WNY0pu": {
"defaultMessage": "The latest version of Komga is already installed",
"description": "Updates view: banner shown at the top"
},
"WhasCZ": {
"defaultMessage": "New password",
"description": "User password change dialog: New Password field label"
},
"Y6VlM9": {
"defaultMessage": "Read List",
"description": "Drawer menu for Import > Read List"
@ -47,6 +123,26 @@
"defaultMessage": "User Interface",
"description": "Drawer menu for Server > User Interface"
},
"app.locale-name": {
"defaultMessage": "English",
"description": "The name of the locale, shown in the language selection menu. Must be translated to the language's name"
},
"app.user-create-dialog..select_create_one": {
"defaultMessage": "Select an item or create one",
"description": "User creation/edit dialog: Allow only labels field selection"
},
"app.user-create-dialog.all_libraries": {
"defaultMessage": "All libraries",
"description": "User creation/edit dialog: Shared Libraries field, value shown when user has access to all libraries"
},
"app.user-create-dialog.select_create_one": {
"defaultMessage": "Select an item or create one",
"description": "User creation/edit dialog: Exclude labels field selection"
},
"b5wVJa": {
"defaultMessage": "Email",
"description": "User creationd ialog: Email field"
},
"cAu/I6": {
"defaultMessage": "Duplicate Pages",
"description": "Drawer menu for Media > Duplicate Pages"
@ -55,6 +151,10 @@
"defaultMessage": "Activity",
"description": "Drawer menu for My Account > Activity"
},
"eVoe+D": {
"defaultMessage": "Please type {validateText} to confirm.",
"description": "Confirmation dialog: default hint to retype validation text"
},
"eW3fXu": {
"defaultMessage": "Duplicate Files",
"description": "Drawer menu for Media > Duplicate Files"
@ -63,6 +163,14 @@
"defaultMessage": "Books",
"description": "Drawer menu for Import > Books"
},
"hEOGa9": {
"defaultMessage": "Age restriction",
"description": "User creation/edit dialog: Age restriction field label"
},
"jywpqq": {
"defaultMessage": "Age",
"description": "User creation/edit dialog: Age Restriction > Age field label"
},
"l/To3S": {
"defaultMessage": "History",
"description": "Drawer menu for History"
@ -71,9 +179,17 @@
"defaultMessage": "Updates",
"description": "Drawer menu for Server > Updates"
},
"localename": {
"defaultMessage": "English",
"description": "The name of the locale, shown in the language selection menu. Must be translated to the language's name"
"n1Ik+L": {
"defaultMessage": "Updates are available",
"description": "Updates view: banner shown at the top"
},
"nJiYF7": {
"defaultMessage": "Confirm password",
"description": "User password change dialog: Confirm Password field label"
},
"o+A10T": {
"defaultMessage": "Password",
"description": "User creation dialog: Password field"
},
"oFOkWZ": {
"defaultMessage": "API Keys",
@ -83,6 +199,10 @@
"defaultMessage": "My Account",
"description": "Drawer menu for My Account"
},
"pENCUD": {
"defaultMessage": "Cancel",
"description": "Confirmation dialog: Cancel button"
},
"qiZm6U": {
"defaultMessage": "Unknown",
"description": "Drawer menu for Media > Duplicate Pages > Unknown"
@ -91,10 +211,22 @@
"defaultMessage": "User Interface",
"description": "Drawer menu for My Account > User Interface"
},
"sUSVQS": {
"defaultMessage": "Mark as read",
"description": "Announcements view: mark as read button tooltip"
},
"t8vOuG": {
"defaultMessage": "Confirm",
"description": "Confirmation dialog: OK button default text"
},
"ti4Pzo": {
"defaultMessage": "Logout",
"description": "Drawer menu for Logout"
},
"wmGcF+": {
"defaultMessage": "Exclude over",
"description": "User creation/edit dialog: Age restriction field possible option"
},
"xYGXuU": {
"defaultMessage": "Details",
"description": "Drawer menu for My Account > Details"

View file

@ -12,7 +12,11 @@
color="primary"
>
<v-list-subheader
title="Translations"
:title="$formatMessage({
description: 'Translations pop-up menu header',
defaultMessage: 'Translations',
id: 'InW6ko'
})"
class="text-high-emphasis text-uppercase font-weight-black"
/>
@ -29,7 +33,13 @@
target="_blank"
>
<v-list-item-title class="font-weight-bold">
Help translate
{{
$formatMessage({
description: 'Translations pop-up menu footer',
defaultMessage: 'Help translate',
id: '3rSWpQ'
})
}}
</v-list-item-title>
</v-list-item>
</v-list>

View file

@ -10,7 +10,11 @@
<v-col>
<v-text-field
v-model="username"
label="Email"
:label="$formatMessage({
description: 'Login screen: email field label',
defaultMessage: 'Email',
id: 'QIr0z7'
})"
autofocus
/>
</v-col>
@ -20,7 +24,11 @@
<v-col>
<v-text-field
v-model="password"
label="Password"
:label="$formatMessage({
description: 'Login screen: password field label',
defaultMessage: 'Password',
id: '5AAGkA'
})"
type="password"
/>
</v-col>
@ -30,7 +38,11 @@
<v-col>
<v-checkbox
v-model="rememberMe"
label="Remember me"
:label="$formatMessage({
description: 'Login screen: Remember Me checkbox',
defaultMessage: 'Remember Me',
id: '0YG9GQ'
})"
/>
</v-col>
</v-row>
@ -38,7 +50,11 @@
<v-row>
<v-col>
<v-btn
text="Sign in"
:text="$formatMessage({
description: 'Login screen: Sign In button',
defaultMessage: 'Sign in',
id: '02SRax'
})"
@click="performLogin"
/>
</v-col>

View file

@ -15,7 +15,17 @@
<template #text>
<slot name="warning" />
<slot name="text">
Please type <span class="font-weight-bold">{{ validateText }}</span> to confirm.
{{
$formatMessage(
{
description: 'Confirmation dialog: default hint to retype validation text',
defaultMessage: 'Please type {validateText} to confirm.',
id: 'eVoe+D'
},
{
validateText: validateText,
})
}}
</slot>
<v-text-field
@ -28,7 +38,11 @@
<template #actions>
<v-spacer />
<v-btn
text="Cancel"
:text="$formatMessage({
description: 'Confirmation dialog: Cancel button',
defaultMessage: 'Cancel',
id: 'pENCUD'
})"
@click="close()"
/>
<v-btn
@ -47,6 +61,7 @@
<script setup lang="ts">
import {useRules} from 'vuetify/labs/rules'
import {useIntl} from 'vue-intl'
const showDialog = defineModel<boolean>('dialog', {required: false})
const emit = defineEmits<{
@ -56,6 +71,7 @@ const emit = defineEmits<{
const formValid = ref<boolean>(false)
const rules = useRules()
const intl = useIntl()
function submitForm() {
if(formValid.value) {
@ -76,7 +92,11 @@ export interface Props {
const {
title = undefined,
subtitle = undefined,
okText = 'Confirm',
okText = intl.formatMessage({
description: 'Confirmation dialog: OK button default text',
defaultMessage: 'Confirm',
id: 't8vOuG'
}),
validateText = 'confirm',
maxWidth = undefined,
activator = undefined,

View file

@ -31,11 +31,19 @@
<template #actions>
<v-spacer />
<v-btn
text="Cancel"
:text="$formatMessage({
description: 'ConfirmEdit dialog: Cancel button',
defaultMessage: 'Cancel',
id: 'G/T8/2'
})"
@click="close()"
/>
<v-btn
text="Save"
:text="$formatMessage({
description: 'ConfirmEdit dialog: Save button',
defaultMessage: 'Save',
id: 'N9WFH4'
})"
type="submit"
/>
</template>
@ -47,7 +55,6 @@
</template>
<script setup lang="ts">
/** Dialog super component de la race*/
const showDialog = defineModel<boolean>('dialog', {required: false})
const record = defineModel<unknown>('record', {required: true})

View file

@ -2,7 +2,11 @@
<v-text-field
v-model="newPassword"
:rules="[rules.required()]"
label="New password"
:label="$formatMessage({
description: 'User password change dialog: New Password field label',
defaultMessage: 'New password',
id: 'WhasCZ'
})"
autocomplete="off"
autofocus
:type="showPassword ? 'text' : 'password'"
@ -12,8 +16,16 @@
<v-text-field
v-model="confirmPassword"
class="mt-2"
:rules="[rules.sameAs(newPassword, 'Passwords must be identical')]"
label="Confirm password"
:rules="[rules.sameAs(newPassword, $formatMessage({
description: 'User password change dialog: Error message if passwords differ',
defaultMessage: 'Passwords must be identical',
id: 'LaxrEO'
}))]"
:label="$formatMessage({
description: 'User password change dialog: Confirm Password field label',
defaultMessage: 'Confirm password',
id: 'nJiYF7'
})"
autocomplete="off"
:type="showPassword ? 'text' : 'password'"
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"

View file

@ -4,14 +4,22 @@
v-model="user!.email"
autofocus
:rules="[rules.required(), rules.email()]"
label="Email"
:label="$formatMessage({
description: 'User creationd ialog: Email field',
defaultMessage: 'Email',
id: 'b5wVJa'
})"
prepend-icon="mdi-account"
/>
<v-text-field
v-model="user.password"
class="mt-1"
:rules="[rules.required()]"
label="Password"
:label="$formatMessage({
description: 'User creation dialog: Password field',
defaultMessage: 'Password',
id: 'o+A10T'
})"
autocomplete="off"
:type="showPassword ? 'text' : 'password'"
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
@ -26,7 +34,11 @@
chips
closable-chips
multiple
label="Roles"
:label="$formatMessage({
description: 'User creation/edit dialog: Roles field',
defaultMessage: 'Roles',
id: 'CUxhzL'
})"
prepend-icon="mdi-key-chain"
:items="userRoles"
/>
@ -35,7 +47,11 @@
<v-select
v-model="user.sharedLibraries!.libraryIds"
multiple
label="Shared Libraries"
:label="$formatMessage({
description: 'User creation/edit dialog: Shared Libraries field',
defaultMessage: 'Shared Libraries',
id: 'UvhIIT'
})"
:items="libraries"
item-title="name"
item-value="id"
@ -46,7 +62,11 @@
<!-- Show an All Libraries chip instead of the selection -->
<v-chip
v-if="user.sharedLibraries?.all"
text="All libraries"
:text="$formatMessage({
description: 'User creation/edit dialog: Shared Libraries field, value shown when user has access to all libraries',
defaultMessage: 'All libraries',
id: 'app.user-create-dialog.all_libraries'
})"
size="small"
/>
</template>
@ -62,7 +82,11 @@
<template #prepend-item>
<v-list-item
title="All libraries"
:title="$formatMessage({
description: 'User creation/edit dialog: Shared Libraries field, value shown when user has access to all libraries',
defaultMessage: 'All libraries',
id: 'app.user-create-dialog.all_libraries'
})"
@click="selectAllLibraries"
>
<template #prepend>
@ -88,7 +112,11 @@
<v-col>
<v-select
v-model="user.ageRestriction!.restriction"
label="Age restriction"
:label="$formatMessage({
description: 'User creation/edit dialog: Age restriction field label',
defaultMessage: 'Age restriction',
id: 'hEOGa9'
})"
:items="ageRestrictions"
prepend-icon="mdi-folder-lock"
/>
@ -97,7 +125,11 @@
<v-number-input
v-model="user.ageRestriction!.age"
:disabled="user.ageRestriction?.restriction?.toString() === 'NONE'"
label="Age"
:label="$formatMessage({
description: 'User creation/edit dialog: Age Restriction > Age field label',
defaultMessage: 'Age',
id: 'jywpqq'
})"
:min="0"
:rules="[rules.required()]"
/>
@ -107,7 +139,11 @@
<!-- Allow labels -->
<v-combobox
v-model="user.labelsAllow"
label="Allow only labels"
:label="$formatMessage({
description: 'User creation/edit dialog: Allow only labels field label',
defaultMessage: 'Allow only labels',
id: 'Sj0HXz'
})"
chips
closable-chips
multiple
@ -116,7 +152,15 @@
>
<template #prepend-item>
<v-list-item>
<span class="font-weight-medium">Select an item or create one</span>
<span class="font-weight-medium">
{{
$formatMessage({
description: 'User creation/edit dialog: Allow only labels field selection',
defaultMessage: 'Select an item or create one',
id: 'app.user-create-dialog..select_create_one'
})
}}
</span>
</v-list-item>
</template>
</v-combobox>
@ -124,7 +168,11 @@
<!-- Exclude labels -->
<v-combobox
v-model="user.labelsExclude"
label="Exclude labels"
:label="$formatMessage({
description: 'User creation/edit dialog: Exclude labels field label',
defaultMessage: 'Exclude labels',
id: '3W0jUi'
})"
chips
closable-chips
multiple
@ -133,7 +181,15 @@
>
<template #prepend-item>
<v-list-item>
<span class="font-weight-medium">Select an item or create one</span>
<span class="font-weight-medium">
{{
$formatMessage({
description: 'User creation/edit dialog: Exclude labels field selection',
defaultMessage: 'Select an item or create one',
id: 'app.user-create-dialog.select_create_one'
})
}}
</span>
</v-list-item>
</template>
</v-combobox>
@ -142,20 +198,23 @@
<script setup lang="ts">
import {UserRoles} from '@/types/UserRoles.ts'
import type {components} from '@/generated/openapi/komga'
import { useRules } from 'vuetify/labs/rules'
import {useRules} from 'vuetify/labs/rules'
import {useLibraries} from '@/colada/queries/libraries.ts'
import {useSharingLabels} from '@/colada/queries/referential.ts'
import {useIntl} from 'vue-intl'
const rules = useRules()
const intl = useIntl()
interface UserExtend {
id?: string,
email: string,
password?: string,
}
type UserCreation = components["schemas"]["UserCreationDto"] & UserExtend
type UserUpdate = components["schemas"]["UserUpdateDto"] & UserExtend
const user = defineModel<UserCreation | UserUpdate>({required: true})
type UserCreation = components['schemas']['UserCreationDto'] & UserExtend
type UserUpdate = components['schemas']['UserUpdateDto'] & UserExtend
const user = defineModel<UserCreation | UserUpdate>({required: true})
const showPassword = ref<boolean>(false)
@ -173,8 +232,29 @@ const userRoles = computed(() => Object.keys(UserRoles).map(x => ({
})))
const ageRestrictions = [
{title: 'No restriction', value: 'NONE'},
{title: 'Allow only under', value: 'ALLOW_ONLY'},
{title: 'Exclude over', value: 'EXCLUDE'},
{
title: intl.formatMessage({
description: 'User creation/edit dialog: Age restriction field possible option',
defaultMessage: 'No restriction',
id: 'AeA9Ka'
}),
value: 'NONE'
},
{
title: intl.formatMessage({
description: 'User creation/edit dialog: Age restriction field possible option',
defaultMessage: 'Allow only under',
id: '/bathK'
}),
value: 'ALLOW_ONLY'
},
{
title: intl.formatMessage({
description: 'User creation/edit dialog: Age restriction field possible option',
defaultMessage: 'Exclude over',
id: 'wmGcF+'
}),
value: 'EXCLUDE'
},
]
</script>

View file

@ -0,0 +1 @@
This folder contains the compiled translation files, using `npm run i18n-compile`.

View file

@ -30,7 +30,11 @@
</v-col>
<v-col cols="auto">
<v-tooltip
text="Mark as read"
:text="$formatMessage({
description: 'Announcements view: mark as read button tooltip',
defaultMessage: 'Mark as read',
id: 'sUSVQS'
})"
:disabled="item._komga?.read"
>
<template #activator="{ props }">

View file

@ -15,7 +15,13 @@
type="success"
variant="tonal"
>
The latest version of Komga is already installed
{{
$formatMessage({
description: 'Updates view: banner shown at the top',
defaultMessage: 'The latest version of Komga is already installed',
id: 'WNY0pu'
})
}}
</v-alert>
</div>
<div v-if="isLatestVersion == false">
@ -23,7 +29,13 @@
type="warning"
variant="tonal"
>
Updates are available
{{
$formatMessage({
description: 'Updates view: banner shown at the top',
defaultMessage: 'Updates are available',
id: 'n1Ik+L'
})
}}
</v-alert>
</div>
</v-col>
@ -53,7 +65,13 @@
rounded
color="info"
>
Currently installed
{{
$formatMessage({
description: 'Updates view: badge showing next to the currently installed release number',
defaultMessage: 'Currently installed',
id: '3jrAF6'
})
}}
</v-chip>
<v-chip
v-if="release.version == latest?.version"
@ -61,7 +79,13 @@
size="small"
rounded
>
Latest
{{
$formatMessage({
description: 'Updates view: badge showing next to the latest release number',
defaultMessage: 'Latest',
id: '2Bh8F2'
})
}}
</v-chip>
</div>
<!-- TODO: i18n the date -->

View file

@ -2,11 +2,10 @@ import {defineMessage} from 'vue-intl'
export const defaultLocale = 'en'
// eslint-disable-next-line
const localeName = defineMessage({
description: 'The name of the locale, shown in the language selection menu. Must be translated to the language\'s name',
defaultMessage: 'English',
id: 'localename'
id: 'app.locale-name'
})
/**