i18n for models with fallback

This commit is contained in:
Jérémy Munsch 2024-01-17 17:44:12 +01:00
parent 1b180f07cf
commit 1f201326ee
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
16 changed files with 152 additions and 71 deletions

18
app.vue
View file

@ -42,7 +42,7 @@ import { defineComponent } from "vue";
import type { RouteLocationNormalizedLoaded } from "vue-router";
import ToolboxHelper from "./classes/ToolboxHelper";
import { useAppStore } from "~/stores/useAppStore";
import data from "data/test.json";
// import data from "data/test.json";
export default defineComponent({
name: "IndexPage",
@ -54,7 +54,7 @@ export default defineComponent({
// adminModalId: string;
// modal: Modal;
// adminModal: Modal;
data: typeof data;
// data: typeof data;
// selectedItem: Item | null;
// konami: KonamiCode;
// contacts: Array<Contact>;
@ -65,7 +65,7 @@ export default defineComponent({
route: useRoute(),
// modalId: "productModal",
// adminModalId: "adminModal",
data,
// data,
// selectedItem: null,
// modal: {} as Modal,
// adminModal: {} as Modal,
@ -97,11 +97,11 @@ export default defineComponent({
},
mounted() {
// * Make sure array does not start using 0 index
const items: typeof data.items = [];
this.data.items.forEach((item, itemIndex) => {
items[itemIndex + 1] = item;
});
this.data.items = items;
// const items: typeof data.items = [];
// this.data.items.forEach((item, itemIndex) => {
// items[itemIndex + 1] = item;
// });
// this.data.items = items;
// * Init contacts (Fetch form local storage or new)
// const serializedContacts = window.localStorage.getItem("contacts");
// this.contacts = !serializedContacts ? new Array<Contact>() : JSON.parse(serializedContacts);
@ -121,7 +121,7 @@ export default defineComponent({
// * INIT STATES
window.ipcRendererObject.getJsonData().then((dataJson) => {
useAppStore().setupAppData(dataJson);
useAppStore().setupAppData(dataJson, this.$i18n.locale, String(this.$i18n.fallbackLocale));
this.loading = false;
this.setTimeout();
dataJson.fonts.forEach(font => this.injectFontFace(font.font_name, font.font));

View file

@ -4,14 +4,36 @@ export default class AppState {
/** Json data as exported from zip and compliant with schemas */
private jsonData: TypeApp;
constructor(jsonData: TypeApp) {
/** Current App locale */
private locale: string;
/** Fallback App locale */
private fallbackLocale: string;
constructor(jsonData: TypeApp, locale: string, fallbackLocale: string) {
this.jsonData = jsonData;
this.locale = locale;
this.fallbackLocale = fallbackLocale;
}
public settings(): TypeSettings {
return this.jsonData.settings;
}
public switchLocale(locale: string): void {
this.locale = locale;
}
public geti18N() {
return {
locale: this.locale,
fallbackLocale: this.fallbackLocale,
};
}
public getFallbackLocale(): string {
return this.fallbackLocale;
}
// * --- BRANDS
public brands(): Array<TypeBrand> {
@ -19,7 +41,7 @@ export default class AppState {
}
public brand(slug:string): TypeBrand|undefined {
return this.jsonData.items.find(brand => brand.slug === slug);
return this.jsonData.items.find(brand => brand.slug[this.locale] === slug);
}
public brandFromRange(range:TypeRange): TypeBrand {
@ -48,7 +70,7 @@ export default class AppState {
}
public range(slug:string): TypeRange|undefined {
return this.ranges().find(range => range.slug === slug);
return this.ranges().find(range => range.slug[this.locale] === slug);
}
// * --- END RANGES

View file

@ -32,7 +32,7 @@ export default class ToolboxHelper {
}
public static trans(text: string, params: Record<string,string> = {}, localeToUse: string|null = null): string {
const { t } = useI18n();
const { t } = useI18n({ useScope: "global"});
if(localeToUse) {
return t(text, params, {
locale: localeToUse,
@ -40,4 +40,15 @@ export default class ToolboxHelper {
}
return t(text, params);
}
public static getTranslatableField(model: TypeBrand | TypeRange | TypeProduct, field: "name" | "slug" | "description"): string {
const { locale, fallbackLocale } = useAppStore().getI18n();
const i18nField: Typei18nField | null = (model[field as keyof typeof model] ?? null) as Typei18nField | null;
if (!i18nField) {
throw new Error(`getTranslatableField ${field} is not part of ${model}`);
}
return i18nField[locale] !== undefined ? i18nField[locale] : i18nField[fallbackLocale];
}
}

View file

@ -5,7 +5,7 @@
<button
class="rounded-pill btn btn-primary px-4 py-1"
:disabled="previousRange === null"
:title="`Voir les produits de la gamme ${previousRange?.name}`"
:title="previousRange ? t('view_range_products', {'range_name': transField(previousRange, 'name')}) : ''"
data-bs-toggle="tooltip"
@click="gotoRange(previousRange)"
>
@ -15,7 +15,7 @@
<div class="col home text-center px-3">
<button
class="rounded-pill btn btn-primary px-4 py-1"
title="Voir les gammes"
:title="activeBrand ? t('view_brand_ranges', {'brand_name': transField(activeBrand, 'name')}) : ''"
data-bs-toggle="tooltip"
@click="gotoBrand(activeBrand)"
>
@ -26,7 +26,7 @@
<button
class="rounded-pill btn btn-primary px-4 py-1"
:disabled="nextRange === null"
:title="`Voir les produits de la gamme ${nextRange?.name}`"
:title="nextRange ? t('view_range_products', {'range_name': transField(nextRange, 'name')}) : ''"
data-bs-toggle="tooltip"
@click="gotoRange(nextRange)"
>
@ -41,11 +41,13 @@
// eslint-disable-next-line import/named
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { defineComponent } from "vue";
import ToolboxHelper from "~/classes/ToolboxHelper";
export default defineComponent({
name: "NavigationMenu",
components: { FontAwesomeIcon },
data(): {
locale: string;
brands: Array<TypeRange>;
nextRange: TypeRange | null;
previousRange: TypeRange | null;
@ -53,6 +55,7 @@ export default defineComponent({
activeBrand: TypeBrand | null;
} {
return {
locale: "",
brands: [],
nextRange: null,
previousRange: null,
@ -67,8 +70,16 @@ export default defineComponent({
},
mounted() {
this.refreshData();
const { locale } = useAppStore().getI18n();
this.locale = locale;
},
methods: {
t(text: string, params: Record<string,string> = {}, localeToUse: string|null = null): string {
return ToolboxHelper.trans(text, params, localeToUse);
},
transField(model: TypeBrand | TypeRange | TypeProduct, field: "name" | "slug" | "description") {
return ToolboxHelper.getTranslatableField(model, field);
},
refreshData() {
this.activeBrand = useAppStore().activeBrand;
this.activeRange = useAppStore().activeRange;
@ -80,14 +91,14 @@ export default defineComponent({
return;
}
useAppStore().setActiveRange(null);
navigateTo(useLocaleRoute()({"name": "brands-slug", "params": {"slug": activeBrand.slug}}));
navigateTo(useLocaleRoute()({"name": "brands-slug", "params": {"slug": activeBrand.slug[this.locale]}}));
},
gotoRange(range: TypeRange|null) {
if(!range) {
return;
}
useAppStore().setActiveRange(range);
navigateTo(useLocaleRoute()({"name": "ranges-slug", "params": {"slug": range.slug}}));
navigateTo(useLocaleRoute()({"name": "ranges-slug", "params": {"slug": range.slug[this.locale]}}));
}
}
});

View file

@ -40,14 +40,14 @@
class="swiper-slide"
:to="`/brands/${brand.slug}`"
data-bs-toggle="tooltip"
:title="`Voir les gammes de la marque ${brand.name}`"
:title="`Voir les gammes de la marque ${transField(brand, 'name')}`"
@click.prevent="gotoBrand(brand)"
>
<img
class="img-fluid"
:src="`/data/${brand.picture}`"
:alt="`Marque ${brand.name}`"
:title="`Le logo de la marque ${brand.name}`"
:alt="`Marque ${transField(brand, 'name')}`"
:title="`Le logo de la marque ${transField(brand, 'name')}`"
data-bs-toggle="tooltip"
>
</NuxtLinkLocale>
@ -87,6 +87,7 @@ export default defineComponent({
name: "AppFooter",
components: {Swiper, SwiperSlide},
data(): {
locale: string;
versions: typeof window.versions;
appInfo: typeof appInfo;
route: RouteLocationNormalizedLoaded;
@ -97,6 +98,7 @@ export default defineComponent({
}>;
} {
return {
locale: "",
route: useRoute(),
versions: window.versions,
appInfo: {
@ -123,17 +125,22 @@ export default defineComponent({
window.ipcRendererObject.getAppInfo().then((appInfo) => {this.appInfo = appInfo;});
this.brands = useAppStore().data.brands();
this.activeBrand = useAppStore().activeBrand;
const { locale } = useAppStore().getI18n();
this.locale = locale;
},
methods: {
t(text: string, params: Record<string,string> = {}, localeToUse: string|null = null): string {
return ToolboxHelper.trans(text, params, localeToUse);
},
transField(model: TypeBrand | TypeRange | TypeProduct, field: "name" | "slug" | "description") {
return ToolboxHelper.getTranslatableField(model, field);
},
getContrastYIQ(hexcolor: string): string {
return ToolboxHelper.getContrastYIQ(hexcolor);
},
gotoBrand(brand: TypeBrand) {
useAppStore().setActiveBrand(brand);
navigateTo(useLocaleRoute()({"name": "brands-slug", "params": {"slug": brand.slug}}));
navigateTo(useLocaleRoute()({"name": "brands-slug", "params": {"slug": this.transField(brand, "slug")}}));
}
}
});

View file

@ -15,19 +15,19 @@
}"
>
<NuxtLinkLocale
:to="`/brands/${activeBrand.slug}`"
:to="`/brands/${transField(activeBrand, 'slug')}`"
:class="{
'd-inline-block py-3 px-2 text-center': true,
'flex-grow-1': !activeRange
}"
data-bs-toggle="tooltip"
:title="t('view_brand_ranges', {'brand_name': activeBrand.name})"
:title="t('view_brand_ranges', {'brand_name': transField(activeBrand, 'name')})"
>
<img
class="img-fluid main-logo"
:src="`/data/${activeBrand.picture}`"
:alt="`Marque ${activeBrand.name}`"
:title="t('brands_logo', {'brand_name': activeBrand.name})"
:alt="`Marque ${transField(activeBrand, 'name')}`"
:title="t('brands_logo', {'brand_name': transField(activeBrand, 'name')})"
>
</NuxtLinkLocale>
<div v-if="activeRange" class="flex-grow-1 text-center">
@ -42,7 +42,7 @@
'text-shadow': `0 0 0.2rem ${toRGBA(getContrastYIQ(activeRange.color), 0.8)}`
}"
>
{{ activeRange.name }}
{{ transField(activeRange, 'name') }}
</span>
</div>
</div>
@ -75,7 +75,11 @@
<button
v-for="(localeObject,localeIndex) of locales"
:key="localeIndex"
class="btn btn-link px-2 shadow"
:class="{
'btn px-2 shadow': true,
'btn-link': locale === localeObject.iso,
'btn-warning': locale !== localeObject.iso,
}"
:title="localeObject.label"
data-bs-toggle="tooltip"
@click.prevent="switchLocale(localeObject)"
@ -97,7 +101,7 @@ import ToolboxHelper from "~/classes/ToolboxHelper";
import { useAppStore } from "~/stores/useAppStore";
import bsTooltips from "~/vueModules/bsTooltips";
type Locale = {
type LocaleParams = {
iso: string,
flag: string,
label: string,
@ -108,8 +112,8 @@ export default defineComponent({
data(): {
versions: typeof window.versions;
settings: TypeSettings;
locales: Array<Locale>,
locale: string|null;
locales: Array<LocaleParams>,
locale: string;
route: RouteLocationNormalizedLoaded;
activeBrand: TypeBrand|null;
activeRange: TypeRange|null;
@ -118,7 +122,7 @@ export default defineComponent({
versions: window.versions,
settings: {} as TypeSettings,
locales: [],
locale: null,
locale: "",
route: useRoute(),
activeBrand: null,
activeRange: null,
@ -133,13 +137,15 @@ export default defineComponent({
}
this.activeBrand = useAppStore().activeBrand;
this.activeRange = useAppStore().activeRange;
const { locale } = useAppStore().getI18n();
this.locale = locale;
},
},
mounted() {
this.$nextTick(() => bsTooltips.methods.initTooltips());
this.settings = useAppStore().data.settings();
this.activeBrand = useAppStore().activeBrand;
const { locale, availableLocales } = useI18n();
const { locale, availableLocales } = useI18n({ useScope: "global"});
this.locale = locale.value;
this.locales = availableLocales.map(localeStr => {
return {
@ -153,10 +159,14 @@ export default defineComponent({
t(text: string, params: Record<string,string> = {}, localeToUse: string|null = null): string {
return ToolboxHelper.trans(text, params, localeToUse);
},
switchLocale(locale:Locale) {
transField(model: TypeBrand | TypeRange | TypeProduct, field: "name" | "slug" | "description") {
return ToolboxHelper.getTranslatableField(model, field);
},
switchLocale(localeToSwitch:LocaleParams) {
const switchLocalePath = useSwitchLocalePath();
Toast.toast(locale.label, ToastTypeEnum.default);
navigateTo(switchLocalePath(locale.iso));
Toast.toast(localeToSwitch.label, ToastTypeEnum.default);
useAppStore().switchLocale(localeToSwitch.iso);
navigateTo(switchLocalePath(localeToSwitch.iso));
},
rangeFont(fontId: number) {
const font = useAppStore().getFont(fontId);

View file

@ -6,7 +6,7 @@ export default defineI18nConfig(() => ({
legacy: false,
globalInjection: true,
locale: "en",
fallbackLocale: "en",
fallbackLocale: "fr",
messages: {
en,
fr,

18
index.d.ts vendored
View file

@ -5,6 +5,9 @@ export { };
// * --- MODELS From JSON Schema
type Locale = string;
type i18nField = Record<Locale, string>
/** Showroom Bo json data format */
type AppModel = {
/** Version de l'application */
@ -47,9 +50,9 @@ type BrandModel = {
/** The unique identifier for a brand */
id: number;
/** Nom de la marque */
name: string;
name: i18nField;
/** Slug de la marque */
slug: string;
slug: i18nField;
/** Couleur de la marque (hex color) */
color: string;
/** Logo de la marque, chemin tel que présent dans le zip */
@ -64,9 +67,9 @@ type RangeModel = {
/** The unique identifier for a range */
id: number;
/** Nom de la gamme */
name: string;
name: i18nField;
/** Slug de la gamme */
slug: string;
slug: i18nField;
/** Logo de la gamme, chemin tel que présent dans le zip */
picture: string;
/** Couleur de la gamme */
@ -89,11 +92,11 @@ type ProductModel = {
/** The unique identifier for a product */
id: number;
/** Nom du produit */
name: string;
name: i18nField;
/** Slug du produit */
slug: string;
slug: i18nField;
/** Description du produit, texte WYSYWYG */
description: string;
description: i18nField;
/** Logo du produit, chemin tel que présent dans le zip */
picture: string;
/** Clé étrangère de la gamme */
@ -150,6 +153,7 @@ declare global {
type TypeProduct = ProductModel;
type TypeFont = FontModel;
type Typei18nField = i18nField;
type ToastTypeEnum = ToastType;

View file

@ -66,8 +66,8 @@ export default defineNuxtConfig({
},
i18n: {
locales: ["en","fr", "nl"],
defaultLocale: "en",
locales: ["en", "fr", "nl"],
defaultLocale: "fr",
// * Ajouter aussi la locale dans ce fichier
vueI18n: "./i18n.config.ts"
}

View file

@ -14,7 +14,7 @@
:class="{
'd-inline-block btn position-relative': true,
}"
:title="t('view_range_products', {'range_name': range.name})"
:title="t('view_range_products', {'range_name': transField(range, 'name')})"
data-bs-toggle="tooltip"
@click.stop="gotoRange(range)"
>
@ -22,8 +22,8 @@
<img
class="img-fluid"
:src="`/data/${range.picture}`"
:alt="t('ranges_logo', {'range_name': range.name})"
:title="t('ranges_logo', {'range_name': range.name})"
:alt="t('ranges_logo', {'range_name': transField(range, 'name')})"
:title="t('ranges_logo', {'range_name': transField(range, 'name')})"
>
</div>
<span
@ -41,7 +41,7 @@
'text-shadow': `0 0 0.2rem ${toRGBA(getContrastYIQ(range.color), 0.8)}`,
}"
>
{{ range.name }}
{{ transField(range, 'name') }}
</span>
</button>
</div>
@ -117,6 +117,9 @@ export default defineComponent({
t(text: string, params: Record<string,string> = {}, localeToUse: string|null = null): string {
return ToolboxHelper.trans(text, params, localeToUse);
},
transField(model: TypeBrand | TypeRange | TypeProduct, field: "name" | "slug" | "description") {
return ToolboxHelper.getTranslatableField(model, field);
},
titleAnimations(rangeIndex: number) {
const animation = this.animations.at(ToolboxHelper.randomInteger(0, this.animations.length -1));
const out: Map<string, boolean> = new Map<string, boolean>();
@ -144,7 +147,7 @@ export default defineComponent({
},
gotoRange(range: TypeRange) {
useAppStore().setActiveRange(range);
navigateTo(useLocaleRoute()({"name": "ranges-slug", "params": {"slug": range.slug}}));
navigateTo(useLocaleRoute()({"name": "ranges-slug", "params": {"slug": this.transField(range, "slug")}}));
}
}
});

View file

@ -28,11 +28,11 @@
:class="{...{'brand-title fw-bold text-capitalize fs-1 animate__animated': true}, ...titleAnimations(brandIndex)}"
:style="{ 'color': brand.color }"
role="button"
:title="t('view_brand_ranges', {'brand_name': brand.name})"
:title="t('view_brand_ranges', {'brand_name': transField(brand, 'name')})"
data-bs-toggle="tooltip"
@click="gotoBrand(brand)"
>
{{ brand.name }}
{{ transField(brand, 'name') }}
</span>
</div>
</div>
@ -46,15 +46,15 @@
<div
class="d-inline-block"
role="button"
:title="t('view_brand_ranges', {'brand_name': brand.name})"
:title="t('view_brand_ranges', {'brand_name': transField(brand, 'name')})"
data-bs-toggle="tooltip"
@click="gotoBrand(brand)"
>
<img
class="img-fluid"
:src="`/data/${brand.picture}`"
:alt="t('brands_logo', {'brand_name': brand.name})"
:title="t('brands_logo', {'brand_name': brand.name})"
:alt="t('brands_logo', {'brand_name': transField(brand, 'name')})"
:title="t('brands_logo', {'brand_name': transField(brand, 'name')})"
>
</div>
</div>
@ -114,6 +114,9 @@ export default defineComponent({
t(text: string, params: Record<string,string> = {}, localeToUse: string|null = null): string {
return ToolboxHelper.trans(text, params, localeToUse);
},
transField(model: TypeBrand | TypeRange | TypeProduct, field: "name" | "slug" | "description") {
return ToolboxHelper.getTranslatableField(model, field);
},
setBrandToAnimate(): void {
clearTimeout(this.animateTimeout);
this.animateIndex = ToolboxHelper.randomInteger(0, this.appState.brands().length -1);
@ -127,7 +130,7 @@ export default defineComponent({
},
gotoBrand(brand: TypeBrand) {
useAppStore().setActiveBrand(brand);
navigateTo(useLocaleRoute()({"name": "brands-slug", "params": {"slug": brand.slug}}));
navigateTo(useLocaleRoute()({"name": "brands-slug", "params": {"slug": this.transField(brand, "slug")}}));
}
},
});

View file

@ -44,13 +44,13 @@
'img-fluid': activeProduct?.id === product.id,
}"
:src="`/data/${product.picture}`"
:alt="t('products_logo', {'product_name': product.name})"
:title="t('products_logo', {'product_name': product.name})"
:alt="t('products_logo', {'product_name': transField(product, 'name')})"
:title="t('products_logo', {'product_name': transField(product, 'name')})"
>
</div>
<Transition>
<div v-if="activeProduct?.id === product.id" class="col pt-5 ps-md-4">
<div v-sane-html="product.description" class="p-md-3 my-md-5 description" />
<div v-sane-html="transField(product, 'description')" class="p-md-3 my-md-5 description" />
</div>
</Transition>
</div>
@ -69,7 +69,7 @@
'background-color': activeProduct?.id === product.id ? '' : range.color
}"
>
{{ product.name }}
{{ transField(product, 'name') }}
</span>
</div>
</div>
@ -133,6 +133,7 @@ export default defineComponent({
return ;
}
this.range = range;
// * Pour que ces putains de titres label aient la meme hauteur.
this.$nextTick(() => {
const spanList = (this.$refs.rangeList as HTMLDivElement).querySelectorAll(".product-title") as NodeListOf<HTMLSpanElement>;
@ -152,6 +153,9 @@ export default defineComponent({
t(text: string, params: Record<string,string> = {}, localeToUse: string|null = null): string {
return ToolboxHelper.trans(text, params, localeToUse);
},
transField(model: TypeBrand | TypeRange | TypeProduct, field: "name" | "slug" | "description") {
return ToolboxHelper.getTranslatableField(model, field);
},
getContrastYIQ(hexcolor: string): string {
return ToolboxHelper.getContrastYIQ(hexcolor);
},

View file

@ -11,11 +11,11 @@
},
"name": {
"description": "Nom de la marque",
"type": "string"
"type": "object"
},
"slug": {
"description": "Slug de la marque",
"type": "string"
"type": "object"
},
"color": {
"description": "Couleur de la marque",

View file

@ -11,15 +11,15 @@
},
"name": {
"description": "Nom du produit",
"type": "string"
"type": "object"
},
"slug": {
"description": "Slug du produit",
"type": "string"
"type": "object"
},
"description": {
"description": "Description du produit, texte WYSYWYG",
"type": "string"
"type": "object"
},
"picture": {
"description": "Logo du produit, chemin tel que présent dans le zip",

View file

@ -11,11 +11,11 @@
},
"name": {
"description": "Nom de la gamme",
"type": "string"
"type": "object"
},
"slug": {
"description": "Slug de la gamme",
"type": "string"
"type": "object"
},
"picture": {
"description": "Logo de la gamme, chemin tel que présent dans le zip",

View file

@ -2,7 +2,7 @@ import AppState from "../classes/AppState";
export const useAppStore = defineStore("appStore", {
state: () => ({
data: new AppState({} as TypeApp),
data: new AppState({} as TypeApp, "fr", "fr"),
activeBrand: null,
activeRange: null,
} as {
@ -11,8 +11,8 @@ export const useAppStore = defineStore("appStore", {
activeRange: TypeRange | null;
}),
actions: {
setupAppData(dataJson: TypeApp): void {
this.data = new AppState(dataJson);
setupAppData(dataJson: TypeApp, locale: string, fallbackLocale: string): void {
this.data = new AppState(dataJson, locale, fallbackLocale);
},
setActiveBrand(brand: TypeBrand|null): void {
this.activeBrand = brand;
@ -47,6 +47,12 @@ export const useAppStore = defineStore("appStore", {
return null;
}
return font;
},
getI18n() {
return this.data.geti18N();
},
switchLocale(locale: string): void {
this.data.switchLocale(locale);
}
}
});