keycloakify 10.1.4 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/account/KcContext/kcContextMocks.js +18 -6
- package/account/KcContext/kcContextMocks.js.map +1 -1
- package/account/Template.js +8 -26
- package/account/Template.js.map +1 -1
- package/account/Template.useInitialize.d.ts +12 -0
- package/account/Template.useInitialize.js +20 -0
- package/account/Template.useInitialize.js.map +1 -0
- package/account/i18n/index.d.ts +5 -5
- package/account/i18n/index.js +1 -1
- package/account/i18n/index.js.map +1 -1
- package/account/i18n/messages_defaultSet/types.d.ts +3 -0
- package/account/i18n/messages_defaultSet/types.js +26 -0
- package/account/i18n/messages_defaultSet/types.js.map +1 -0
- package/account/i18n/noJsx/GenericI18n_noJsx.d.ts +63 -0
- package/account/i18n/noJsx/GenericI18n_noJsx.js +2 -0
- package/account/i18n/noJsx/GenericI18n_noJsx.js.map +1 -0
- package/account/i18n/noJsx/getI18n.d.ts +41 -0
- package/account/i18n/noJsx/getI18n.js +207 -0
- package/account/i18n/noJsx/getI18n.js.map +1 -0
- package/account/i18n/noJsx/i18nBuilder.d.ts +18 -0
- package/account/i18n/noJsx/i18nBuilder.js +27 -0
- package/account/i18n/noJsx/i18nBuilder.js.map +1 -0
- package/account/i18n/noJsx/index.d.ts +3 -0
- package/account/i18n/noJsx/index.js +2 -0
- package/account/i18n/noJsx/index.js.map +1 -0
- package/account/i18n/withJsx/GenericI18n.d.ts +72 -0
- package/account/i18n/withJsx/GenericI18n.js +5 -0
- package/account/i18n/withJsx/GenericI18n.js.map +1 -0
- package/account/i18n/withJsx/i18nBuilder.d.ts +18 -0
- package/account/i18n/withJsx/i18nBuilder.js +27 -0
- package/account/i18n/withJsx/i18nBuilder.js.map +1 -0
- package/account/i18n/withJsx/index.d.ts +3 -0
- package/account/i18n/withJsx/index.js +2 -0
- package/account/i18n/withJsx/index.js.map +1 -0
- package/account/i18n/withJsx/useI18n.d.ts +27 -0
- package/account/i18n/{useI18n.js → withJsx/useI18n.js} +6 -4
- package/account/i18n/withJsx/useI18n.js.map +1 -0
- package/account/index.d.ts +1 -1
- package/account/index.js +1 -1
- package/account/index.js.map +1 -1
- package/account/pages/Totp.js +3 -2
- package/account/pages/Totp.js.map +1 -1
- package/bin/246.index.js +191 -191
- package/bin/499.index.js +252 -68
- package/bin/main.js +1 -1
- package/lib/kcSanitize/HtmlPolicyBuilder.d.ts +28 -0
- package/lib/kcSanitize/HtmlPolicyBuilder.js +209 -0
- package/lib/kcSanitize/HtmlPolicyBuilder.js.map +1 -0
- package/lib/kcSanitize/KcSanitizer.d.ts +12 -0
- package/lib/kcSanitize/KcSanitizer.js +46 -0
- package/lib/kcSanitize/KcSanitizer.js.map +1 -0
- package/lib/kcSanitize/KcSanitizerPolicy.d.ts +24 -0
- package/lib/kcSanitize/KcSanitizerPolicy.js +149 -0
- package/lib/kcSanitize/KcSanitizerPolicy.js.map +1 -0
- package/lib/kcSanitize/index.d.ts +1 -0
- package/lib/kcSanitize/index.js +5 -0
- package/lib/kcSanitize/index.js.map +1 -0
- package/login/KcContext/kcContextMocks.js +24 -6
- package/login/KcContext/kcContextMocks.js.map +1 -1
- package/login/Template.js +7 -7
- package/login/Template.js.map +1 -1
- package/login/{Template.useStylesAndScripts.d.ts → Template.useInitialize.d.ts} +1 -4
- package/login/{Template.useStylesAndScripts.js → Template.useInitialize.js} +4 -15
- package/login/Template.useInitialize.js.map +1 -0
- package/login/i18n/index.d.ts +5 -5
- package/login/i18n/index.js +1 -1
- package/login/i18n/index.js.map +1 -1
- package/login/i18n/messages_defaultSet/types.d.ts +3 -0
- package/login/i18n/messages_defaultSet/types.js +33 -0
- package/login/i18n/messages_defaultSet/types.js.map +1 -0
- package/login/i18n/noJsx/GenericI18n_noJsx.d.ts +63 -0
- package/login/i18n/noJsx/GenericI18n_noJsx.js +2 -0
- package/login/i18n/noJsx/GenericI18n_noJsx.js.map +1 -0
- package/login/i18n/noJsx/getI18n.d.ts +41 -0
- package/login/i18n/noJsx/getI18n.js +207 -0
- package/login/i18n/noJsx/getI18n.js.map +1 -0
- package/login/i18n/noJsx/i18nBuilder.d.ts +18 -0
- package/login/i18n/noJsx/i18nBuilder.js +27 -0
- package/login/i18n/noJsx/i18nBuilder.js.map +1 -0
- package/login/i18n/noJsx/index.d.ts +3 -0
- package/login/i18n/noJsx/index.js +2 -0
- package/login/i18n/noJsx/index.js.map +1 -0
- package/login/i18n/withJsx/GenericI18n.d.ts +72 -0
- package/login/i18n/withJsx/GenericI18n.js +5 -0
- package/login/i18n/withJsx/GenericI18n.js.map +1 -0
- package/login/i18n/withJsx/i18nBuilder.d.ts +18 -0
- package/login/i18n/withJsx/i18nBuilder.js +27 -0
- package/login/i18n/withJsx/i18nBuilder.js.map +1 -0
- package/login/i18n/withJsx/index.d.ts +3 -0
- package/login/i18n/withJsx/index.js +2 -0
- package/login/i18n/withJsx/index.js.map +1 -0
- package/login/i18n/withJsx/useI18n.d.ts +27 -0
- package/login/i18n/{useI18n.js → withJsx/useI18n.js} +6 -4
- package/login/i18n/withJsx/useI18n.js.map +1 -0
- package/login/index.d.ts +1 -1
- package/login/index.js +1 -1
- package/login/index.js.map +1 -1
- package/login/lib/useUserProfileForm.d.ts +1 -1
- package/login/lib/useUserProfileForm.js +2 -1
- package/login/lib/useUserProfileForm.js.map +1 -1
- package/login/pages/Error.js +2 -1
- package/login/pages/Error.js.map +1 -1
- package/login/pages/Info.js +4 -3
- package/login/pages/Info.js.map +1 -1
- package/login/pages/Login.js +4 -3
- package/login/pages/Login.js.map +1 -1
- package/login/pages/LoginConfigTotp.js +3 -2
- package/login/pages/LoginConfigTotp.js.map +1 -1
- package/login/pages/LoginOtp.js +2 -1
- package/login/pages/LoginOtp.js.map +1 -1
- package/login/pages/LoginPassword.js +2 -1
- package/login/pages/LoginPassword.js.map +1 -1
- package/login/pages/LoginRecoveryAuthnCodeInput.js +2 -1
- package/login/pages/LoginRecoveryAuthnCodeInput.js.map +1 -1
- package/login/pages/LoginResetPassword.js +2 -1
- package/login/pages/LoginResetPassword.js.map +1 -1
- package/login/pages/LoginUpdatePassword.js +3 -2
- package/login/pages/LoginUpdatePassword.js.map +1 -1
- package/login/pages/Register.js +2 -1
- package/login/pages/Register.js.map +1 -1
- package/package.json +110 -31
- package/src/account/KcContext/kcContextMocks.ts +49 -29
- package/src/account/Template.tsx +11 -32
- package/src/account/Template.useInitialize.ts +35 -0
- package/src/account/i18n/index.ts +5 -5
- package/src/account/i18n/messages_defaultSet/types.ts +30 -0
- package/src/account/i18n/noJsx/GenericI18n_noJsx.ts +64 -0
- package/src/account/i18n/noJsx/getI18n.tsx +341 -0
- package/src/account/i18n/noJsx/i18nBuilder.ts +117 -0
- package/src/account/i18n/noJsx/index.ts +3 -0
- package/src/account/i18n/withJsx/GenericI18n.tsx +81 -0
- package/src/account/i18n/withJsx/i18nBuilder.ts +117 -0
- package/src/account/i18n/withJsx/index.ts +3 -0
- package/src/{login/i18n → account/i18n/withJsx}/useI18n.tsx +43 -11
- package/src/account/index.ts +1 -1
- package/src/account/pages/Totp.tsx +3 -2
- package/src/bin/initialize-account-theme/src/multi-page/i18n.ts +10 -3
- package/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl +11 -9
- package/src/bin/keycloakify/generateResources/generateMessageProperties.ts +371 -121
- package/src/bin/keycloakify/generateResources/generateResources.ts +8 -6
- package/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts +61 -29
- package/src/bin/keycloakify/generateResources/generateResourcesForThemeVariant.ts +15 -9
- package/src/lib/kcSanitize/HtmlPolicyBuilder.ts +252 -0
- package/src/lib/kcSanitize/KcSanitizer.ts +60 -0
- package/src/lib/kcSanitize/KcSanitizerPolicy.ts +294 -0
- package/src/lib/kcSanitize/index.ts +5 -0
- package/src/login/KcContext/kcContextMocks.ts +54 -29
- package/src/login/Template.tsx +11 -17
- package/src/login/{Template.useStylesAndScripts.ts → Template.useInitialize.ts} +4 -21
- package/src/login/i18n/index.ts +5 -5
- package/src/login/i18n/messages_defaultSet/types.ts +37 -0
- package/src/login/i18n/noJsx/GenericI18n_noJsx.ts +64 -0
- package/src/login/i18n/noJsx/getI18n.tsx +341 -0
- package/src/login/i18n/noJsx/i18nBuilder.ts +117 -0
- package/src/login/i18n/noJsx/index.ts +3 -0
- package/src/login/i18n/withJsx/GenericI18n.tsx +81 -0
- package/src/login/i18n/withJsx/i18nBuilder.ts +117 -0
- package/src/login/i18n/withJsx/index.ts +3 -0
- package/src/{account/i18n → login/i18n/withJsx}/useI18n.tsx +43 -11
- package/src/login/index.ts +1 -1
- package/src/login/lib/useUserProfileForm.tsx +3 -2
- package/src/login/pages/Error.tsx +2 -1
- package/src/login/pages/Info.tsx +13 -10
- package/src/login/pages/Login.tsx +4 -3
- package/src/login/pages/LoginConfigTotp.tsx +3 -2
- package/src/login/pages/LoginOtp.tsx +2 -1
- package/src/login/pages/LoginPassword.tsx +2 -1
- package/src/login/pages/LoginRecoveryAuthnCodeInput.tsx +2 -1
- package/src/login/pages/LoginResetPassword.tsx +2 -1
- package/src/login/pages/LoginUpdatePassword.tsx +3 -2
- package/src/login/pages/Register.tsx +2 -1
- package/src/tools/vendor/dompurify.ts +3 -0
- package/stories/intro/intro.stories.tsx +0 -1
- package/tools/vendor/dompurify.d.ts +2 -0
- package/tools/vendor/dompurify.js +2 -0
- package/account/i18n/GenericI18n.d.ts +0 -6
- package/account/i18n/GenericI18n.js +0 -2
- package/account/i18n/GenericI18n.js.map +0 -1
- package/account/i18n/i18n.d.ts +0 -87
- package/account/i18n/i18n.js +0 -111
- package/account/i18n/i18n.js.map +0 -1
- package/account/i18n/useI18n.d.ts +0 -14
- package/account/i18n/useI18n.js.map +0 -1
- package/login/Template.useStylesAndScripts.js.map +0 -1
- package/login/i18n/GenericI18n.d.ts +0 -6
- package/login/i18n/GenericI18n.js +0 -2
- package/login/i18n/GenericI18n.js.map +0 -1
- package/login/i18n/i18n.d.ts +0 -87
- package/login/i18n/i18n.js +0 -113
- package/login/i18n/i18n.js.map +0 -1
- package/login/i18n/useI18n.d.ts +0 -14
- package/login/i18n/useI18n.js.map +0 -1
- package/src/account/i18n/GenericI18n.tsx +0 -6
- package/src/account/i18n/i18n.tsx +0 -250
- package/src/login/i18n/GenericI18n.tsx +0 -6
- package/src/login/i18n/i18n.tsx +0 -252
@@ -0,0 +1,35 @@
|
|
1
|
+
import { assert } from "keycloakify/tools/assert";
|
2
|
+
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
3
|
+
import type { KcContext } from "keycloakify/account/KcContext";
|
4
|
+
|
5
|
+
export type KcContextLike = {
|
6
|
+
url: {
|
7
|
+
resourcesCommonPath: string;
|
8
|
+
resourcesPath: string;
|
9
|
+
};
|
10
|
+
};
|
11
|
+
|
12
|
+
assert<keyof KcContextLike extends keyof KcContext ? true : false>();
|
13
|
+
assert<KcContext extends KcContextLike ? true : false>();
|
14
|
+
|
15
|
+
export function useInitialize(params: {
|
16
|
+
kcContext: KcContextLike;
|
17
|
+
doUseDefaultCss: boolean;
|
18
|
+
}) {
|
19
|
+
const { kcContext, doUseDefaultCss } = params;
|
20
|
+
|
21
|
+
const { url } = kcContext;
|
22
|
+
|
23
|
+
const { areAllStyleSheetsLoaded } = useInsertLinkTags({
|
24
|
+
componentOrHookName: "Template",
|
25
|
+
hrefs: !doUseDefaultCss
|
26
|
+
? []
|
27
|
+
: [
|
28
|
+
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
29
|
+
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
30
|
+
`${url.resourcesPath}/css/account.css`
|
31
|
+
]
|
32
|
+
});
|
33
|
+
|
34
|
+
return { isReadyToRender: areAllStyleSheetsLoaded };
|
35
|
+
}
|
@@ -1,5 +1,5 @@
|
|
1
|
-
|
2
|
-
import type {
|
3
|
-
|
4
|
-
|
5
|
-
export
|
1
|
+
export * from "./withJsx";
|
2
|
+
import type { GenericI18n } from "./withJsx/GenericI18n";
|
3
|
+
import type { MessageKey as MessageKey_defaultSet } from "./messages_defaultSet/types";
|
4
|
+
/** INTERNAL: DO NOT IMPORT THIS */
|
5
|
+
export type I18n = GenericI18n<MessageKey_defaultSet, string>;
|
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
export const languageTags = [
|
3
|
+
"ar",
|
4
|
+
"ca",
|
5
|
+
"cs",
|
6
|
+
"da",
|
7
|
+
"de",
|
8
|
+
"en",
|
9
|
+
"es",
|
10
|
+
"fi",
|
11
|
+
"fr",
|
12
|
+
"hu",
|
13
|
+
"it",
|
14
|
+
"ja",
|
15
|
+
"lt",
|
16
|
+
"lv",
|
17
|
+
"nl",
|
18
|
+
"no",
|
19
|
+
"pl",
|
20
|
+
"pt-BR",
|
21
|
+
"ru",
|
22
|
+
"sk",
|
23
|
+
"sv",
|
24
|
+
"tr",
|
25
|
+
"zh-CN"
|
26
|
+
] as const;
|
27
|
+
|
28
|
+
export type LanguageTag = typeof languageTags[number];
|
29
|
+
|
30
|
+
export type MessageKey = keyof typeof import("./en")["default"];
|
@@ -0,0 +1,64 @@
|
|
1
|
+
export type GenericI18n_noJsx<MessageKey extends string, LanguageTag extends string> = {
|
2
|
+
currentLanguage: {
|
3
|
+
/**
|
4
|
+
* e.g: "en", "fr", "zh-CN"
|
5
|
+
*
|
6
|
+
* The current language
|
7
|
+
*/
|
8
|
+
languageTag: LanguageTag;
|
9
|
+
/**
|
10
|
+
* e.g: "English", "Français", "中文(简体)"
|
11
|
+
*
|
12
|
+
* The current language
|
13
|
+
*/
|
14
|
+
label: string;
|
15
|
+
};
|
16
|
+
/**
|
17
|
+
* Array of languages enabled on the realm.
|
18
|
+
*/
|
19
|
+
enabledLanguages: {
|
20
|
+
languageTag: LanguageTag;
|
21
|
+
label: string;
|
22
|
+
href: string;
|
23
|
+
}[];
|
24
|
+
/**
|
25
|
+
*
|
26
|
+
* Examples assuming currentLanguageTag === "en"
|
27
|
+
* {
|
28
|
+
* en: {
|
29
|
+
* "access-denied": "Access denied",
|
30
|
+
* "impersonateTitleHtml": "<strong>{0}</strong> Impersonate User",
|
31
|
+
* "bar": "Bar {0}"
|
32
|
+
* }
|
33
|
+
* }
|
34
|
+
*
|
35
|
+
* msgStr("access-denied") === "Access denied"
|
36
|
+
* msgStr("not-a-message-key") Throws an error
|
37
|
+
* msgStr("impersonateTitleHtml", "Foo") === "<strong>Foo</strong> Impersonate User"
|
38
|
+
* msgStr("${bar}", "<strong>c</strong>") === "Bar <strong>XXX</strong>"
|
39
|
+
* The html in the arg is partially escaped for security reasons, it might come from an untrusted source, it's not safe to render it as html.
|
40
|
+
*/
|
41
|
+
msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
|
42
|
+
/**
|
43
|
+
* This is meant to be used when the key argument is variable, something that might have been configured by the user
|
44
|
+
* in the Keycloak admin for example.
|
45
|
+
*
|
46
|
+
* Examples assuming currentLanguageTag === "en"
|
47
|
+
* {
|
48
|
+
* en: {
|
49
|
+
* "access-denied": "Access denied",
|
50
|
+
* }
|
51
|
+
* }
|
52
|
+
*
|
53
|
+
* advancedMsgStr("${access-denied}") === advancedMsgStr("access-denied") === msgStr("access-denied") === "Access denied"
|
54
|
+
* advancedMsgStr("${not-a-message-key}") === advancedMsgStr("not-a-message-key") === "not-a-message-key"
|
55
|
+
*/
|
56
|
+
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Initially the messages are in english (fallback language).
|
60
|
+
* The translations in the current language are being fetched dynamically.
|
61
|
+
* This property is true while the translations are being fetched.
|
62
|
+
*/
|
63
|
+
isFetchingTranslations: boolean;
|
64
|
+
};
|
@@ -0,0 +1,341 @@
|
|
1
|
+
import "keycloakify/tools/Object.fromEntries";
|
2
|
+
import { assert } from "tsafe/assert";
|
3
|
+
import messages_defaultSet_fallbackLanguage from "../messages_defaultSet/en";
|
4
|
+
import { fetchMessages_defaultSet } from "../messages_defaultSet";
|
5
|
+
import type { KcContext } from "../../KcContext";
|
6
|
+
import { FALLBACK_LANGUAGE_TAG } from "keycloakify/bin/shared/constants";
|
7
|
+
import { id } from "tsafe/id";
|
8
|
+
import { is } from "tsafe/is";
|
9
|
+
import { Reflect } from "tsafe/Reflect";
|
10
|
+
import {
|
11
|
+
type LanguageTag as LanguageTag_defaultSet,
|
12
|
+
type MessageKey as MessageKey_defaultSet,
|
13
|
+
languageTags as languageTags_defaultSet
|
14
|
+
} from "../messages_defaultSet/types";
|
15
|
+
import type { GenericI18n_noJsx } from "./GenericI18n_noJsx";
|
16
|
+
|
17
|
+
export type KcContextLike = {
|
18
|
+
themeName: string;
|
19
|
+
locale?: {
|
20
|
+
currentLanguageTag: string;
|
21
|
+
supported: { languageTag: string; url: string; label: string }[];
|
22
|
+
};
|
23
|
+
"x-keycloakify": {
|
24
|
+
messages: Record<string, string>;
|
25
|
+
};
|
26
|
+
};
|
27
|
+
|
28
|
+
assert<KcContext extends KcContextLike ? true : false>();
|
29
|
+
|
30
|
+
export type ReturnTypeOfCreateGetI18n<MessageKey_themeDefined extends string, LanguageTag_notInDefaultSet extends string> = {
|
31
|
+
getI18n: (params: { kcContext: KcContextLike }) => {
|
32
|
+
i18n: GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined, LanguageTag_defaultSet | LanguageTag_notInDefaultSet>;
|
33
|
+
prI18n_currentLanguage:
|
34
|
+
| Promise<GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined, LanguageTag_defaultSet | LanguageTag_notInDefaultSet>>
|
35
|
+
| undefined;
|
36
|
+
};
|
37
|
+
ofTypeI18n: GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined, LanguageTag_defaultSet | LanguageTag_notInDefaultSet>;
|
38
|
+
};
|
39
|
+
|
40
|
+
export function createGetI18n<
|
41
|
+
ThemeName extends string = string,
|
42
|
+
MessageKey_themeDefined extends string = never,
|
43
|
+
LanguageTag_notInDefaultSet extends string = never
|
44
|
+
>(params: {
|
45
|
+
extraLanguageTranslations: {
|
46
|
+
[languageTag in LanguageTag_notInDefaultSet]: {
|
47
|
+
label: string;
|
48
|
+
getMessages: () => Promise<{ default: Record<MessageKey_defaultSet, string> }>;
|
49
|
+
};
|
50
|
+
};
|
51
|
+
messagesByLanguageTag_themeDefined: Partial<{
|
52
|
+
[languageTag in LanguageTag_defaultSet | LanguageTag_notInDefaultSet]: {
|
53
|
+
[key in MessageKey_themeDefined]: string | Record<ThemeName, string>;
|
54
|
+
};
|
55
|
+
}>;
|
56
|
+
}): ReturnTypeOfCreateGetI18n<MessageKey_themeDefined, LanguageTag_notInDefaultSet> {
|
57
|
+
const { extraLanguageTranslations, messagesByLanguageTag_themeDefined } = params;
|
58
|
+
|
59
|
+
Object.keys(extraLanguageTranslations).forEach(languageTag_notInDefaultSet => {
|
60
|
+
if (id<readonly string[]>(languageTags_defaultSet).includes(languageTag_notInDefaultSet)) {
|
61
|
+
throw new Error(
|
62
|
+
[
|
63
|
+
`Language "${languageTag_notInDefaultSet}" is already in the default set, you don't need to provide your own base translations for it`,
|
64
|
+
`If you want to override some translations for this language, you can use the "withCustomTranslations" method`
|
65
|
+
].join(" ")
|
66
|
+
);
|
67
|
+
}
|
68
|
+
});
|
69
|
+
|
70
|
+
type LanguageTag = LanguageTag_defaultSet | LanguageTag_notInDefaultSet;
|
71
|
+
|
72
|
+
type MessageKey = MessageKey_defaultSet | MessageKey_themeDefined;
|
73
|
+
|
74
|
+
type I18n = GenericI18n_noJsx<MessageKey, LanguageTag>;
|
75
|
+
|
76
|
+
type Result = { i18n: I18n; prI18n_currentLanguage: Promise<I18n> | undefined };
|
77
|
+
|
78
|
+
const cachedResultByKcContext = new WeakMap<KcContextLike, Result>();
|
79
|
+
|
80
|
+
function getI18n(params: { kcContext: KcContextLike }): Result {
|
81
|
+
const { kcContext } = params;
|
82
|
+
|
83
|
+
use_cache: {
|
84
|
+
const cachedResult = cachedResultByKcContext.get(kcContext);
|
85
|
+
|
86
|
+
if (cachedResult === undefined) {
|
87
|
+
break use_cache;
|
88
|
+
}
|
89
|
+
|
90
|
+
return cachedResult;
|
91
|
+
}
|
92
|
+
|
93
|
+
{
|
94
|
+
const currentLanguageTag = kcContext.locale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG;
|
95
|
+
const html = document.querySelector("html");
|
96
|
+
assert(html !== null);
|
97
|
+
html.lang = currentLanguageTag;
|
98
|
+
}
|
99
|
+
|
100
|
+
const getLanguageLabel = (languageTag: LanguageTag) => {
|
101
|
+
form_user_added_languages: {
|
102
|
+
if (!(languageTag in extraLanguageTranslations)) {
|
103
|
+
break form_user_added_languages;
|
104
|
+
}
|
105
|
+
assert(is<Exclude<LanguageTag, LanguageTag_defaultSet>>(languageTag));
|
106
|
+
|
107
|
+
const entry = extraLanguageTranslations[languageTag];
|
108
|
+
|
109
|
+
return entry.label;
|
110
|
+
}
|
111
|
+
|
112
|
+
from_server: {
|
113
|
+
if (kcContext.locale === undefined) {
|
114
|
+
break from_server;
|
115
|
+
}
|
116
|
+
|
117
|
+
const supportedEntry = kcContext.locale.supported.find(entry => entry.languageTag === languageTag);
|
118
|
+
|
119
|
+
if (supportedEntry === undefined) {
|
120
|
+
break from_server;
|
121
|
+
}
|
122
|
+
|
123
|
+
// cspell: disable-next-line
|
124
|
+
// from "Espagnol (Español)" we want to extract "Español"
|
125
|
+
const match = supportedEntry.label.match(/[^(]+\(([^)]+)\)/);
|
126
|
+
|
127
|
+
if (match !== null) {
|
128
|
+
return match[1];
|
129
|
+
}
|
130
|
+
|
131
|
+
return supportedEntry.label;
|
132
|
+
}
|
133
|
+
|
134
|
+
// NOTE: This should never happen
|
135
|
+
return languageTag;
|
136
|
+
};
|
137
|
+
|
138
|
+
const currentLanguage: I18n["currentLanguage"] = (() => {
|
139
|
+
const languageTag = id<string>(kcContext.locale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG) as LanguageTag;
|
140
|
+
|
141
|
+
return {
|
142
|
+
languageTag,
|
143
|
+
label: getLanguageLabel(languageTag)
|
144
|
+
};
|
145
|
+
})();
|
146
|
+
|
147
|
+
const enabledLanguages: I18n["enabledLanguages"] = (() => {
|
148
|
+
const enabledLanguages: I18n["enabledLanguages"] = [];
|
149
|
+
|
150
|
+
if (kcContext.locale !== undefined) {
|
151
|
+
for (const entry of kcContext.locale.supported ?? []) {
|
152
|
+
const languageTag = id<string>(entry.languageTag) as LanguageTag;
|
153
|
+
|
154
|
+
enabledLanguages.push({
|
155
|
+
languageTag,
|
156
|
+
label: getLanguageLabel(languageTag),
|
157
|
+
href: entry.url
|
158
|
+
});
|
159
|
+
}
|
160
|
+
}
|
161
|
+
|
162
|
+
if (enabledLanguages.find(({ languageTag }) => languageTag === currentLanguage.languageTag) === undefined) {
|
163
|
+
enabledLanguages.push({
|
164
|
+
languageTag: currentLanguage.languageTag,
|
165
|
+
label: getLanguageLabel(currentLanguage.languageTag),
|
166
|
+
href: "#"
|
167
|
+
});
|
168
|
+
}
|
169
|
+
|
170
|
+
return enabledLanguages;
|
171
|
+
})();
|
172
|
+
|
173
|
+
const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory<MessageKey_themeDefined>({
|
174
|
+
themeName: kcContext.themeName,
|
175
|
+
messages_themeDefined:
|
176
|
+
messagesByLanguageTag_themeDefined[currentLanguage.languageTag] ??
|
177
|
+
messagesByLanguageTag_themeDefined[id<string>(FALLBACK_LANGUAGE_TAG) as LanguageTag] ??
|
178
|
+
(() => {
|
179
|
+
const firstLanguageTag = Object.keys(messagesByLanguageTag_themeDefined)[0];
|
180
|
+
if (firstLanguageTag === undefined) {
|
181
|
+
return undefined;
|
182
|
+
}
|
183
|
+
return messagesByLanguageTag_themeDefined[firstLanguageTag as LanguageTag];
|
184
|
+
})(),
|
185
|
+
messages_fromKcServer: kcContext["x-keycloakify"].messages
|
186
|
+
});
|
187
|
+
|
188
|
+
const isCurrentLanguageFallbackLanguage = currentLanguage.languageTag === FALLBACK_LANGUAGE_TAG;
|
189
|
+
|
190
|
+
const result: Result = {
|
191
|
+
i18n: {
|
192
|
+
currentLanguage,
|
193
|
+
enabledLanguages,
|
194
|
+
...createI18nTranslationFunctions({
|
195
|
+
messages_defaultSet_currentLanguage: isCurrentLanguageFallbackLanguage ? messages_defaultSet_fallbackLanguage : undefined
|
196
|
+
}),
|
197
|
+
isFetchingTranslations: !isCurrentLanguageFallbackLanguage
|
198
|
+
},
|
199
|
+
prI18n_currentLanguage: isCurrentLanguageFallbackLanguage
|
200
|
+
? undefined
|
201
|
+
: (async () => {
|
202
|
+
const messages_defaultSet_currentLanguage = await (async () => {
|
203
|
+
const currentLanguageTag = currentLanguage.languageTag;
|
204
|
+
|
205
|
+
const fromDefaultSet = await fetchMessages_defaultSet(currentLanguageTag);
|
206
|
+
|
207
|
+
const isEmpty = (() => {
|
208
|
+
for (let _key in fromDefaultSet) {
|
209
|
+
return false;
|
210
|
+
}
|
211
|
+
|
212
|
+
return true;
|
213
|
+
})();
|
214
|
+
|
215
|
+
if (isEmpty) {
|
216
|
+
assert(is<Exclude<LanguageTag, LanguageTag_defaultSet>>(currentLanguageTag));
|
217
|
+
|
218
|
+
const entry = extraLanguageTranslations[currentLanguageTag];
|
219
|
+
|
220
|
+
assert(entry !== undefined);
|
221
|
+
|
222
|
+
return entry.getMessages().then(({ default: messages }) => messages);
|
223
|
+
}
|
224
|
+
|
225
|
+
return fromDefaultSet;
|
226
|
+
})();
|
227
|
+
|
228
|
+
const i18n_currentLanguage: I18n = {
|
229
|
+
currentLanguage,
|
230
|
+
enabledLanguages,
|
231
|
+
...createI18nTranslationFunctions({ messages_defaultSet_currentLanguage }),
|
232
|
+
isFetchingTranslations: false
|
233
|
+
};
|
234
|
+
|
235
|
+
// NOTE: This promise.resolve is just because without it we TypeScript
|
236
|
+
// gives a Variable 'result' is used before being assigned. error
|
237
|
+
await Promise.resolve().then(() => {
|
238
|
+
result.i18n = i18n_currentLanguage;
|
239
|
+
result.prI18n_currentLanguage = undefined;
|
240
|
+
});
|
241
|
+
|
242
|
+
return i18n_currentLanguage;
|
243
|
+
})()
|
244
|
+
};
|
245
|
+
|
246
|
+
cachedResultByKcContext.set(kcContext, result);
|
247
|
+
|
248
|
+
return result;
|
249
|
+
}
|
250
|
+
|
251
|
+
return {
|
252
|
+
getI18n,
|
253
|
+
ofTypeI18n: Reflect<I18n>()
|
254
|
+
};
|
255
|
+
}
|
256
|
+
|
257
|
+
function createI18nTranslationFunctionsFactory<MessageKey_themeDefined extends string>(params: {
|
258
|
+
themeName: string;
|
259
|
+
messages_themeDefined: Record<MessageKey_themeDefined, string | Record<string, string>> | undefined;
|
260
|
+
messages_fromKcServer: Record<string, string>;
|
261
|
+
}) {
|
262
|
+
const { themeName, messages_themeDefined, messages_fromKcServer } = params;
|
263
|
+
|
264
|
+
function createI18nTranslationFunctions(params: {
|
265
|
+
messages_defaultSet_currentLanguage: Partial<Record<MessageKey_defaultSet, string>> | undefined;
|
266
|
+
}): Pick<GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined, string>, "msgStr" | "advancedMsgStr"> {
|
267
|
+
const { messages_defaultSet_currentLanguage } = params;
|
268
|
+
|
269
|
+
function resolveMsg(props: { key: string; args: (string | undefined)[] }): string | undefined {
|
270
|
+
const { key, args } = props;
|
271
|
+
|
272
|
+
const message =
|
273
|
+
id<Record<string, string | undefined>>(messages_fromKcServer)[key] ??
|
274
|
+
(() => {
|
275
|
+
const messageOrMap = id<Record<string, string | Record<string, string> | undefined> | undefined>(messages_themeDefined)?.[key];
|
276
|
+
|
277
|
+
if (messageOrMap === undefined) {
|
278
|
+
return undefined;
|
279
|
+
}
|
280
|
+
|
281
|
+
if (typeof messageOrMap === "string") {
|
282
|
+
return messageOrMap;
|
283
|
+
}
|
284
|
+
|
285
|
+
const message = messageOrMap[themeName];
|
286
|
+
|
287
|
+
assert(message !== undefined, `No translation for theme variant "${themeName}" for key "${key}"`);
|
288
|
+
|
289
|
+
return message;
|
290
|
+
})() ??
|
291
|
+
id<Record<string, string | undefined> | undefined>(messages_defaultSet_currentLanguage)?.[key] ??
|
292
|
+
id<Record<string, string | undefined>>(messages_defaultSet_fallbackLanguage)[key];
|
293
|
+
|
294
|
+
if (message === undefined) {
|
295
|
+
return undefined;
|
296
|
+
}
|
297
|
+
|
298
|
+
const startIndex = message
|
299
|
+
.match(/{[0-9]+}/g)
|
300
|
+
?.map(g => g.match(/{([0-9]+)}/)![1])
|
301
|
+
.map(indexStr => parseInt(indexStr))
|
302
|
+
.sort((a, b) => a - b)[0];
|
303
|
+
|
304
|
+
if (startIndex === undefined) {
|
305
|
+
// No {0} in message (no arguments expected)
|
306
|
+
return message;
|
307
|
+
}
|
308
|
+
|
309
|
+
let messageWithArgsInjected = message;
|
310
|
+
|
311
|
+
args.forEach((arg, i) => {
|
312
|
+
if (arg === undefined) {
|
313
|
+
return;
|
314
|
+
}
|
315
|
+
|
316
|
+
messageWithArgsInjected = messageWithArgsInjected.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
|
317
|
+
});
|
318
|
+
|
319
|
+
return messageWithArgsInjected;
|
320
|
+
}
|
321
|
+
|
322
|
+
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[] }): string {
|
323
|
+
const { key, args } = props;
|
324
|
+
|
325
|
+
const match = key.match(/^\$\{(.+)\}$/);
|
326
|
+
|
327
|
+
return resolveMsg({ key: match !== null ? match[1] : key, args }) ?? key;
|
328
|
+
}
|
329
|
+
|
330
|
+
return {
|
331
|
+
msgStr: (key, ...args) => {
|
332
|
+
const resolvedMessage = resolveMsg({ key, args });
|
333
|
+
assert(resolvedMessage !== undefined, `Message with key "${key}" not found`);
|
334
|
+
return resolvedMessage;
|
335
|
+
},
|
336
|
+
advancedMsgStr: (key, ...args) => resolveMsgAdvanced({ key, args })
|
337
|
+
};
|
338
|
+
}
|
339
|
+
|
340
|
+
return { createI18nTranslationFunctions };
|
341
|
+
}
|
@@ -0,0 +1,117 @@
|
|
1
|
+
import type {
|
2
|
+
LanguageTag as LanguageTag_defaultSet,
|
3
|
+
MessageKey as MessageKey_defaultSet
|
4
|
+
} from "../messages_defaultSet/types";
|
5
|
+
import { type ReturnTypeOfCreateGetI18n, createGetI18n } from "./getI18n";
|
6
|
+
|
7
|
+
export type I18nBuilder<
|
8
|
+
ThemeName extends string = never,
|
9
|
+
MessageKey_themeDefined extends string = never,
|
10
|
+
LanguageTag_notInDefaultSet extends string = never,
|
11
|
+
ExcludedMethod extends
|
12
|
+
| "withThemeName"
|
13
|
+
| "withExtraLanguages"
|
14
|
+
| "withCustomTranslations" = never
|
15
|
+
> = Omit<
|
16
|
+
{
|
17
|
+
withThemeName: <ThemeName extends string>() => I18nBuilder<
|
18
|
+
ThemeName,
|
19
|
+
MessageKey_themeDefined,
|
20
|
+
LanguageTag_notInDefaultSet,
|
21
|
+
ExcludedMethod | "withThemeName"
|
22
|
+
>;
|
23
|
+
withExtraLanguages: <
|
24
|
+
LanguageTag_notInDefaultSet extends string
|
25
|
+
>(extraLanguageTranslations: {
|
26
|
+
[LanguageTag in LanguageTag_notInDefaultSet]: {
|
27
|
+
label: string;
|
28
|
+
getMessages: () => Promise<{
|
29
|
+
default: Record<MessageKey_defaultSet, string>;
|
30
|
+
}>;
|
31
|
+
};
|
32
|
+
}) => I18nBuilder<
|
33
|
+
ThemeName,
|
34
|
+
MessageKey_themeDefined,
|
35
|
+
LanguageTag_notInDefaultSet,
|
36
|
+
ExcludedMethod | "withExtraLanguages"
|
37
|
+
>;
|
38
|
+
withCustomTranslations: <MessageKey_themeDefined extends string>(
|
39
|
+
messagesByLanguageTag_themeDefined: Partial<{
|
40
|
+
[LanguageTag in
|
41
|
+
| LanguageTag_defaultSet
|
42
|
+
| LanguageTag_notInDefaultSet]: Record<
|
43
|
+
MessageKey_themeDefined,
|
44
|
+
string | Record<ThemeName, string>
|
45
|
+
>;
|
46
|
+
}>
|
47
|
+
) => I18nBuilder<
|
48
|
+
ThemeName,
|
49
|
+
MessageKey_themeDefined,
|
50
|
+
LanguageTag_notInDefaultSet,
|
51
|
+
ExcludedMethod | "withCustomTranslations"
|
52
|
+
>;
|
53
|
+
build: () => ReturnTypeOfCreateGetI18n<
|
54
|
+
MessageKey_themeDefined,
|
55
|
+
LanguageTag_notInDefaultSet
|
56
|
+
>;
|
57
|
+
},
|
58
|
+
ExcludedMethod
|
59
|
+
>;
|
60
|
+
|
61
|
+
function createI18nBuilder<
|
62
|
+
ThemeName extends string = never,
|
63
|
+
MessageKey_themeDefined extends string = never,
|
64
|
+
LanguageTag_notInDefaultSet extends string = never
|
65
|
+
>(params: {
|
66
|
+
extraLanguageTranslations: {
|
67
|
+
[LanguageTag in LanguageTag_notInDefaultSet]: {
|
68
|
+
label: string;
|
69
|
+
getMessages: () => Promise<{
|
70
|
+
default: Record<MessageKey_defaultSet, string>;
|
71
|
+
}>;
|
72
|
+
};
|
73
|
+
};
|
74
|
+
messagesByLanguageTag_themeDefined: Partial<{
|
75
|
+
[LanguageTag in LanguageTag_defaultSet | LanguageTag_notInDefaultSet]: Record<
|
76
|
+
MessageKey_themeDefined,
|
77
|
+
string | Record<ThemeName, string>
|
78
|
+
>;
|
79
|
+
}>;
|
80
|
+
}): I18nBuilder<ThemeName, MessageKey_themeDefined, LanguageTag_notInDefaultSet> {
|
81
|
+
const i18nBuilder: I18nBuilder<
|
82
|
+
ThemeName,
|
83
|
+
MessageKey_themeDefined,
|
84
|
+
LanguageTag_notInDefaultSet
|
85
|
+
> = {
|
86
|
+
withThemeName: () =>
|
87
|
+
createI18nBuilder({
|
88
|
+
extraLanguageTranslations: params.extraLanguageTranslations,
|
89
|
+
messagesByLanguageTag_themeDefined:
|
90
|
+
params.messagesByLanguageTag_themeDefined as any
|
91
|
+
}),
|
92
|
+
withExtraLanguages: extraLanguageTranslations =>
|
93
|
+
createI18nBuilder({
|
94
|
+
extraLanguageTranslations,
|
95
|
+
messagesByLanguageTag_themeDefined:
|
96
|
+
params.messagesByLanguageTag_themeDefined as any
|
97
|
+
}),
|
98
|
+
withCustomTranslations: messagesByLanguageTag_themeDefined =>
|
99
|
+
createI18nBuilder({
|
100
|
+
extraLanguageTranslations: params.extraLanguageTranslations,
|
101
|
+
messagesByLanguageTag_themeDefined
|
102
|
+
}),
|
103
|
+
build: () =>
|
104
|
+
createGetI18n({
|
105
|
+
extraLanguageTranslations: params.extraLanguageTranslations,
|
106
|
+
messagesByLanguageTag_themeDefined:
|
107
|
+
params.messagesByLanguageTag_themeDefined
|
108
|
+
})
|
109
|
+
};
|
110
|
+
|
111
|
+
return i18nBuilder;
|
112
|
+
}
|
113
|
+
|
114
|
+
export const i18nBuilder = createI18nBuilder({
|
115
|
+
extraLanguageTranslations: {},
|
116
|
+
messagesByLanguageTag_themeDefined: {}
|
117
|
+
});
|
@@ -0,0 +1,81 @@
|
|
1
|
+
import type { GenericI18n_noJsx } from "../noJsx/GenericI18n_noJsx";
|
2
|
+
import { assert, type Equals } from "tsafe/assert";
|
3
|
+
|
4
|
+
export type GenericI18n<MessageKey extends string, LanguageTag extends string> = {
|
5
|
+
currentLanguage: {
|
6
|
+
/**
|
7
|
+
* e.g: "en", "fr", "zh-CN"
|
8
|
+
*
|
9
|
+
* The current language
|
10
|
+
*/
|
11
|
+
languageTag: LanguageTag;
|
12
|
+
/**
|
13
|
+
* e.g: "English", "Français", "中文(简体)"
|
14
|
+
*
|
15
|
+
* The current language
|
16
|
+
*/
|
17
|
+
label: string;
|
18
|
+
};
|
19
|
+
/**
|
20
|
+
* Array of languages enabled on the realm.
|
21
|
+
*/
|
22
|
+
enabledLanguages: {
|
23
|
+
languageTag: LanguageTag;
|
24
|
+
label: string;
|
25
|
+
href: string;
|
26
|
+
}[];
|
27
|
+
/**
|
28
|
+
*
|
29
|
+
* Examples assuming currentLanguageTag === "en"
|
30
|
+
* {
|
31
|
+
* en: {
|
32
|
+
* "access-denied": "Access denied",
|
33
|
+
* "impersonateTitleHtml": "<strong>{0}</strong> Impersonate User",
|
34
|
+
* "bar": "Bar {0}"
|
35
|
+
* }
|
36
|
+
* }
|
37
|
+
*
|
38
|
+
* msgStr("access-denied") === "Access denied"
|
39
|
+
* msgStr("not-a-message-key") Throws an error
|
40
|
+
* msgStr("impersonateTitleHtml", "Foo") === "<strong>Foo</strong> Impersonate User"
|
41
|
+
* msgStr("${bar}", "<strong>c</strong>") === "Bar <strong>XXX</strong>"
|
42
|
+
* The html in the arg is partially escaped for security reasons, it might come from an untrusted source, it's not safe to render it as html.
|
43
|
+
*/
|
44
|
+
msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
|
45
|
+
/**
|
46
|
+
* This is meant to be used when the key argument is variable, something that might have been configured by the user
|
47
|
+
* in the Keycloak admin for example.
|
48
|
+
*
|
49
|
+
* Examples assuming currentLanguageTag === "en"
|
50
|
+
* {
|
51
|
+
* en: {
|
52
|
+
* "access-denied": "Access denied",
|
53
|
+
* }
|
54
|
+
* }
|
55
|
+
*
|
56
|
+
* advancedMsgStr("${access-denied}") === advancedMsgStr("access-denied") === msgStr("access-denied") === "Access denied"
|
57
|
+
* advancedMsgStr("${not-a-message-key}") === advancedMsgStr("not-a-message-key") === "not-a-message-key"
|
58
|
+
*/
|
59
|
+
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
60
|
+
/**
|
61
|
+
* Initially the messages are in english (fallback language).
|
62
|
+
* The translations in the current language are being fetched dynamically.
|
63
|
+
* This property is true while the translations are being fetched.
|
64
|
+
*/
|
65
|
+
isFetchingTranslations: boolean;
|
66
|
+
/**
|
67
|
+
* Same as msgStr but returns a JSX.Element with the html string rendered as html.
|
68
|
+
*/
|
69
|
+
msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element;
|
70
|
+
/**
|
71
|
+
* Same as advancedMsgStr but returns a JSX.Element with the html string rendered as html.
|
72
|
+
*/
|
73
|
+
advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element;
|
74
|
+
};
|
75
|
+
|
76
|
+
{
|
77
|
+
type A = Omit<GenericI18n<string, string>, "msg" | "advancedMsg">;
|
78
|
+
type B = GenericI18n_noJsx<string, string>;
|
79
|
+
|
80
|
+
assert<Equals<A, B>>;
|
81
|
+
}
|