Start implementing per theme variant translations and ability to add extra languages
This commit is contained in:
parent
e15f13646c
commit
aad89a2001
@ -1,6 +1,6 @@
|
||||
import type { GenericI18n_noJsx } from "./i18n";
|
||||
|
||||
export type GenericI18n<MessageKey extends string> = GenericI18n_noJsx<MessageKey> & {
|
||||
export type GenericI18n<MessageKey extends string, LanguageTag extends string> = GenericI18n_noJsx<MessageKey, LanguageTag> & {
|
||||
msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element;
|
||||
advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element;
|
||||
};
|
||||
|
@ -5,8 +5,12 @@ import { fetchMessages_defaultSet } from "./messages_defaultSet";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import { FALLBACK_LANGUAGE_TAG } from "keycloakify/bin/shared/constants";
|
||||
import { id } from "tsafe/id";
|
||||
import { is } from "tsafe/is";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
import type { LanguageTag as LanguageTag_defaultSet } from "keycloakify/login/i18n/messages_defaultSet/LanguageTag";
|
||||
|
||||
export type KcContextLike = {
|
||||
themeName: string;
|
||||
locale?: {
|
||||
currentLanguageTag: string;
|
||||
supported: { languageTag: string; url: string; label: string }[];
|
||||
@ -18,18 +22,18 @@ export type KcContextLike = {
|
||||
|
||||
assert<KcContext extends KcContextLike ? true : false>();
|
||||
|
||||
export type GenericI18n_noJsx<MessageKey extends string> = {
|
||||
export type GenericI18n_noJsx<MessageKey extends string, LanguageTag extends string> = {
|
||||
/**
|
||||
* e.g: "en", "fr", "zh-CN"
|
||||
*
|
||||
* The current language
|
||||
*/
|
||||
currentLanguageTag: string;
|
||||
currentLanguageTag: LanguageTag;
|
||||
/**
|
||||
* Redirect to this url to change the language.
|
||||
* After reload currentLanguageTag === newLanguageTag
|
||||
*/
|
||||
getChangeLocaleUrl: (newLanguageTag: string) => string;
|
||||
getChangeLocaleUrl: (newLanguageTag: LanguageTag) => string;
|
||||
/**
|
||||
* e.g. "en" => "English", "fr" => "Français", ...
|
||||
*
|
||||
@ -81,10 +85,35 @@ export type GenericI18n_noJsx<MessageKey extends string> = {
|
||||
|
||||
export type MessageKey_defaultSet = keyof typeof messages_defaultSet_fallbackLanguage;
|
||||
|
||||
export function createGetI18n<MessageKey_themeDefined extends string = never>(messagesByLanguageTag_themeDefined: {
|
||||
[languageTag: string]: { [key in MessageKey_themeDefined]: string };
|
||||
}) {
|
||||
type I18n = GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined>;
|
||||
export type ReturnTypeOfCreateGetI18n<MessageKey_themeDefined extends string, LanguageTag_notInDefaultSet extends string> = {
|
||||
getI18n: (params: { kcContext: KcContextLike }) => {
|
||||
i18n: GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined, LanguageTag_defaultSet | LanguageTag_notInDefaultSet>;
|
||||
prI18n_currentLanguage:
|
||||
| Promise<GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined, LanguageTag_defaultSet | LanguageTag_notInDefaultSet>>
|
||||
| undefined;
|
||||
};
|
||||
ofTypeI18n: GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined, LanguageTag_defaultSet | LanguageTag_notInDefaultSet>;
|
||||
};
|
||||
|
||||
export function createGetI18n<
|
||||
ThemeName extends string = string,
|
||||
MessageKey_themeDefined extends string = never,
|
||||
LanguageTag_notInDefaultSet extends string = never
|
||||
>(params: {
|
||||
extraLanguageTranslations: {
|
||||
[languageTag in LanguageTag_notInDefaultSet]: () => Promise<{ default: Record<MessageKey_defaultSet, string> }>;
|
||||
};
|
||||
messagesByLanguageTag_themeDefined: Partial<{
|
||||
[languageTag in LanguageTag_defaultSet | LanguageTag_notInDefaultSet]: {
|
||||
[key in MessageKey_themeDefined]: string | Record<ThemeName, string>;
|
||||
};
|
||||
}>;
|
||||
}): ReturnTypeOfCreateGetI18n<MessageKey_themeDefined, LanguageTag_notInDefaultSet> {
|
||||
const { extraLanguageTranslations, messagesByLanguageTag_themeDefined } = params;
|
||||
|
||||
type LanguageTag = LanguageTag_defaultSet | LanguageTag_notInDefaultSet;
|
||||
|
||||
type I18n = GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined, LanguageTag>;
|
||||
|
||||
type Result = { i18n: I18n; prI18n_currentLanguage: Promise<I18n> | undefined };
|
||||
|
||||
@ -104,7 +133,7 @@ export function createGetI18n<MessageKey_themeDefined extends string = never>(me
|
||||
}
|
||||
|
||||
const partialI18n: Pick<I18n, "currentLanguageTag" | "getChangeLocaleUrl" | "labelBySupportedLanguageTag"> = {
|
||||
currentLanguageTag: kcContext.locale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG,
|
||||
currentLanguageTag: kcContext.locale?.currentLanguageTag ?? (FALLBACK_LANGUAGE_TAG as any),
|
||||
getChangeLocaleUrl: newLanguageTag => {
|
||||
const { locale } = kcContext;
|
||||
|
||||
@ -120,15 +149,16 @@ export function createGetI18n<MessageKey_themeDefined extends string = never>(me
|
||||
};
|
||||
|
||||
const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory<MessageKey_themeDefined>({
|
||||
themeName: kcContext.themeName,
|
||||
messages_themeDefined:
|
||||
messagesByLanguageTag_themeDefined[partialI18n.currentLanguageTag] ??
|
||||
messagesByLanguageTag_themeDefined[FALLBACK_LANGUAGE_TAG] ??
|
||||
messagesByLanguageTag_themeDefined[FALLBACK_LANGUAGE_TAG as LanguageTag] ??
|
||||
(() => {
|
||||
const firstLanguageTag = Object.keys(messagesByLanguageTag_themeDefined)[0];
|
||||
if (firstLanguageTag === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return messagesByLanguageTag_themeDefined[firstLanguageTag];
|
||||
return messagesByLanguageTag_themeDefined[firstLanguageTag as LanguageTag];
|
||||
})(),
|
||||
messages_fromKcServer: kcContext["x-keycloakify"].messages
|
||||
});
|
||||
@ -146,7 +176,29 @@ export function createGetI18n<MessageKey_themeDefined extends string = never>(me
|
||||
prI18n_currentLanguage: isCurrentLanguageFallbackLanguage
|
||||
? undefined
|
||||
: (async () => {
|
||||
const messages_defaultSet_currentLanguage = await fetchMessages_defaultSet(partialI18n.currentLanguageTag);
|
||||
const messages_defaultSet_currentLanguage = await (async () => {
|
||||
const currentLanguageTag = partialI18n.currentLanguageTag;
|
||||
|
||||
const fromDefaultSet = await fetchMessages_defaultSet(currentLanguageTag);
|
||||
|
||||
const isEmpty = (() => {
|
||||
for (let _key in fromDefaultSet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})();
|
||||
|
||||
if (isEmpty) {
|
||||
assert(is<Exclude<LanguageTag, LanguageTag_defaultSet>>(currentLanguageTag));
|
||||
|
||||
const asyncFunction = extraLanguageTranslations[currentLanguageTag];
|
||||
|
||||
assert(asyncFunction !== undefined);
|
||||
|
||||
return asyncFunction().then(({ default: messages }) => messages);
|
||||
}
|
||||
})();
|
||||
|
||||
const i18n_currentLanguage: I18n = {
|
||||
...partialI18n,
|
||||
@ -170,18 +222,22 @@ export function createGetI18n<MessageKey_themeDefined extends string = never>(me
|
||||
return result;
|
||||
}
|
||||
|
||||
return { getI18n };
|
||||
return {
|
||||
getI18n,
|
||||
ofTypeI18n: Reflect<I18n>()
|
||||
};
|
||||
}
|
||||
|
||||
function createI18nTranslationFunctionsFactory<MessageKey_themeDefined extends string>(params: {
|
||||
messages_themeDefined: Record<MessageKey_themeDefined, string> | undefined;
|
||||
themeName: string;
|
||||
messages_themeDefined: Record<MessageKey_themeDefined, string | Record<string, string>> | undefined;
|
||||
messages_fromKcServer: Record<string, string>;
|
||||
}) {
|
||||
const { messages_themeDefined, messages_fromKcServer } = params;
|
||||
const { themeName, messages_themeDefined, messages_fromKcServer } = params;
|
||||
|
||||
function createI18nTranslationFunctions(params: {
|
||||
messages_defaultSet_currentLanguage: Partial<Record<MessageKey_defaultSet, string>> | undefined;
|
||||
}): Pick<GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined>, "msgStr" | "advancedMsgStr"> {
|
||||
}): Pick<GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined, string>, "msgStr" | "advancedMsgStr"> {
|
||||
const { messages_defaultSet_currentLanguage } = params;
|
||||
|
||||
function resolveMsg(props: { key: string; args: (string | undefined)[] }): string | undefined {
|
||||
@ -189,7 +245,23 @@ function createI18nTranslationFunctionsFactory<MessageKey_themeDefined extends s
|
||||
|
||||
const message =
|
||||
id<Record<string, string | undefined>>(messages_fromKcServer)[key] ??
|
||||
id<Record<string, string | undefined> | undefined>(messages_themeDefined)?.[key] ??
|
||||
(() => {
|
||||
const messageOrMap = id<Record<string, string | Record<string, string> | undefined> | undefined>(messages_themeDefined)?.[key];
|
||||
|
||||
if (messageOrMap === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof messageOrMap === "string") {
|
||||
return messageOrMap;
|
||||
}
|
||||
|
||||
const message = messageOrMap[themeName];
|
||||
|
||||
assert(message !== undefined, `No translation for theme variant "${themeName}" for key "${key}"`);
|
||||
|
||||
return message;
|
||||
})() ??
|
||||
id<Record<string, string | undefined> | undefined>(messages_defaultSet_currentLanguage)?.[key] ??
|
||||
id<Record<string, string | undefined>>(messages_defaultSet_fallbackLanguage)[key];
|
||||
|
||||
|
@ -2,4 +2,4 @@ import type { GenericI18n } from "./GenericI18n";
|
||||
import type { MessageKey_defaultSet, KcContextLike } from "./i18n";
|
||||
export type { MessageKey_defaultSet, KcContextLike };
|
||||
export type I18n = GenericI18n<MessageKey_defaultSet>;
|
||||
export { createUseI18n } from "./useI18n";
|
||||
export { createUseI18n, i18nApi } from "./useI18n";
|
||||
|
130
src/login/i18n/pinApi.ts
Normal file
130
src/login/i18n/pinApi.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import type { LanguageTag as LanguageTag_defaultSet } from "keycloakify/login/i18n/messages_defaultSet/LanguageTag";
|
||||
import {
|
||||
type MessageKey_defaultSet,
|
||||
type ReturnTypeOfCreateGetI18n,
|
||||
createGetI18n
|
||||
} from "./i18n";
|
||||
|
||||
export type I18nInitializer<
|
||||
ThemeName extends string = never,
|
||||
MessageKey_themeDefined extends string = never,
|
||||
LanguageTag_notInDefaultSet extends string = never,
|
||||
ExcludedMethod extends
|
||||
| "withThemeName"
|
||||
| "withExtraLanguages"
|
||||
| "withCustomTranslations" = never
|
||||
> = Omit<
|
||||
{
|
||||
withThemeName: <ThemeName extends string>() => I18nInitializer<
|
||||
ThemeName,
|
||||
MessageKey_themeDefined,
|
||||
LanguageTag_notInDefaultSet,
|
||||
ExcludedMethod | "withThemeName"
|
||||
>;
|
||||
withExtraLanguages: <
|
||||
LanguageTag_notInDefaultSet extends string
|
||||
>(extraLanguageTranslations: {
|
||||
[LanguageTag in LanguageTag_notInDefaultSet]: () => Promise<
|
||||
Record<MessageKey_defaultSet, string>
|
||||
>;
|
||||
}) => I18nInitializer<
|
||||
ThemeName,
|
||||
MessageKey_themeDefined,
|
||||
LanguageTag_notInDefaultSet,
|
||||
ExcludedMethod | "withExtraLanguages"
|
||||
>;
|
||||
withCustomTranslations: <MessageKey_themeDefined extends string>(
|
||||
messagesByLanguageTag_themeDefined: Partial<{
|
||||
[LanguageTag in
|
||||
| LanguageTag_defaultSet
|
||||
| LanguageTag_notInDefaultSet]: Record<
|
||||
MessageKey_themeDefined,
|
||||
string | Record<ThemeName, string>
|
||||
>;
|
||||
}>
|
||||
) => I18nInitializer<
|
||||
ThemeName,
|
||||
MessageKey_themeDefined,
|
||||
LanguageTag_notInDefaultSet,
|
||||
ExcludedMethod | "withCustomTranslations"
|
||||
>;
|
||||
create: () => ReturnTypeOfCreateGetI18n<
|
||||
MessageKey_themeDefined,
|
||||
LanguageTag_notInDefaultSet
|
||||
>;
|
||||
},
|
||||
ExcludedMethod
|
||||
>;
|
||||
|
||||
function createI18nInitializer<
|
||||
ThemeName extends string = never,
|
||||
MessageKey_themeDefined extends string = never,
|
||||
LanguageTag_notInDefaultSet extends string = never
|
||||
>(params: {
|
||||
extraLanguageTranslations: {
|
||||
[LanguageTag in LanguageTag_notInDefaultSet]: () => Promise<
|
||||
Record<MessageKey_defaultSet, string>
|
||||
>;
|
||||
};
|
||||
messagesByLanguageTag_themeDefined: Partial<{
|
||||
[LanguageTag in LanguageTag_defaultSet | LanguageTag_notInDefaultSet]: Record<
|
||||
MessageKey_themeDefined,
|
||||
string | Record<ThemeName, string>
|
||||
>;
|
||||
}>;
|
||||
}): I18nInitializer<ThemeName, MessageKey_themeDefined, LanguageTag_notInDefaultSet> {
|
||||
const i18nInitializer: I18nInitializer<
|
||||
ThemeName,
|
||||
MessageKey_themeDefined,
|
||||
LanguageTag_notInDefaultSet
|
||||
> = {
|
||||
withThemeName: () =>
|
||||
createI18nInitializer({
|
||||
extraLanguageTranslations: params.extraLanguageTranslations,
|
||||
messagesByLanguageTag_themeDefined:
|
||||
params.messagesByLanguageTag_themeDefined as any
|
||||
}),
|
||||
withExtraLanguages: extraLanguageTranslations =>
|
||||
createI18nInitializer({
|
||||
extraLanguageTranslations,
|
||||
messagesByLanguageTag_themeDefined:
|
||||
params.messagesByLanguageTag_themeDefined as any
|
||||
}),
|
||||
withCustomTranslations: messagesByLanguageTag_themeDefined =>
|
||||
createI18nInitializer({
|
||||
extraLanguageTranslations: params.extraLanguageTranslations,
|
||||
messagesByLanguageTag_themeDefined
|
||||
}),
|
||||
create: () =>
|
||||
createGetI18n({
|
||||
extraLanguageTranslations: params.extraLanguageTranslations,
|
||||
messagesByLanguageTag_themeDefined:
|
||||
params.messagesByLanguageTag_themeDefined
|
||||
})
|
||||
};
|
||||
|
||||
return i18nInitializer;
|
||||
}
|
||||
|
||||
export const i18nInitializer = createI18nInitializer({});
|
||||
|
||||
const i18n = i18nInitializer
|
||||
.withThemeName<"my-theme-1" | "my-theme-2">()
|
||||
.withExtraLanguages({
|
||||
xx: async () => ({}) as any
|
||||
})
|
||||
.withCustomTranslations({
|
||||
en: {
|
||||
myCustomKey: {
|
||||
"my-theme-1": "my-theme-1-en",
|
||||
"my-theme-2": "my-theme-2-en"
|
||||
}
|
||||
},
|
||||
xx: {
|
||||
myCustomKey: {
|
||||
"my-theme-1": "my-theme-1-xx",
|
||||
"my-theme-2": "my-theme-2-xx"
|
||||
}
|
||||
}
|
||||
})
|
||||
.create();
|
@ -2,16 +2,33 @@ import { useEffect, useState } from "react";
|
||||
import { createGetI18n, type GenericI18n_noJsx, type KcContextLike, type MessageKey_defaultSet } from "./i18n";
|
||||
import { GenericI18n } from "./GenericI18n";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
import type { LanguageTag as LanguageTag_defaultSet } from "keycloakify/login/i18n/messages_defaultSet/LanguageTag";
|
||||
|
||||
export function createUseI18n<MessageKey_themeDefined extends string = never>(messagesByLanguageTag: {
|
||||
[languageTag: string]: { [key in MessageKey_themeDefined]: string };
|
||||
export const i18nApi = {
|
||||
withThemeName: <ThemeName extends string>() => ({
|
||||
withTranslations: <MessageKey_themeDefined extends string = never>(messagesByLanguageTag: {
|
||||
[languageTag: string]: { [key in MessageKey_themeDefined]: string | Record<ThemeName, string> };
|
||||
}) => ({
|
||||
create: () => createUseI18n<MessageKey_themeDefined, ThemeName>(messagesByLanguageTag)
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
export function createUseI18n<
|
||||
MessageKey_themeDefined extends string = never,
|
||||
ThemeName extends string = string,
|
||||
LanguageTag extends string = LanguageTag_defaultSet
|
||||
>(params: {
|
||||
messagesByLanguageTag: {
|
||||
[languageTag: string]: { [key in MessageKey_themeDefined]: string | Record<ThemeName, string> };
|
||||
};
|
||||
}) {
|
||||
type MessageKey = MessageKey_defaultSet | MessageKey_themeDefined;
|
||||
|
||||
type I18n = GenericI18n<MessageKey>;
|
||||
type I18n = GenericI18n<MessageKey, LanguageTag>;
|
||||
|
||||
const { withJsx } = (() => {
|
||||
const cache = new WeakMap<GenericI18n_noJsx<MessageKey>, GenericI18n<MessageKey>>();
|
||||
const cache = new WeakMap<GenericI18n_noJsx<MessageKey, LanguageTag>, GenericI18n<MessageKey, LanguageTag>>();
|
||||
|
||||
function renderHtmlString(params: { htmlString: string; msgKey: string }): JSX.Element {
|
||||
const { htmlString, msgKey } = params;
|
||||
@ -25,7 +42,7 @@ export function createUseI18n<MessageKey_themeDefined extends string = never>(me
|
||||
);
|
||||
}
|
||||
|
||||
function withJsx(i18n_noJsx: GenericI18n_noJsx<MessageKey>): I18n {
|
||||
function withJsx(i18n_noJsx: GenericI18n_noJsx<MessageKey, LanguageTag>): I18n {
|
||||
use_cache: {
|
||||
const i18n = cache.get(i18n_noJsx);
|
||||
|
||||
@ -63,7 +80,7 @@ export function createUseI18n<MessageKey_themeDefined extends string = never>(me
|
||||
(styleElement.textContent = `[data-kc-msg] { display: inline-block; }`), document.head.prepend(styleElement);
|
||||
}
|
||||
|
||||
const { getI18n } = createGetI18n(messagesByLanguageTag);
|
||||
const { getI18n } = createGetI18n({ messagesByLanguageTag, extraLanguageTranslations });
|
||||
|
||||
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } {
|
||||
const { kcContext } = params;
|
||||
|
@ -1,3 +1,3 @@
|
||||
export type { ExtendKcContext, Attribute } from "keycloakify/login/KcContext";
|
||||
export type { ClassKey } from "keycloakify/login/TemplateProps";
|
||||
export { createUseI18n } from "keycloakify/login/i18n";
|
||||
export { createUseI18n, i18nApi } from "keycloakify/login/i18n";
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { createUseI18n } from "../../dist/login";
|
||||
import { i18nApi } from "../../dist/login";
|
||||
import type { ThemeName } from "../kc.gen";
|
||||
|
||||
export const { useI18n, ofTypeI18n } = createUseI18n({});
|
||||
export const { useI18n, ofTypeI18n } = i18nApi
|
||||
.withThemeName<ThemeName>()
|
||||
.withTranslations({})
|
||||
.create();
|
||||
|
||||
export type I18n = typeof ofTypeI18n;
|
||||
|
Loading…
x
Reference in New Issue
Block a user