refactor: cleanup translations
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
/** Locale used before runtime localization configuration is available. */
|
||||||
|
export const DEFAULT_LOCALE = 'en';
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createI18n } from 'vue-i18n';
|
import { createI18n } from 'vue-i18n';
|
||||||
import { en as vuetifyEn, de as vuetifyDe } from 'vuetify/locale';
|
import { en as vuetifyEn, de as vuetifyDe } from 'vuetify/locale';
|
||||||
|
import { DEFAULT_LOCALE } from '@KTXC/l10n/config';
|
||||||
export const FALLBACK_LOCALE = 'en';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The single vue-i18n instance shared by the core shell and all modules.
|
* The single vue-i18n instance shared by the core shell and all modules.
|
||||||
@@ -9,8 +8,8 @@ export const FALLBACK_LOCALE = 'en';
|
|||||||
export const i18n = createI18n({
|
export const i18n = createI18n({
|
||||||
legacy: false,
|
legacy: false,
|
||||||
globalInjection: false,
|
globalInjection: false,
|
||||||
locale: FALLBACK_LOCALE,
|
locale: DEFAULT_LOCALE,
|
||||||
fallbackLocale: FALLBACK_LOCALE,
|
fallbackLocale: DEFAULT_LOCALE,
|
||||||
missingWarn: import.meta.env.DEV,
|
missingWarn: import.meta.env.DEV,
|
||||||
fallbackWarn: false,
|
fallbackWarn: false,
|
||||||
messages: {
|
messages: {
|
||||||
@@ -31,3 +30,8 @@ export function mergeCatalog(locale: string, namespace: string, messages: Record
|
|||||||
export function applyLocale(locale: string): void {
|
export function applyLocale(locale: string): void {
|
||||||
i18n.global.locale.value = locale;
|
i18n.global.locale.value = locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Update the locale used when a translation is missing from the active locale. */
|
||||||
|
export function applyFallbackLocale(locale: string): void {
|
||||||
|
i18n.global.fallbackLocale.value = locale;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { FALLBACK_LOCALE, mergeCatalog, applyLocale } from '@KTXC/l10n/runtime';
|
import { DEFAULT_LOCALE } from '@KTXC/l10n/config';
|
||||||
|
import { mergeCatalog, applyLocale, applyFallbackLocale } from '@KTXC/l10n/runtime';
|
||||||
import { useUserStore } from '@KTXC/stores/userStore';
|
import { useUserStore } from '@KTXC/stores/userStore';
|
||||||
|
|
||||||
const CORE_NAMESPACE = 'core';
|
const CORE_NAMESPACE = 'core';
|
||||||
const SETTING_KEY = 'core.locale';
|
const SETTING_KEY = 'core.locale';
|
||||||
const RTL_LANGUAGES = ['ar', 'fa', 'he', 'ur'];
|
const RTL_LANGUAGES = ['ar', 'fa', 'he', 'ur'];
|
||||||
|
|
||||||
export type CatalogMessages = Record<string, unknown>;
|
export type TranslationCatalog = Record<string, unknown>;
|
||||||
export type CatalogProvider = (locale: string) => Promise<CatalogMessages | null>;
|
export type TranslationLoader = (locale: string) => Promise<TranslationCatalog | null>;
|
||||||
|
|
||||||
export interface L10nInitPayload {
|
export interface L10nInitPayload {
|
||||||
locale?: string;
|
locale?: string;
|
||||||
@@ -17,14 +18,15 @@ export interface L10nInitPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useL10nStore = defineStore('l10nStore', () => {
|
export const useL10nStore = defineStore('l10nStore', () => {
|
||||||
const locale = ref(FALLBACK_LOCALE);
|
const locale = ref(DEFAULT_LOCALE);
|
||||||
const available = ref<string[]>([FALLBACK_LOCALE]);
|
const fallbackLocale = ref(DEFAULT_LOCALE);
|
||||||
|
const available = ref<string[]>([DEFAULT_LOCALE]);
|
||||||
|
|
||||||
const isRtl = computed(() => RTL_LANGUAGES.includes(locale.value.split('-')[0]));
|
const isRtl = computed(() => RTL_LANGUAGES.includes(locale.value.split('-')[0]));
|
||||||
|
|
||||||
// Catalog providers know where their namespace's messages come from. The
|
// Translation loaders know where their namespace's messages come from. The
|
||||||
// store only coordinates them as the active locale changes.
|
// store only coordinates them as the active locale changes.
|
||||||
const catalogProviders = new Map<string, CatalogProvider>([
|
const translationLoaders = new Map<string, TranslationLoader>([
|
||||||
[CORE_NAMESPACE, async (target: string) => {
|
[CORE_NAMESPACE, async (target: string) => {
|
||||||
const response = await fetch(`/l10n/${target}.json`, {
|
const response = await fetch(`/l10n/${target}.json`, {
|
||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
@@ -40,10 +42,10 @@ export const useL10nStore = defineStore('l10nStore', () => {
|
|||||||
const cacheKey = `${namespace}:${target}`;
|
const cacheKey = `${namespace}:${target}`;
|
||||||
const known = catalogState.get(cacheKey);
|
const known = catalogState.get(cacheKey);
|
||||||
if (known !== undefined) return known;
|
if (known !== undefined) return known;
|
||||||
const provider = catalogProviders.get(namespace);
|
const loader = translationLoaders.get(namespace);
|
||||||
if (!provider) return false;
|
if (!loader) return false;
|
||||||
try {
|
try {
|
||||||
const messages = await provider(target);
|
const messages = await loader(target);
|
||||||
if (!messages) {
|
if (!messages) {
|
||||||
catalogState.set(cacheKey, false);
|
catalogState.set(cacheKey, false);
|
||||||
return false;
|
return false;
|
||||||
@@ -65,28 +67,28 @@ export const useL10nStore = defineStore('l10nStore', () => {
|
|||||||
/** Load every registered namespace (core + modules) for a locale. */
|
/** Load every registered namespace (core + modules) for a locale. */
|
||||||
async function loadCatalogs(target: string): Promise<void> {
|
async function loadCatalogs(target: string): Promise<void> {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
[...catalogProviders.keys()].map((namespace) => loadCatalog(namespace, target)),
|
[...translationLoaders.keys()].map((namespace) => loadCatalog(namespace, target)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a namespace-specific catalog provider. The caller owns catalog
|
* Register a namespace-specific translation loader. The caller owns catalog
|
||||||
* discovery and transport; this store owns locale orchestration and merging.
|
* discovery and transport; this store owns locale orchestration and merging.
|
||||||
*/
|
*/
|
||||||
async function registerCatalogProvider(namespace: string, provider: CatalogProvider): Promise<void> {
|
async function registerLoader(namespace: string, loader: TranslationLoader): Promise<void> {
|
||||||
catalogProviders.set(namespace, provider);
|
translationLoaders.set(namespace, loader);
|
||||||
for (const cacheKey of catalogState.keys()) {
|
for (const cacheKey of catalogState.keys()) {
|
||||||
if (cacheKey.startsWith(`${namespace}:`)) catalogState.delete(cacheKey);
|
if (cacheKey.startsWith(`${namespace}:`)) catalogState.delete(cacheKey);
|
||||||
}
|
}
|
||||||
await loadCatalog(namespace, FALLBACK_LOCALE);
|
await loadCatalog(namespace, fallbackLocale.value);
|
||||||
if (locale.value !== FALLBACK_LOCALE) {
|
if (locale.value !== fallbackLocale.value) {
|
||||||
await loadCatalog(namespace, locale.value);
|
await loadCatalog(namespace, locale.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function activate(target: string): Promise<void> {
|
async function activate(target: string): Promise<void> {
|
||||||
await loadCatalogs(FALLBACK_LOCALE);
|
await loadCatalogs(fallbackLocale.value);
|
||||||
if (target !== FALLBACK_LOCALE) {
|
if (target !== fallbackLocale.value) {
|
||||||
await loadCatalogs(target);
|
await loadCatalogs(target);
|
||||||
}
|
}
|
||||||
locale.value = target;
|
locale.value = target;
|
||||||
@@ -115,6 +117,10 @@ export const useL10nStore = defineStore('l10nStore', () => {
|
|||||||
* preferences alone when there is no session (public shell).
|
* preferences alone when there is no session (public shell).
|
||||||
*/
|
*/
|
||||||
async function init(payload?: L10nInitPayload | null): Promise<void> {
|
async function init(payload?: L10nInitPayload | null): Promise<void> {
|
||||||
|
if (payload?.fallback) {
|
||||||
|
fallbackLocale.value = payload.fallback;
|
||||||
|
applyFallbackLocale(payload.fallback);
|
||||||
|
}
|
||||||
if (payload?.available?.length) {
|
if (payload?.available?.length) {
|
||||||
available.value = payload.available;
|
available.value = payload.available;
|
||||||
}
|
}
|
||||||
@@ -124,12 +130,12 @@ export const useL10nStore = defineStore('l10nStore', () => {
|
|||||||
}
|
}
|
||||||
// No resolved locale: probe browser candidates against served catalogs
|
// No resolved locale: probe browser candidates against served catalogs
|
||||||
for (const candidate of browserCandidates()) {
|
for (const candidate of browserCandidates()) {
|
||||||
if (candidate === FALLBACK_LOCALE || (await loadCoreCatalog(candidate))) {
|
if (candidate === fallbackLocale.value || (await loadCoreCatalog(candidate))) {
|
||||||
await activate(candidate);
|
await activate(candidate);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await activate(FALLBACK_LOCALE);
|
await activate(fallbackLocale.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Switch locale; persists as the user's `core.locale` setting by default. */
|
/** Switch locale; persists as the user's `core.locale` setting by default. */
|
||||||
@@ -146,6 +152,6 @@ export const useL10nStore = defineStore('l10nStore', () => {
|
|||||||
isRtl,
|
isRtl,
|
||||||
init,
|
init,
|
||||||
setLocale,
|
setLocale,
|
||||||
registerCatalogProvider,
|
registerLoader,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { router } from '@KTXC/router';
|
|||||||
import { useModuleStore } from '@KTXC/stores/moduleStore';
|
import { useModuleStore } from '@KTXC/stores/moduleStore';
|
||||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
||||||
import { useL10nStore } from '@KTXC/stores/l10nStore';
|
import { useL10nStore } from '@KTXC/stores/l10nStore';
|
||||||
import type { CatalogMessages } from '@KTXC/stores/l10nStore';
|
import type { TranslationCatalog } from '@KTXC/stores/l10nStore';
|
||||||
|
|
||||||
function installModuleCSS(moduleHandle: string, cssPaths: string | string[]): void {
|
function installModuleCSS(moduleHandle: string, cssPaths: string | string[]): void {
|
||||||
const cssFiles = Array.isArray(cssPaths) ? cssPaths : [cssPaths];
|
const cssFiles = Array.isArray(cssPaths) ? cssPaths : [cssPaths];
|
||||||
@@ -53,10 +53,10 @@ function installModuleIntegrations(moduleHandle: string, integrations: Record<st
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function installModuleLocalizations(moduleHandle: string): Promise<void> {
|
async function installModuleLocalizations(moduleHandle: string): Promise<void> {
|
||||||
await useL10nStore().registerCatalogProvider(moduleHandle, (locale) => loadModuleLocalizations(moduleHandle, locale));
|
await useL10nStore().registerLoader(moduleHandle, (locale) => loadModuleLocalizations(moduleHandle, locale));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadModuleLocalizations(moduleHandle: string, locale: string): Promise<CatalogMessages | null> {
|
async function loadModuleLocalizations(moduleHandle: string, locale: string): Promise<TranslationCatalog | null> {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/modules/${moduleHandle}/static/l10n/${locale}.json`,
|
`/modules/${moduleHandle}/static/l10n/${locale}.json`,
|
||||||
{ headers: { Accept: 'application/json' } },
|
{ headers: { Accept: 'application/json' } },
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
* by hand in en.manual.json next to the target catalog; they are merged in.
|
* by hand in en.manual.json next to the target catalog; they are merged in.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* node scripts/l10n-extract.mjs # write core/src/l10n/en.json
|
* node scripts/l10n-extract.mjs # write core/l10n/en.json
|
||||||
* node scripts/l10n-extract.mjs --check # exit 1 if the file is stale
|
* node scripts/l10n-extract.mjs --check # exit 1 if the file is stale
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ const targets = [
|
|||||||
{
|
{
|
||||||
name: 'core',
|
name: 'core',
|
||||||
sourceDir: path.join(rootDir, 'core/src'),
|
sourceDir: path.join(rootDir, 'core/src'),
|
||||||
catalogFile: path.join(rootDir, 'core/src/l10n/en.json'),
|
catalogFile: path.join(rootDir, 'core/l10n/en.json'),
|
||||||
},
|
},
|
||||||
...discoverModuleTargets(),
|
...discoverModuleTargets(),
|
||||||
];
|
];
|
||||||
|
|||||||
+1
-1
@@ -51,7 +51,7 @@ export default defineConfig(({ mode }) => ({
|
|||||||
rename: { stripBase: true },
|
rename: { stripBase: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: path.resolve(__dirname, 'core/src/l10n/*.json'),
|
src: path.resolve(__dirname, 'core/l10n/*.json'),
|
||||||
dest: 'l10n',
|
dest: 'l10n',
|
||||||
rename: { stripBase: true },
|
rename: { stripBase: true },
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user