facilmap/frontend/src/lib/utils/i18n.ts

95 wiersze
2.8 KiB
TypeScript

/// <reference types="vite/client" />
import { type i18n } from "i18next";
import { defineComponent, ref } from "vue";
import messagesEn from "../../i18n/en.json";
import messagesDe from "../../i18n/de.json";
import { decodeQueryString, getRawI18n, onI18nReady, setCurrentUnitsGetter } from "facilmap-utils";
import { cookies } from "./cookies";
import { unitsValidator } from "facilmap-types";
const namespace = "facilmap-frontend";
onI18nReady((i18n) => {
i18n.addResourceBundle("en", namespace, messagesEn);
i18n.addResourceBundle("de", namespace, messagesDe);
});
if (import.meta.hot) {
import.meta.hot.accept("../../i18n/en.json", (m) => {
onI18nReady((i18n) => {
i18n.addResourceBundle("en", namespace, m!.default);
});
});
import.meta.hot.accept("../../i18n/de.json", (m) => {
onI18nReady((i18n) => {
i18n.addResourceBundle("de", namespace, m!.default);
});
});
}
const i18nResourceChangeCounter = ref(0);
const onI18nResourceChange = () => {
i18nResourceChangeCounter.value++;
};
onI18nReady((i18n) => {
i18n.store.on("added", onI18nResourceChange);
i18n.store.on("removed", onI18nResourceChange);
i18n.on("languageChanged", onI18nResourceChange);
i18n.on("loaded", onI18nResourceChange);
let tBkp = i18n.t;
i18n.t = function(this: any, ...args: any) {
// Consume resource change counter to make calls to t() reactive to i18n resource changes
i18nResourceChangeCounter.value;
return tBkp.apply(this, args);
} as any;
});
setCurrentUnitsGetter(() => {
const queryParams = decodeQueryString(location.search);
const query = queryParams.format ? unitsValidator.safeParse(queryParams.format) : undefined;
return query?.success ? query.data : cookies.units;
});
export function useI18n(): {
t: i18n["t"];
changeLanguage: (lang: string) => Promise<void>;
} {
return {
t: getRawI18n().getFixedT(null, namespace),
changeLanguage: async (lang) => {
await getRawI18n().changeLanguage(lang);
}
};
}
/**
* Renders a translated message. Each interpolation variable needs to be specified as a slot, making it possible to interpolate
* components and rich text.
*/
export const T = defineComponent({
props: {
k: { type: String, required: true }
},
setup(props, { slots }) {
const i18n = useI18n();
return () => {
const mappedSlots = Object.entries(slots).map(([name, slot], i) => ({ name, placeholder: `%___SLOT_${i}___%`, slot }));
const placeholderByName = Object.fromEntries(mappedSlots.map(({ name, placeholder }) => [name, placeholder]));
const slotByPlaceholder = Object.fromEntries(mappedSlots.map(({ placeholder, slot }) => [placeholder, slot]));
const message = i18n.t(props.k, placeholderByName);
return message.split(/(%___SLOT_\d+___%)/g).map((v, i) => {
if (i % 2 === 0) {
return v;
} else {
return slotByPlaceholder[v]!();
}
});
};
}
});