2024-02-17 22:11:17 +00:00
|
|
|
import { cache } from "src/utils/cachedPayload";
|
2024-01-29 22:37:55 +00:00
|
|
|
import en from "./en.json";
|
2024-02-01 20:00:55 +00:00
|
|
|
import fr from "./fr.json";
|
2024-02-17 22:11:17 +00:00
|
|
|
import ja from "./ja.json";
|
2024-02-01 20:00:55 +00:00
|
|
|
|
2024-02-17 22:11:17 +00:00
|
|
|
import acceptLanguage from "accept-language";
|
2024-02-26 02:08:59 +00:00
|
|
|
import { KeysTypes } from "src/shared/payload/payload-sdk";
|
2024-01-29 22:37:55 +00:00
|
|
|
|
|
|
|
type WordingKeys = keyof typeof en;
|
2024-02-01 20:00:55 +00:00
|
|
|
const translationFiles: Record<string, Record<WordingKeys, string>> = {
|
|
|
|
en,
|
|
|
|
fr,
|
2024-02-17 22:11:17 +00:00
|
|
|
ja,
|
2024-02-01 20:00:55 +00:00
|
|
|
};
|
2024-01-29 22:37:55 +00:00
|
|
|
|
2024-01-29 04:02:58 +00:00
|
|
|
export const getI18n = async (locale: string) => {
|
2024-02-01 20:00:55 +00:00
|
|
|
const translations = translationFiles[locale];
|
2024-01-29 04:02:58 +00:00
|
|
|
|
2024-01-29 22:37:55 +00:00
|
|
|
const formatWithValues = (
|
|
|
|
templateName: string,
|
|
|
|
template: string,
|
|
|
|
values: Record<string, any>
|
|
|
|
): string => {
|
|
|
|
Object.entries(values).forEach(([key, value]) => {
|
|
|
|
if (
|
|
|
|
!template.match(new RegExp(`{{ ${key}\\+|{{ ${key}\\?|{{ ${key} }}`))
|
|
|
|
) {
|
|
|
|
console.warn(
|
|
|
|
"Value",
|
|
|
|
key,
|
|
|
|
"has been provided but is not present in template",
|
|
|
|
templateName
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof value === "number") {
|
|
|
|
// Find "plural" tokens
|
|
|
|
const matches = [
|
|
|
|
...template.matchAll(
|
|
|
|
new RegExp(`{{ ${key}\\+,[\\w\\s=>{},]+ }}`, "g")
|
|
|
|
),
|
|
|
|
].map(limitMatchToBalanceCurlyBraces);
|
|
|
|
|
|
|
|
const handlePlural = (match: string): string => {
|
|
|
|
match = match.substring(3, match.length - 3);
|
|
|
|
const options = match.split(",").splice(1);
|
|
|
|
for (const option of options) {
|
|
|
|
const optionCondition = option.split("{")[0];
|
|
|
|
if (!optionCondition) continue;
|
|
|
|
let optionValue = option.substring(optionCondition.length + 1);
|
|
|
|
if (!optionValue) continue;
|
|
|
|
optionValue = optionValue.substring(0, optionValue.length - 1);
|
|
|
|
if (option.startsWith("=")) {
|
|
|
|
const optionConditionValue = Number.parseInt(
|
|
|
|
optionCondition.substring(1)
|
|
|
|
);
|
|
|
|
if (value === optionConditionValue) {
|
|
|
|
return optionValue;
|
|
|
|
}
|
|
|
|
} else if (option.startsWith(">")) {
|
|
|
|
const optionConditionValue = Number.parseInt(
|
|
|
|
optionCondition.substring(1)
|
|
|
|
);
|
|
|
|
if (value > optionConditionValue) {
|
|
|
|
return optionValue;
|
|
|
|
}
|
|
|
|
} else if (option.startsWith("<")) {
|
|
|
|
const optionConditionValue = Number.parseInt(
|
|
|
|
optionCondition.substring(1)
|
|
|
|
);
|
|
|
|
if (value < optionConditionValue) {
|
|
|
|
return optionValue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "";
|
|
|
|
};
|
|
|
|
|
|
|
|
matches.forEach((match) => {
|
|
|
|
template = template.replace(match, handlePlural(match));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find "conditional" tokens
|
|
|
|
const matches = [
|
|
|
|
...template.matchAll(new RegExp(`{{ ${key}\\?,[\\w\\s{},]+ }}`, "g")),
|
|
|
|
].map(limitMatchToBalanceCurlyBraces);
|
|
|
|
|
|
|
|
const handleConditional = (match: string): string => {
|
|
|
|
match = match.substring(3, match.length - 3);
|
|
|
|
const options = match.split(",").splice(1);
|
|
|
|
if (value !== undefined && value !== null && value !== "") {
|
|
|
|
return options[0] ?? "";
|
|
|
|
}
|
|
|
|
return options[1] ?? "";
|
|
|
|
};
|
|
|
|
|
|
|
|
matches.forEach((match) => {
|
|
|
|
template = template.replace(match, handleConditional(match));
|
|
|
|
});
|
|
|
|
|
|
|
|
// Find "variable" tokens
|
|
|
|
let prettyValue = value;
|
|
|
|
if (typeof prettyValue === "number") {
|
|
|
|
prettyValue = prettyValue.toLocaleString(locale);
|
|
|
|
}
|
|
|
|
template = template.replaceAll(`{{ ${key} }}`, prettyValue);
|
|
|
|
});
|
|
|
|
return template;
|
|
|
|
};
|
|
|
|
|
2024-02-26 02:08:59 +00:00
|
|
|
const getLocalizedMatch = <T extends { language: string }>(
|
|
|
|
options: T[],
|
|
|
|
fallback: Omit<T, "language">
|
|
|
|
): Omit<T, "language"> & { language?: string } =>
|
|
|
|
options.find(({ language }) => language === locale) ??
|
|
|
|
options.find(({ language }) => language === defaultLocale) ?? {
|
|
|
|
...fallback,
|
|
|
|
};
|
|
|
|
|
|
|
|
const getLocalizedKey = (
|
|
|
|
keyType: KeysTypes,
|
|
|
|
keyId: string,
|
|
|
|
format: "short" | "default"
|
|
|
|
) => {
|
|
|
|
const category = cache.keys.find(
|
|
|
|
({ id, type }) => id === keyId && type === keyType
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!category) {
|
|
|
|
return "UNKNOWN";
|
|
|
|
}
|
|
|
|
if (!category.translations) {
|
|
|
|
return category.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
const translation = getLocalizedMatch(category.translations, {
|
|
|
|
name: category.name,
|
|
|
|
short: category.name,
|
|
|
|
});
|
|
|
|
|
|
|
|
return format === "default" ? translation.name : translation.short;
|
|
|
|
};
|
|
|
|
|
2024-01-29 04:02:58 +00:00
|
|
|
return {
|
2024-01-29 22:37:55 +00:00
|
|
|
t: (key: WordingKeys, values: Record<string, any> = {}): string => {
|
2024-02-01 20:00:55 +00:00
|
|
|
if (translations && key in translations) {
|
2024-01-29 22:37:55 +00:00
|
|
|
return formatWithValues(key, translations[key]!, values);
|
2024-01-29 04:02:58 +00:00
|
|
|
}
|
2024-01-29 22:37:55 +00:00
|
|
|
return `«${key}»`;
|
2024-01-29 04:02:58 +00:00
|
|
|
},
|
2024-01-29 22:37:55 +00:00
|
|
|
getLocalizedUrl: (url: string): string => `/${locale}${url}`,
|
2024-02-26 02:08:59 +00:00
|
|
|
getLocalizedMatch,
|
|
|
|
formatCategory: (
|
|
|
|
id: string,
|
|
|
|
format: "short" | "default" = "default"
|
|
|
|
): string => getLocalizedKey(KeysTypes.Categories, id, format),
|
|
|
|
formatContentType: (id: string): string =>
|
|
|
|
getLocalizedKey(KeysTypes.Contents, id, "default"),
|
|
|
|
formatRecorder: (recorderId: string): string => {
|
|
|
|
const result = cache.recorders.find(({ id }) => id === recorderId);
|
|
|
|
|
|
|
|
if (!result) {
|
|
|
|
return "UNKNOWN";
|
|
|
|
}
|
|
|
|
|
|
|
|
return result.username;
|
|
|
|
},
|
|
|
|
formatLocale: (code: string): string =>
|
|
|
|
cache.locales.find(({ id }) => id === code)?.name ?? code,
|
2024-01-29 04:02:58 +00:00
|
|
|
};
|
|
|
|
};
|
2024-01-29 22:37:55 +00:00
|
|
|
|
|
|
|
const limitMatchToBalanceCurlyBraces = (
|
|
|
|
matchArray: RegExpMatchArray
|
|
|
|
): string => {
|
|
|
|
// Cut match as soon as curly braces are balanced.
|
|
|
|
const match = matchArray[0];
|
|
|
|
let curlyCount = 2;
|
|
|
|
let index = 2;
|
|
|
|
while (index < match.length && curlyCount > 0) {
|
|
|
|
if (match[index] === "{") {
|
|
|
|
curlyCount++;
|
|
|
|
}
|
|
|
|
if (match[index] === "}") {
|
|
|
|
curlyCount--;
|
|
|
|
}
|
|
|
|
index++;
|
|
|
|
}
|
|
|
|
return match.substring(0, index);
|
|
|
|
};
|
2024-02-01 20:00:55 +00:00
|
|
|
|
2024-02-17 22:11:17 +00:00
|
|
|
export type Locale = string;
|
2024-02-01 20:00:55 +00:00
|
|
|
export const defaultLocale: Locale = "en";
|
|
|
|
|
|
|
|
export const getCurrentLocale = (pathname: string): Locale | undefined => {
|
2024-02-17 22:11:17 +00:00
|
|
|
for (const locale of cache.locales) {
|
2024-02-26 02:08:59 +00:00
|
|
|
if (pathname.startsWith(`/${locale.id}`)) {
|
|
|
|
return locale.id;
|
2024-02-01 20:00:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
};
|
|
|
|
|
2024-02-17 22:11:17 +00:00
|
|
|
export const getBestAcceptedLanguage = (
|
|
|
|
request: Request
|
|
|
|
): Locale | undefined => {
|
2024-02-26 02:08:59 +00:00
|
|
|
acceptLanguage.languages(cache.locales.map(({ id }) => id));
|
2024-02-01 20:00:55 +00:00
|
|
|
|
2024-02-17 22:11:17 +00:00
|
|
|
return (
|
|
|
|
(acceptLanguage.get(
|
|
|
|
request.headers.get("Accept-Language")
|
|
|
|
) as Locale | null) ?? undefined
|
|
|
|
);
|
2024-02-01 20:00:55 +00:00
|
|
|
};
|