Make the i18n API more type safe
This commit is contained in:
@ -1,8 +1,5 @@
|
||||
export * from "./withJsx";
|
||||
import type { GenericI18n } from "./withJsx/GenericI18n";
|
||||
import type {
|
||||
LanguageTag as LanguageTag_defaultSet,
|
||||
MessageKey as MessageKey_defaultSet
|
||||
} from "./messages_defaultSet/types";
|
||||
import type { MessageKey as MessageKey_defaultSet } from "./messages_defaultSet/types";
|
||||
/** INTERNAL: DO NOT IMPORT THIS */
|
||||
export type I18n = GenericI18n<MessageKey_defaultSet, LanguageTag_defaultSet>;
|
||||
export type I18n = GenericI18n<MessageKey_defaultSet, string>;
|
||||
|
@ -1,22 +1,26 @@
|
||||
export type GenericI18n_noJsx<MessageKey extends string, LanguageTag extends string> = {
|
||||
currentLanguage: {
|
||||
/**
|
||||
* e.g: "en", "fr", "zh-CN"
|
||||
*
|
||||
* The current language
|
||||
*/
|
||||
languageTag: LanguageTag;
|
||||
/**
|
||||
* e.g: "English", "Français", "中文(简体)"
|
||||
*
|
||||
* The current language
|
||||
*/
|
||||
label: string;
|
||||
};
|
||||
/**
|
||||
* e.g: "en", "fr", "zh-CN"
|
||||
*
|
||||
* The current language
|
||||
* Array of languages enabled on the realm.
|
||||
*/
|
||||
currentLanguageTag: LanguageTag;
|
||||
/**
|
||||
* Redirect to this url to change the language.
|
||||
* After reload currentLanguageTag === newLanguageTag
|
||||
*/
|
||||
getChangeLocaleUrl: (newLanguageTag: string /*LanguageTag*/) => string;
|
||||
/**
|
||||
* e.g. "en" => "English", "fr" => "Français", ...
|
||||
*
|
||||
* Used to render a select that enable user to switch language.
|
||||
* ex: https://user-images.githubusercontent.com/6702424/186044799-38801eec-4e89-483b-81dd-8e9233e8c0eb.png
|
||||
* */
|
||||
labelBySupportedLanguageTag: Record<string /*LanguageTag*/, string>;
|
||||
enabledLanguages: {
|
||||
languageTag: LanguageTag;
|
||||
label: string;
|
||||
href: string;
|
||||
}[];
|
||||
/**
|
||||
*
|
||||
* Examples assuming currentLanguageTag === "en"
|
||||
|
@ -90,64 +90,89 @@ export function createGetI18n<
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const partialI18n: Pick<I18n, "currentLanguageTag" | "getChangeLocaleUrl" | "labelBySupportedLanguageTag"> = {
|
||||
currentLanguageTag: kcContext.locale?.currentLanguageTag ?? (FALLBACK_LANGUAGE_TAG as any),
|
||||
getChangeLocaleUrl: newLanguageTag => {
|
||||
const { locale } = kcContext;
|
||||
{
|
||||
const currentLanguageTag = kcContext.locale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG;
|
||||
const html = document.querySelector("html");
|
||||
assert(html !== null);
|
||||
html.lang = currentLanguageTag;
|
||||
}
|
||||
|
||||
assert(locale !== undefined, "Internationalization not enabled");
|
||||
const getLanguageLabel = (languageTag: LanguageTag) => {
|
||||
form_user_added_languages: {
|
||||
if (!(languageTag in extraLanguageTranslations)) {
|
||||
break form_user_added_languages;
|
||||
}
|
||||
assert(is<Exclude<LanguageTag, LanguageTag_defaultSet>>(languageTag));
|
||||
|
||||
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
|
||||
const entry = extraLanguageTranslations[languageTag];
|
||||
|
||||
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
|
||||
return entry.label;
|
||||
}
|
||||
|
||||
return targetSupportedLocale.url;
|
||||
},
|
||||
labelBySupportedLanguageTag: (() => {
|
||||
const labelBySupportedLanguageTag = Object.fromEntries(
|
||||
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
|
||||
);
|
||||
|
||||
// NOTE: For IE11
|
||||
if (typeof Proxy === undefined) {
|
||||
return labelBySupportedLanguageTag;
|
||||
from_server: {
|
||||
if (kcContext.locale === undefined) {
|
||||
break from_server;
|
||||
}
|
||||
|
||||
// NOTE: This is for convenience in Storybook
|
||||
return new Proxy<Record<string, string>>(
|
||||
{},
|
||||
{
|
||||
get: function (...args) {
|
||||
const [, languageTag] = args;
|
||||
const supportedEntry = kcContext.locale.supported.find(entry => entry.languageTag === languageTag);
|
||||
|
||||
if (typeof languageTag !== "string") {
|
||||
return window.Reflect.get(...args);
|
||||
}
|
||||
if (supportedEntry === undefined) {
|
||||
break from_server;
|
||||
}
|
||||
|
||||
let label = labelBySupportedLanguageTag[languageTag];
|
||||
// cspell: disable-next-line
|
||||
// from "Espagnol (Español)" we want to extract "Español"
|
||||
const match = supportedEntry.label.match(/[^(]+\(([^)]+)\)/);
|
||||
|
||||
if (label === undefined || label === "" || label === languageTag) {
|
||||
assert(is<Exclude<LanguageTag, LanguageTag_defaultSet>>(languageTag));
|
||||
if (match !== null) {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
const entry = extraLanguageTranslations[languageTag];
|
||||
return supportedEntry.label;
|
||||
}
|
||||
|
||||
assert(entry !== undefined);
|
||||
|
||||
label = entry.label;
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
}
|
||||
);
|
||||
})()
|
||||
// NOTE: This should never happen
|
||||
return languageTag;
|
||||
};
|
||||
|
||||
const currentLanguage: I18n["currentLanguage"] = (() => {
|
||||
const languageTag = id<string>(kcContext.locale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG) as LanguageTag;
|
||||
|
||||
return {
|
||||
languageTag,
|
||||
label: getLanguageLabel(languageTag)
|
||||
};
|
||||
})();
|
||||
|
||||
const enabledLanguages: I18n["enabledLanguages"] = (() => {
|
||||
const enabledLanguages: I18n["enabledLanguages"] = [];
|
||||
|
||||
if (kcContext.locale !== undefined) {
|
||||
for (const { languageTag, label, url } of kcContext.locale.supported ?? []) {
|
||||
enabledLanguages.push({
|
||||
languageTag: id<string>(languageTag) as LanguageTag,
|
||||
label,
|
||||
href: url
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (enabledLanguages.find(({ languageTag }) => languageTag === currentLanguage.languageTag) === undefined) {
|
||||
enabledLanguages.push({
|
||||
languageTag: currentLanguage.languageTag,
|
||||
label: getLanguageLabel(currentLanguage.languageTag),
|
||||
href: "#"
|
||||
});
|
||||
}
|
||||
|
||||
return enabledLanguages;
|
||||
})();
|
||||
|
||||
const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory<MessageKey_themeDefined>({
|
||||
themeName: kcContext.themeName,
|
||||
messages_themeDefined:
|
||||
messagesByLanguageTag_themeDefined[partialI18n.currentLanguageTag] ??
|
||||
messagesByLanguageTag_themeDefined[FALLBACK_LANGUAGE_TAG as LanguageTag] ??
|
||||
messagesByLanguageTag_themeDefined[currentLanguage.languageTag] ??
|
||||
messagesByLanguageTag_themeDefined[id<string>(FALLBACK_LANGUAGE_TAG) as LanguageTag] ??
|
||||
(() => {
|
||||
const firstLanguageTag = Object.keys(messagesByLanguageTag_themeDefined)[0];
|
||||
if (firstLanguageTag === undefined) {
|
||||
@ -158,11 +183,12 @@ export function createGetI18n<
|
||||
messages_fromKcServer: kcContext["x-keycloakify"].messages
|
||||
});
|
||||
|
||||
const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag === FALLBACK_LANGUAGE_TAG;
|
||||
const isCurrentLanguageFallbackLanguage = currentLanguage.languageTag === FALLBACK_LANGUAGE_TAG;
|
||||
|
||||
const result: Result = {
|
||||
i18n: {
|
||||
...partialI18n,
|
||||
currentLanguage,
|
||||
enabledLanguages,
|
||||
...createI18nTranslationFunctions({
|
||||
messages_defaultSet_currentLanguage: isCurrentLanguageFallbackLanguage ? messages_defaultSet_fallbackLanguage : undefined
|
||||
}),
|
||||
@ -172,7 +198,7 @@ export function createGetI18n<
|
||||
? undefined
|
||||
: (async () => {
|
||||
const messages_defaultSet_currentLanguage = await (async () => {
|
||||
const currentLanguageTag = partialI18n.currentLanguageTag;
|
||||
const currentLanguageTag = currentLanguage.languageTag;
|
||||
|
||||
const fromDefaultSet = await fetchMessages_defaultSet(currentLanguageTag);
|
||||
|
||||
@ -198,7 +224,8 @@ export function createGetI18n<
|
||||
})();
|
||||
|
||||
const i18n_currentLanguage: I18n = {
|
||||
...partialI18n,
|
||||
currentLanguage,
|
||||
enabledLanguages,
|
||||
...createI18nTranslationFunctions({ messages_defaultSet_currentLanguage }),
|
||||
isFetchingTranslations: false
|
||||
};
|
||||
|
@ -2,24 +2,28 @@ import type { GenericI18n_noJsx } from "../noJsx/GenericI18n_noJsx";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
|
||||
export type GenericI18n<MessageKey extends string, LanguageTag extends string> = {
|
||||
currentLanguage: {
|
||||
/**
|
||||
* e.g: "en", "fr", "zh-CN"
|
||||
*
|
||||
* The current language
|
||||
*/
|
||||
languageTag: LanguageTag;
|
||||
/**
|
||||
* e.g: "English", "Français", "中文(简体)"
|
||||
*
|
||||
* The current language
|
||||
*/
|
||||
label: string;
|
||||
};
|
||||
/**
|
||||
* e.g: "en", "fr", "zh-CN"
|
||||
*
|
||||
* The current language
|
||||
* Array of languages enabled on the realm.
|
||||
*/
|
||||
currentLanguageTag: LanguageTag;
|
||||
/**
|
||||
* Redirect to this url to change the language.
|
||||
* After reload currentLanguageTag === newLanguageTag
|
||||
*/
|
||||
getChangeLocaleUrl: (newLanguageTag: string /*LanguageTag*/) => string;
|
||||
/**
|
||||
* e.g. "en" => "English", "fr" => "Français", ...
|
||||
*
|
||||
* Used to render a select that enable user to switch language.
|
||||
* ex: https://user-images.githubusercontent.com/6702424/186044799-38801eec-4e89-483b-81dd-8e9233e8c0eb.png
|
||||
* */
|
||||
labelBySupportedLanguageTag: Record<string /*LanguageTag*/, string>;
|
||||
enabledLanguages: {
|
||||
languageTag: LanguageTag;
|
||||
label: string;
|
||||
href: string;
|
||||
}[];
|
||||
/**
|
||||
*
|
||||
* Examples assuming currentLanguageTag === "en"
|
||||
|
Reference in New Issue
Block a user