vueless
Version:
Vue Styleless UI Component Library, powered by Tailwind CSS.
196 lines (155 loc) • 5.26 kB
text/typescript
import { shallowRef, ref } from "vue";
import { merge } from "lodash-es";
import en from "./locales/en.ts";
import type { Ref } from "vue";
import type { UnknownObject } from "../types.ts";
import type { VueMessageType } from "vue-i18n";
export interface LocaleMessages {
[key: string]: LocaleMessages | string;
}
export interface LocaleOptions {
messages?: LocaleMessages;
locale?: string;
fallback?: string;
adapter?: LocaleInstance;
}
export interface LocaleInstance {
name: string;
messages: Ref<LocaleMessages>;
locale: Ref<string>;
fallback: Ref<string>;
t: (key: string, ...params: unknown[]) => string;
n: (value: number) => string;
tm: <TMassages>(key: string) => Partial<TMassages>;
}
const FALLBACK_LOCALE_CODE = "en";
export default function createVuelessAdapter(options?: LocaleOptions): LocaleInstance {
const current = shallowRef(options?.locale ?? FALLBACK_LOCALE_CODE);
const fallback = shallowRef(options?.fallback ?? FALLBACK_LOCALE_CODE);
const messages = ref(merge({ en }, options?.messages));
return {
name: "vueless",
locale: current,
fallback,
messages,
t: createTranslateFunction(current, fallback, messages),
tm: createTranslateMessageFunction(current, fallback, messages),
n: createNumberFunction(current, fallback),
};
}
function createTranslateFunction(
current: Ref<string>,
fallback: Ref<string>,
messages: Ref<LocaleMessages>,
) {
return (key: string, ...params: unknown[]) => {
const currentLocale = current.value && messages.value[current.value];
const fallbackLocale = fallback.value && messages.value[fallback.value];
let str = getObjectValueByPath<LocaleMessages | string, unknown>(currentLocale, key, null);
if (!str) {
// eslint-disable-next-line no-console
console.warn(
`Translation key "${key}" not found in "${current.value}", trying fallback locale`,
);
str = getObjectValueByPath(fallbackLocale, key, null);
}
if (!str) {
// eslint-disable-next-line no-console
console.warn(`Translation key "${key}" not found in fallback`);
str = key;
}
if (typeof str !== "string") {
// eslint-disable-next-line no-console
console.warn(`Translation key "${key}" has a non-string value`);
str = key;
}
return replace(String(str), params);
};
}
function createTranslateMessageFunction(
current: Ref<string>,
fallback: Ref<string>,
messages: Ref<LocaleMessages>,
) {
return <TMassages>(key: string): Partial<TMassages> => {
const currentLocale = current.value && messages.value[current.value];
const fallbackLocale = fallback.value && messages.value[fallback.value];
let str = getObjectValueByPath<LocaleMessages | string, unknown>(currentLocale, key, null);
if (str === undefined) {
// eslint-disable-next-line no-console
console.warn(
`Translation key "${key}" not found in "${current.value}", trying fallback locale`,
);
str = getObjectValueByPath(fallbackLocale, key, null);
}
return str as TMassages;
};
}
const replace = (str: string, params: unknown[]) => {
return str.replace(/\{(\d+)\}/g, (match, index) => {
return String(params[+index]);
});
};
function createNumberFunction(current: Ref<string>, fallback: Ref<string>) {
return (value: number, options?: Intl.NumberFormatOptions) => {
const numberFormat = new Intl.NumberFormat([current.value, fallback.value], options);
return numberFormat.format(value);
};
}
export function getObjectValueByPath<T, K = unknown>(
obj: T,
path?: string,
fallback?: K,
): K | undefined {
if (obj == null || !path || typeof path !== "string") return fallback;
const unknownObject = obj as UnknownObject;
if (unknownObject[path] !== undefined) {
return unknownObject[path] as K;
}
path = path.replace(/\[(\w+)\]/g, ".$1"); // convert indexes to properties
path = path.replace(/^\./, ""); // strip a leading dot
return getNestedValue(obj, path.split("."), fallback);
}
export function getNestedValue<T, K = unknown>(
obj: T | null | undefined,
path: (string | number)[],
fallback?: K,
): K | undefined {
const last = path.length - 1;
if (last < 0) {
return obj === undefined ? fallback : (obj as unknown as K);
}
for (let i = 0; i < last; i++) {
if (obj == null) {
return fallback;
}
obj = (obj as Record<string | number, unknown>)[path[i]] as T;
}
if (obj == null) {
return fallback;
}
const unknownObject = obj as Record<string | number, unknown>;
return (unknownObject[path[last]] === undefined ? fallback : unknownObject[path[last]]) as K;
}
export function recursiveRt(obj: VueMessageType): unknown {
if (typeof obj !== "object" || obj === null) {
return obj;
}
if ("loc" in obj) {
return obj.loc!.source;
}
const messages: Record<string, unknown> = {};
for (const key in obj) {
const val = obj[key as keyof typeof obj];
if (val && typeof val === "object") {
if ("loc" in val) {
messages[key] = val.loc.source;
} else {
messages[key] = recursiveRt(val as VueMessageType);
}
} else {
messages[key] = val;
}
}
return messages;
}