feat: localizations

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-07 18:16:54 -04:00
parent f4fb8277d2
commit 0099a63fad
11 changed files with 353 additions and 94 deletions
+84
View File
@@ -0,0 +1,84 @@
{
"account": {
"address": "Adresse",
"addressPlaceholder": "Musterstraße 123",
"city": "Stadt",
"cityPlaceholder": "Berlin",
"countries": {
"australia": "Australien",
"brazil": "Brasilien",
"canada": "Kanada",
"china": "China",
"france": "Frankreich",
"germany": "Deutschland",
"india": "Indien",
"japan": "Japan",
"mexico": "Mexiko",
"southAfrica": "Südafrika",
"unitedKingdom": "Vereinigtes Königreich",
"unitedStates": "Vereinigte Staaten"
},
"country": "Land",
"email": "E-Mail",
"firstName": "Vorname",
"firstNamePlaceholder": "Max",
"lastName": "Nachname",
"lastNamePlaceholder": "Mustermann",
"managedFieldHint": "Wird vom Authentifizierungsanbieter verwaltet",
"phoneNumber": "Telefonnummer",
"photoRequirements": "Zulässig sind JPG, GIF oder PNG. Maximale Größe: 800 KB",
"selectCountry": "Land auswählen",
"stateOrProvince": "Bundesland/Provinz",
"statePlaceholder": "Bundesland",
"title": "Kontodetails",
"uploadPhoto": "Neues Foto hochladen",
"zipCode": "Postleitzahl"
},
"actions": {
"reset": "Zurücksetzen",
"saveChanges": "Änderungen speichern"
},
"nav": {
"preferences": "Einstellungen",
"profile": "Profil",
"profileSettings": "Profil & Einstellungen",
"security": "Sicherheit"
},
"notifications": {
"columns": {
"app": "App",
"browser": "BROWSER",
"email": "E-MAIL",
"type": "Typ"
},
"permissionDescription": "Wir benötigen die Berechtigung Ihres Browsers, um Benachrichtigungen anzuzeigen.",
"requestPermission": "Berechtigung anfordern",
"timing": {
"anytime": "Jederzeit",
"online": "Nur wenn ich online bin"
},
"timingQuestion": "Wann sollen wir Ihnen Benachrichtigungen senden?",
"title": "Benachrichtigungen",
"types": {
"accountActivity": "Kontoaktivität",
"newBrowserSignIn": "Anmeldung über einen neuen Browser",
"newDeviceLinked": "Ein neues Gerät wurde verknüpft",
"newForYou": "Neu für Sie"
}
},
"preferences": {
"languageHint": "Wird sofort angewendet und in Ihrem Konto gespeichert",
"languageLabel": "Anzeigesprache",
"languageTitle": "Sprache"
},
"security": {
"emptyDescription": "Derzeit sind keine Sicherheitsmodule aktiviert oder installiert.",
"emptyTitle": "Keine Sicherheitseinstellungen verfügbar"
},
"tabs": {
"account": "Konto",
"notifications": "Benachrichtigungen",
"preferences": "Einstellungen",
"security": "Sicherheit"
}
}
+84
View File
@@ -0,0 +1,84 @@
{
"account": {
"address": "Address",
"addressPlaceholder": "123 Main St",
"city": "City",
"cityPlaceholder": "New York",
"countries": {
"australia": "Australia",
"brazil": "Brazil",
"canada": "Canada",
"china": "China",
"france": "France",
"germany": "Germany",
"india": "India",
"japan": "Japan",
"mexico": "Mexico",
"southAfrica": "South Africa",
"unitedKingdom": "United Kingdom",
"unitedStates": "United States"
},
"country": "Country",
"email": "E-mail",
"firstName": "First Name",
"firstNamePlaceholder": "John",
"lastName": "Last Name",
"lastNamePlaceholder": "Doe",
"managedFieldHint": "Managed by authentication provider",
"phoneNumber": "Phone Number",
"photoRequirements": "Allowed JPG, GIF or PNG. Max size of 800K",
"selectCountry": "Select Country",
"stateOrProvince": "State/Province",
"statePlaceholder": "State",
"title": "Account Details",
"uploadPhoto": "Upload new photo",
"zipCode": "Zip Code"
},
"actions": {
"reset": "Reset",
"saveChanges": "Save changes"
},
"nav": {
"preferences": "Preferences",
"profile": "Profile",
"profileSettings": "Profile & Settings",
"security": "Security"
},
"notifications": {
"columns": {
"app": "App",
"browser": "BROWSER",
"email": "EMAIL",
"type": "Type"
},
"permissionDescription": "We need permission from your browser to show notifications.",
"requestPermission": "Request Permission",
"timing": {
"anytime": "Anytime",
"online": "Only when I'm online"
},
"timingQuestion": "When should we send you notifications?",
"title": "Notifications",
"types": {
"accountActivity": "Account activity",
"newBrowserSignIn": "A new browser used to sign in",
"newDeviceLinked": "A new device is linked",
"newForYou": "New for you"
}
},
"preferences": {
"languageHint": "Applies immediately and is saved to your account",
"languageLabel": "Display language",
"languageTitle": "Language"
},
"security": {
"emptyDescription": "No security modules are currently enabled or installed.",
"emptyTitle": "No Security Settings Available"
},
"tabs": {
"account": "Account",
"notifications": "Notifications",
"preferences": "Preferences",
"security": "Security"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"nav": {
"preferences": "Preferences",
"profile": "Profile",
"profileSettings": "Profile & Settings",
"security": "Security"
}
}
+41 -39
View File
@@ -2,8 +2,10 @@
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@KTXC/stores/userStore' import { useUserStore } from '@KTXC/stores/userStore'
import { userService } from '@KTXC/services/user/userService' import { userService } from '@KTXC/services/user/userService'
import { useL10n } from '@KTXC'
const userStore = useUserStore() const userStore = useUserStore()
const { t } = useL10n('user_profile')
const refInputEl = ref<HTMLElement>() const refInputEl = ref<HTMLElement>()
const isSaving = ref(false) const isSaving = ref(false)
@@ -134,27 +136,27 @@ const resetAvatar = () => {
} }
} }
const countries = [ const countries = computed(() => [
'United States', { value: 'United States', title: t('account.countries.unitedStates', 'United States') },
'United Kingdom', { value: 'United Kingdom', title: t('account.countries.unitedKingdom', 'United Kingdom') },
'Canada', { value: 'Canada', title: t('account.countries.canada', 'Canada') },
'Australia', { value: 'Australia', title: t('account.countries.australia', 'Australia') },
'Germany', { value: 'Germany', title: t('account.countries.germany', 'Germany') },
'France', { value: 'France', title: t('account.countries.france', 'France') },
'India', { value: 'India', title: t('account.countries.india', 'India') },
'China', { value: 'China', title: t('account.countries.china', 'China') },
'Japan', { value: 'Japan', title: t('account.countries.japan', 'Japan') },
'Brazil', { value: 'Brazil', title: t('account.countries.brazil', 'Brazil') },
'Mexico', { value: 'Mexico', title: t('account.countries.mexico', 'Mexico') },
'South Africa', { value: 'South Africa', title: t('account.countries.southAfrica', 'South Africa') },
] ])
</script> </script>
<template> <template>
<VRow> <VRow>
<VCol cols="12"> <VCol cols="12">
<VCard title="Account Details"> <VCard :title="t('account.title', 'Account Details')">
<VCardText class="d-flex"> <VCardText class="d-flex">
<!-- 👉 Avatar --> <!-- 👉 Avatar -->
<VAvatar <VAvatar
@@ -172,7 +174,7 @@ const countries = [
@click="refInputEl?.click()" @click="refInputEl?.click()"
> >
<VIcon icon="mdi-cloud-upload" class="d-sm-none" /> <VIcon icon="mdi-cloud-upload" class="d-sm-none" />
<span class="d-none d-sm-block">Upload new photo</span> <span class="d-none d-sm-block">{{ t('account.uploadPhoto', 'Upload new photo') }}</span>
</VBtn> </VBtn>
<input <input
@@ -190,13 +192,13 @@ const countries = [
variant="outlined" variant="outlined"
@click="resetAvatar" @click="resetAvatar"
> >
<span class="d-none d-sm-block">Reset</span> <span class="d-none d-sm-block">{{ t('actions.reset', 'Reset') }}</span>
<VIcon icon="mdi-refresh" class="d-sm-none" /> <VIcon icon="mdi-refresh" class="d-sm-none" />
</VBtn> </VBtn>
</div> </div>
<p class="text-body-1 mb-0"> <p class="text-body-1 mb-0">
Allowed JPG, GIF or PNG. Max size of 800K {{ t('account.photoRequirements', 'Allowed JPG, GIF or PNG. Max size of 800K') }}
</p> </p>
</form> </form>
</VCardText> </VCardText>
@@ -214,10 +216,10 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.name_given" v-model="accountDataLocal.name_given"
placeholder="John" :placeholder="t('account.firstNamePlaceholder', 'John')"
label="First Name" :label="t('account.firstName', 'First Name')"
:readonly="!isFieldEditable('name_given')" :readonly="!isFieldEditable('name_given')"
:hint="!isFieldEditable('name_given') ? 'Managed by authentication provider' : ''" :hint="!isFieldEditable('name_given') ? t('account.managedFieldHint', 'Managed by authentication provider') : ''"
persistent-hint persistent-hint
/> />
</VCol> </VCol>
@@ -229,10 +231,10 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.name_family" v-model="accountDataLocal.name_family"
placeholder="Doe" :placeholder="t('account.lastNamePlaceholder', 'Doe')"
label="Last Name" :label="t('account.lastName', 'Last Name')"
:readonly="!isFieldEditable('name_family')" :readonly="!isFieldEditable('name_family')"
:hint="!isFieldEditable('name_family') ? 'Managed by authentication provider' : ''" :hint="!isFieldEditable('name_family') ? t('account.managedFieldHint', 'Managed by authentication provider') : ''"
persistent-hint persistent-hint
/> />
</VCol> </VCol>
@@ -244,11 +246,11 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.email" v-model="accountDataLocal.email"
label="E-mail" :label="t('account.email', 'E-mail')"
placeholder="john.doe@example.com" placeholder="john.doe@example.com"
type="email" type="email"
:readonly="!isFieldEditable('email')" :readonly="!isFieldEditable('email')"
:hint="!isFieldEditable('email') ? 'Managed by authentication provider' : ''" :hint="!isFieldEditable('email') ? t('account.managedFieldHint', 'Managed by authentication provider') : ''"
persistent-hint persistent-hint
/> />
</VCol> </VCol>
@@ -260,10 +262,10 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.phone" v-model="accountDataLocal.phone"
label="Phone Number" :label="t('account.phoneNumber', 'Phone Number')"
placeholder="+1 (555) 123-4567" placeholder="+1 (555) 123-4567"
:readonly="!isFieldEditable('phone')" :readonly="!isFieldEditable('phone')"
:hint="!isFieldEditable('phone') ? 'Managed by authentication provider' : ''" :hint="!isFieldEditable('phone') ? t('account.managedFieldHint', 'Managed by authentication provider') : ''"
persistent-hint persistent-hint
/> />
</VCol> </VCol>
@@ -275,8 +277,8 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.address" v-model="accountDataLocal.address"
label="Address" :label="t('account.address', 'Address')"
placeholder="123 Main St" :placeholder="t('account.addressPlaceholder', '123 Main St')"
/> />
</VCol> </VCol>
@@ -287,8 +289,8 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.city" v-model="accountDataLocal.city"
label="City" :label="t('account.city', 'City')"
placeholder="New York" :placeholder="t('account.cityPlaceholder', 'New York')"
/> />
</VCol> </VCol>
@@ -299,8 +301,8 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.state" v-model="accountDataLocal.state"
label="State/Province" :label="t('account.stateOrProvince', 'State/Province')"
placeholder="State" :placeholder="t('account.statePlaceholder', 'State')"
/> />
</VCol> </VCol>
@@ -311,7 +313,7 @@ const countries = [
> >
<VTextField <VTextField
v-model="accountDataLocal.zip" v-model="accountDataLocal.zip"
label="Zip Code" :label="t('account.zipCode', 'Zip Code')"
placeholder="10001" placeholder="10001"
/> />
</VCol> </VCol>
@@ -323,9 +325,9 @@ const countries = [
> >
<VSelect <VSelect
v-model="accountDataLocal.country" v-model="accountDataLocal.country"
label="Country" :label="t('account.country', 'Country')"
:items="countries" :items="countries"
placeholder="Select Country" :placeholder="t('account.selectCountry', 'Select Country')"
/> />
</VCol> </VCol>
@@ -339,7 +341,7 @@ const countries = [
:loading="isSaving" :loading="isSaving"
color="primary" color="primary"
> >
Save changes {{ t('actions.saveChanges', 'Save changes') }}
</VBtn> </VBtn>
<VBtn <VBtn
@@ -348,7 +350,7 @@ const countries = [
type="reset" type="reset"
@click.prevent="resetForm" @click.prevent="resetForm"
> >
Reset {{ t('actions.reset', 'Reset') }}
</VBtn> </VBtn>
</VCol> </VCol>
</VRow> </VRow>
+36 -43
View File
@@ -1,69 +1,62 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { computed, ref } from 'vue'
import { useL10n } from '@KTXC'
const recentDevices = ref( const { t } = useL10n('user_profile')
[
{
type: 'New for you',
email: true,
browser: true,
app: true,
},
{
type: 'Account activity',
email: true,
browser: true,
app: true,
},
{
type: 'A new browser used to sign in',
email: true,
browser: true,
app: false,
},
{
type: 'A new device is linked',
email: true,
browser: false,
app: false,
},
],
)
const selectedNotification = ref('Only when I\'m online') const recentDevices = ref([
{ id: 'newForYou', email: true, browser: true, app: true },
{ id: 'accountActivity', email: true, browser: true, app: true },
{ id: 'newBrowserSignIn', email: true, browser: true, app: false },
{ id: 'newDeviceLinked', email: true, browser: false, app: false },
])
const notificationTypeLabels = computed<Record<string, string>>(() => ({
newForYou: t('notifications.types.newForYou', 'New for you'),
accountActivity: t('notifications.types.accountActivity', 'Account activity'),
newBrowserSignIn: t('notifications.types.newBrowserSignIn', 'A new browser used to sign in'),
newDeviceLinked: t('notifications.types.newDeviceLinked', 'A new device is linked'),
}))
const notificationTimingOptions = computed(() => [
{ value: 'online', title: t('notifications.timing.online', 'Only when I\'m online') },
{ value: 'anytime', title: t('notifications.timing.anytime', 'Anytime') },
])
const selectedNotification = ref('online')
</script> </script>
<template> <template>
<VCard title="Notifications"> <VCard :title="t('notifications.title', 'Notifications')">
<VCardText> <VCardText>
We need permission from your browser to show notifications. {{ t('notifications.permissionDescription', 'We need permission from your browser to show notifications.') }}
<a href="javascript:void(0)">Request Permission</a> <a href="javascript:void(0)">{{ t('notifications.requestPermission', 'Request Permission') }}</a>
</VCardText> </VCardText>
<VTable class="text-no-wrap"> <VTable class="text-no-wrap">
<thead> <thead>
<tr> <tr>
<th scope="col"> <th scope="col">
Type {{ t('notifications.columns.type', 'Type') }}
</th> </th>
<th scope="col"> <th scope="col">
EMAIL {{ t('notifications.columns.email', 'EMAIL') }}
</th> </th>
<th scope="col"> <th scope="col">
BROWSER {{ t('notifications.columns.browser', 'BROWSER') }}
</th> </th>
<th scope="col"> <th scope="col">
App {{ t('notifications.columns.app', 'App') }}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr <tr
v-for="device in recentDevices" v-for="device in recentDevices"
:key="device.type" :key="device.id"
> >
<td> <td>
{{ device.type }} {{ notificationTypeLabels[device.id] }}
</td> </td>
<td> <td>
<VCheckbox v-model="device.email" /> <VCheckbox v-model="device.email" />
@@ -82,7 +75,7 @@ const selectedNotification = ref('Only when I\'m online')
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<p class="text-base font-weight-medium"> <p class="text-base font-weight-medium">
When should we send you notifications? {{ t('notifications.timingQuestion', 'When should we send you notifications?') }}
</p> </p>
<VRow> <VRow>
@@ -93,21 +86,21 @@ const selectedNotification = ref('Only when I\'m online')
<VSelect <VSelect
v-model="selectedNotification" v-model="selectedNotification"
mandatory mandatory
:items="['Only when I\'m online', 'Anytime']" :items="notificationTimingOptions"
/> />
</VCol> </VCol>
</VRow> </VRow>
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit"> <VBtn type="submit">
Save Changes {{ t('actions.saveChanges', 'Save changes') }}
</VBtn> </VBtn>
<VBtn <VBtn
color="secondary" color="secondary"
variant="outlined" variant="outlined"
type="reset" type="reset"
> >
Reset {{ t('actions.reset', 'Reset') }}
</VBtn> </VBtn>
</div> </div>
</VForm> </VForm>
+45
View File
@@ -0,0 +1,45 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { useL10nStore, useL10n } from '@KTXC'
const l10nStore = useL10nStore()
const { t } = useL10n('user_profile')
// Offer each available locale under its own native name
const languageOptions = computed(() =>
l10nStore.available.map((locale: string) => {
let title = locale
try {
title = new Intl.DisplayNames([locale], { type: 'language' }).of(locale) ?? locale
} catch {
// unknown tag: fall back to the raw locale code
}
return { value: locale, title }
})
)
const selectLanguage = (locale: string | null) => {
if (locale && locale !== l10nStore.locale) {
l10nStore.setLocale(locale)
}
}
</script>
<template>
<VRow>
<VCol cols="12" md="6">
<VCard :title="t('preferences.languageTitle', 'Language')">
<VCardText>
<VSelect
:model-value="l10nStore.locale"
:items="languageOptions"
:label="t('preferences.languageLabel', 'Display language')"
:hint="t('preferences.languageHint', 'Applies immediately and is saved to your account')"
persistent-hint
@update:model-value="selectLanguage"
/>
</VCardText>
</VCard>
</VCol>
</VRow>
</template>
+6 -2
View File
@@ -1,8 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useIntegrationStore } from '@KTXC/stores/integrationStore' import { useIntegrationStore } from '@KTXC/stores/integrationStore'
import { useL10n } from '@KTXC'
const integrationStore = useIntegrationStore() const integrationStore = useIntegrationStore()
const { t } = useL10n('user_profile')
// Get all registered security panel components from modules // Get all registered security panel components from modules
const securityPanels = computed(() => { const securityPanels = computed(() => {
@@ -59,9 +61,11 @@ onMounted(async () => {
class="mb-4" class="mb-4"
color="grey" color="grey"
/> />
<p class="text-h6 mb-2">No Security Settings Available</p> <p class="text-h6 mb-2">
{{ t('security.emptyTitle', 'No Security Settings Available') }}
</p>
<p class="text-body-2 text-medium-emphasis"> <p class="text-body-2 text-medium-emphasis">
No security modules are currently enabled or installed. {{ t('security.emptyDescription', 'No security modules are currently enabled or installed.') }}
</p> </p>
</VCardText> </VCardText>
</VCard> </VCard>
+5 -1
View File
@@ -5,17 +5,19 @@ const integrations: ModuleIntegrations = {
{ {
id: 'user_profile', id: 'user_profile',
label: 'Profile & Settings', label: 'Profile & Settings',
l10n: 'nav.profileSettings',
path: '/me', path: '/me',
icon: 'mdi-account-outline', icon: 'mdi-account-outline',
priority: 90, priority: 90,
}, },
], ],
// User personal settings // User personal settings
user_settings_menu: [ user_settings_menu: [
{ {
id: 'profile', id: 'profile',
label: 'Profile', label: 'Profile',
l10n: 'nav.profile',
path: '/me/profile', path: '/me/profile',
icon: 'mdi-account-circle', icon: 'mdi-account-circle',
priority: 10, priority: 10,
@@ -23,6 +25,7 @@ const integrations: ModuleIntegrations = {
{ {
id: 'preferences', id: 'preferences',
label: 'Preferences', label: 'Preferences',
l10n: 'nav.preferences',
path: '/me/preferences', path: '/me/preferences',
icon: 'mdi-tune', icon: 'mdi-tune',
priority: 20, priority: 20,
@@ -30,6 +33,7 @@ const integrations: ModuleIntegrations = {
{ {
id: 'security', id: 'security',
label: 'Security', label: 'Security',
l10n: 'nav.security',
path: '/me/security', path: '/me/security',
icon: 'mdi-shield-lock', icon: 'mdi-shield-lock',
priority: 30, priority: 30,
+3
View File
@@ -1,4 +1,7 @@
import routes from '@/routes' import routes from '@/routes'
import integrations from '@/integrations' import integrations from '@/integrations'
// Module ships translation catalogs at static/l10n/{locale}.json
export const l10n = true
export { routes, integrations } export { routes, integrations }
+15 -6
View File
@@ -1,20 +1,24 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { computed, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import ProfileAccount from '@/components/ProfileAccount.vue' import ProfileAccount from '@/components/ProfileAccount.vue'
import ProfileNotifications from '@/components/ProfileNotifications.vue' import ProfileNotifications from '@/components/ProfileNotifications.vue'
import ProfilePreferences from '@/components/ProfilePreferences.vue'
import ProfileSecurity from '@/components/ProfileSecurity.vue' import ProfileSecurity from '@/components/ProfileSecurity.vue'
import { useL10n } from '@KTXC'
const route = useRoute() const route = useRoute()
const { t } = useL10n('user_profile')
const activeTab = ref(route.params.tab || 'account') const activeTab = ref(route.params.tab || 'account')
// tabs // tabs
const tabs = [ const tabs = computed(() => [
{ title: 'Account', icon: 'mdi-account-outline', tab: 'account' }, { title: t('tabs.account', 'Account'), icon: 'mdi-account-outline', tab: 'account' },
{ title: 'Security', icon: 'mdi-lock-outline', tab: 'security' }, { title: t('tabs.security', 'Security'), icon: 'mdi-lock-outline', tab: 'security' },
{ title: 'Notifications', icon: 'mdi-bell-outline', tab: 'notifications' }, { title: t('tabs.preferences', 'Preferences'), icon: 'mdi-tune', tab: 'preferences' },
] { title: t('tabs.notifications', 'Notifications'), icon: 'mdi-bell-outline', tab: 'notifications' },
])
</script> </script>
<template> <template>
@@ -61,6 +65,11 @@ const tabs = [
<VWindowItem value="notifications"> <VWindowItem value="notifications">
<ProfileNotifications /> <ProfileNotifications />
</VWindowItem> </VWindowItem>
<!-- Preferences -->
<VWindowItem value="preferences">
<ProfilePreferences />
</VWindowItem>
</VWindow> </VWindow>
</PerfectScrollbar> </PerfectScrollbar>
</template> </template>
+26 -3
View File
@@ -1,10 +1,30 @@
import { defineConfig } from 'vite' import { defineConfig, type PluginOption } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import path from 'path' import path from 'path'
import fs from 'fs'
// Copy translation catalogs (l10n/*.json, except the extractor-only
// en.manual.json) into static/l10n/ so the core shell can fetch them at
// /modules/{handle}/static/l10n/{locale}.json
const copyL10nCatalogs = (): PluginOption => ({
name: 'copy-l10n-catalogs',
apply: 'build',
closeBundle() {
const sourceDir = path.resolve(__dirname, 'l10n')
const outDir = path.resolve(__dirname, 'static/l10n')
if (!fs.existsSync(sourceDir)) return
fs.mkdirSync(outDir, { recursive: true })
for (const file of fs.readdirSync(sourceDir)) {
if (file.endsWith('.json') && !file.endsWith('.manual.json')) {
fs.copyFileSync(path.join(sourceDir, file), path.join(outDir, file))
}
}
},
})
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue(), copyL10nCatalogs()],
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),
@@ -20,7 +40,10 @@ export default defineConfig({
fileName: () => 'module.mjs', fileName: () => 'module.mjs',
}, },
rollupOptions: { rollupOptions: {
external: ['vue', 'vue-router', 'pinia'], external: ['vue', 'vue-router', 'pinia', '@KTXC'],
output: {
paths: (id) => (id === '@KTXC' ? '/js/ktxc.mjs' : id),
},
}, },
}, },
}) })