keycloakify 10.1.3 → 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} +5 -23
- 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 +15 -0
- 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} +5 -29
- 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/useInsertScriptTags.ts +1 -1
- package/src/tools/vendor/dompurify.ts +3 -0
- package/stories/intro/intro.stories.tsx +0 -1
- package/tools/useInsertScriptTags.d.ts +1 -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
@@ -1,250 +0,0 @@
|
|
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
|
-
|
9
|
-
export type KcContextLike = {
|
10
|
-
locale?: {
|
11
|
-
currentLanguageTag: string;
|
12
|
-
supported: { languageTag: string; url: string; label: string }[];
|
13
|
-
};
|
14
|
-
"x-keycloakify": {
|
15
|
-
messages: Record<string, string>;
|
16
|
-
};
|
17
|
-
};
|
18
|
-
|
19
|
-
assert<KcContext extends KcContextLike ? true : false>();
|
20
|
-
|
21
|
-
export type GenericI18n_noJsx<MessageKey extends string> = {
|
22
|
-
/**
|
23
|
-
* e.g: "en", "fr", "zh-CN"
|
24
|
-
*
|
25
|
-
* The current language
|
26
|
-
*/
|
27
|
-
currentLanguageTag: string;
|
28
|
-
/**
|
29
|
-
* Redirect to this url to change the language.
|
30
|
-
* After reload currentLanguageTag === newLanguageTag
|
31
|
-
*/
|
32
|
-
getChangeLocaleUrl: (newLanguageTag: string) => string;
|
33
|
-
/**
|
34
|
-
* e.g. "en" => "English", "fr" => "Français", ...
|
35
|
-
*
|
36
|
-
* Used to render a select that enable user to switch language.
|
37
|
-
* ex: https://user-images.githubusercontent.com/6702424/186044799-38801eec-4e89-483b-81dd-8e9233e8c0eb.png
|
38
|
-
* */
|
39
|
-
labelBySupportedLanguageTag: Record<string, string>;
|
40
|
-
/**
|
41
|
-
*
|
42
|
-
* Examples assuming currentLanguageTag === "en"
|
43
|
-
* {
|
44
|
-
* en: {
|
45
|
-
* "access-denied": "Access denied",
|
46
|
-
* "impersonateTitleHtml": "<strong>{0}</strong> Impersonate User",
|
47
|
-
* "bar": "Bar {0}"
|
48
|
-
* }
|
49
|
-
* }
|
50
|
-
*
|
51
|
-
* msgStr("access-denied") === "Access denied"
|
52
|
-
* msgStr("not-a-message-key") Throws an error
|
53
|
-
* msgStr("impersonateTitleHtml", "Foo") === "<strong>Foo</strong> Impersonate User"
|
54
|
-
* msgStr("${bar}", "<strong>c</strong>") === "Bar <strong>XXX</strong>"
|
55
|
-
* 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.
|
56
|
-
*/
|
57
|
-
msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
|
58
|
-
/**
|
59
|
-
* This is meant to be used when the key argument is variable, something that might have been configured by the user
|
60
|
-
* in the Keycloak admin for example.
|
61
|
-
*
|
62
|
-
* Examples assuming currentLanguageTag === "en"
|
63
|
-
* {
|
64
|
-
* en: {
|
65
|
-
* "access-denied": "Access denied",
|
66
|
-
* }
|
67
|
-
* }
|
68
|
-
*
|
69
|
-
* advancedMsgStr("${access-denied}") === advancedMsgStr("access-denied") === msgStr("access-denied") === "Access denied"
|
70
|
-
* advancedMsgStr("${not-a-message-key}") === advancedMsgStr("not-a-message-key") === "not-a-message-key"
|
71
|
-
*/
|
72
|
-
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
73
|
-
|
74
|
-
/**
|
75
|
-
* Initially the messages are in english (fallback language).
|
76
|
-
* The translations in the current language are being fetched dynamically.
|
77
|
-
* This property is true while the translations are being fetched.
|
78
|
-
*/
|
79
|
-
isFetchingTranslations: boolean;
|
80
|
-
};
|
81
|
-
|
82
|
-
export type MessageKey_defaultSet = keyof typeof messages_defaultSet_fallbackLanguage;
|
83
|
-
|
84
|
-
export function createGetI18n<MessageKey_themeDefined extends string = never>(messagesByLanguageTag_themeDefined: {
|
85
|
-
[languageTag: string]: { [key in MessageKey_themeDefined]: string };
|
86
|
-
}) {
|
87
|
-
type I18n = GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined>;
|
88
|
-
|
89
|
-
type Result = { i18n: I18n; prI18n_currentLanguage: Promise<I18n> | undefined };
|
90
|
-
|
91
|
-
const cachedResultByKcContext = new WeakMap<KcContextLike, Result>();
|
92
|
-
|
93
|
-
function getI18n(params: { kcContext: KcContextLike }): Result {
|
94
|
-
const { kcContext } = params;
|
95
|
-
|
96
|
-
use_cache: {
|
97
|
-
const cachedResult = cachedResultByKcContext.get(kcContext);
|
98
|
-
|
99
|
-
if (cachedResult === undefined) {
|
100
|
-
break use_cache;
|
101
|
-
}
|
102
|
-
|
103
|
-
return cachedResult;
|
104
|
-
}
|
105
|
-
|
106
|
-
const partialI18n: Pick<I18n, "currentLanguageTag" | "getChangeLocaleUrl" | "labelBySupportedLanguageTag"> = {
|
107
|
-
currentLanguageTag: kcContext.locale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG,
|
108
|
-
getChangeLocaleUrl: newLanguageTag => {
|
109
|
-
const { locale } = kcContext;
|
110
|
-
|
111
|
-
assert(locale !== undefined, "Internationalization not enabled");
|
112
|
-
|
113
|
-
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
|
114
|
-
|
115
|
-
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
|
116
|
-
|
117
|
-
return targetSupportedLocale.url;
|
118
|
-
},
|
119
|
-
labelBySupportedLanguageTag: Object.fromEntries((kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label]))
|
120
|
-
};
|
121
|
-
|
122
|
-
const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory<MessageKey_themeDefined>({
|
123
|
-
messages_themeDefined:
|
124
|
-
messagesByLanguageTag_themeDefined[partialI18n.currentLanguageTag] ??
|
125
|
-
messagesByLanguageTag_themeDefined[FALLBACK_LANGUAGE_TAG] ??
|
126
|
-
(() => {
|
127
|
-
const firstLanguageTag = Object.keys(messagesByLanguageTag_themeDefined)[0];
|
128
|
-
if (firstLanguageTag === undefined) {
|
129
|
-
return undefined;
|
130
|
-
}
|
131
|
-
return messagesByLanguageTag_themeDefined[firstLanguageTag];
|
132
|
-
})(),
|
133
|
-
messages_fromKcServer: kcContext["x-keycloakify"].messages
|
134
|
-
});
|
135
|
-
|
136
|
-
const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag === FALLBACK_LANGUAGE_TAG;
|
137
|
-
|
138
|
-
const result: Result = {
|
139
|
-
i18n: {
|
140
|
-
...partialI18n,
|
141
|
-
...createI18nTranslationFunctions({
|
142
|
-
messages_defaultSet_currentLanguage: isCurrentLanguageFallbackLanguage ? messages_defaultSet_fallbackLanguage : undefined
|
143
|
-
}),
|
144
|
-
isFetchingTranslations: !isCurrentLanguageFallbackLanguage
|
145
|
-
},
|
146
|
-
prI18n_currentLanguage: isCurrentLanguageFallbackLanguage
|
147
|
-
? undefined
|
148
|
-
: (async () => {
|
149
|
-
const messages_defaultSet_currentLanguage = await fetchMessages_defaultSet(partialI18n.currentLanguageTag);
|
150
|
-
|
151
|
-
const i18n_currentLanguage: I18n = {
|
152
|
-
...partialI18n,
|
153
|
-
...createI18nTranslationFunctions({ messages_defaultSet_currentLanguage }),
|
154
|
-
isFetchingTranslations: false
|
155
|
-
};
|
156
|
-
|
157
|
-
// NOTE: This promise.resolve is just because without it we TypeScript
|
158
|
-
// gives a Variable 'result' is used before being assigned. error
|
159
|
-
await Promise.resolve().then(() => {
|
160
|
-
result.i18n = i18n_currentLanguage;
|
161
|
-
result.prI18n_currentLanguage = undefined;
|
162
|
-
});
|
163
|
-
|
164
|
-
return i18n_currentLanguage;
|
165
|
-
})()
|
166
|
-
};
|
167
|
-
|
168
|
-
cachedResultByKcContext.set(kcContext, result);
|
169
|
-
|
170
|
-
return result;
|
171
|
-
}
|
172
|
-
|
173
|
-
return { getI18n };
|
174
|
-
}
|
175
|
-
|
176
|
-
function createI18nTranslationFunctionsFactory<MessageKey_themeDefined extends string>(params: {
|
177
|
-
messages_themeDefined: Record<MessageKey_themeDefined, string> | undefined;
|
178
|
-
messages_fromKcServer: Record<string, string>;
|
179
|
-
}) {
|
180
|
-
const { messages_themeDefined, messages_fromKcServer } = params;
|
181
|
-
|
182
|
-
function createI18nTranslationFunctions(params: {
|
183
|
-
messages_defaultSet_currentLanguage: Partial<Record<MessageKey_defaultSet, string>> | undefined;
|
184
|
-
}): Pick<GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined>, "msgStr" | "advancedMsgStr"> {
|
185
|
-
const { messages_defaultSet_currentLanguage } = params;
|
186
|
-
|
187
|
-
function resolveMsg(props: { key: string; args: (string | undefined)[] }): string | undefined {
|
188
|
-
const { key, args } = props;
|
189
|
-
|
190
|
-
const message =
|
191
|
-
id<Record<string, string | undefined>>(messages_fromKcServer)[key] ??
|
192
|
-
id<Record<string, string | undefined> | undefined>(messages_themeDefined)?.[key] ??
|
193
|
-
id<Record<string, string | undefined> | undefined>(messages_defaultSet_currentLanguage)?.[key] ??
|
194
|
-
id<Record<string, string | undefined>>(messages_defaultSet_fallbackLanguage)[key];
|
195
|
-
|
196
|
-
if (message === undefined) {
|
197
|
-
return undefined;
|
198
|
-
}
|
199
|
-
|
200
|
-
const startIndex = message
|
201
|
-
.match(/{[0-9]+}/g)
|
202
|
-
?.map(g => g.match(/{([0-9]+)}/)![1])
|
203
|
-
.map(indexStr => parseInt(indexStr))
|
204
|
-
.sort((a, b) => a - b)[0];
|
205
|
-
|
206
|
-
if (startIndex === undefined) {
|
207
|
-
// No {0} in message (no arguments expected)
|
208
|
-
return message;
|
209
|
-
}
|
210
|
-
|
211
|
-
let messageWithArgsInjected = message;
|
212
|
-
|
213
|
-
args.forEach((arg, i) => {
|
214
|
-
if (arg === undefined) {
|
215
|
-
return;
|
216
|
-
}
|
217
|
-
|
218
|
-
messageWithArgsInjected = messageWithArgsInjected.replace(
|
219
|
-
new RegExp(`\\{${i + startIndex}\\}`, "g"),
|
220
|
-
arg.replace(/</g, "<").replace(/>/g, ">")
|
221
|
-
);
|
222
|
-
});
|
223
|
-
|
224
|
-
return messageWithArgsInjected;
|
225
|
-
}
|
226
|
-
|
227
|
-
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[] }): string {
|
228
|
-
const { key, args } = props;
|
229
|
-
|
230
|
-
const match = key.match(/^\$\{(.+)\}$/);
|
231
|
-
|
232
|
-
if (match === null) {
|
233
|
-
return key;
|
234
|
-
}
|
235
|
-
|
236
|
-
return resolveMsg({ key: match[1], args }) ?? key;
|
237
|
-
}
|
238
|
-
|
239
|
-
return {
|
240
|
-
msgStr: (key, ...args) => {
|
241
|
-
const resolvedMessage = resolveMsg({ key, args });
|
242
|
-
assert(resolvedMessage !== undefined, `Message with key "${key}" not found`);
|
243
|
-
return resolvedMessage;
|
244
|
-
},
|
245
|
-
advancedMsgStr: (key, ...args) => resolveMsgAdvanced({ key, args })
|
246
|
-
};
|
247
|
-
}
|
248
|
-
|
249
|
-
return { createI18nTranslationFunctions };
|
250
|
-
}
|
@@ -1,6 +0,0 @@
|
|
1
|
-
import type { GenericI18n_noJsx } from "./i18n";
|
2
|
-
|
3
|
-
export type GenericI18n<MessageKey extends string> = GenericI18n_noJsx<MessageKey> & {
|
4
|
-
msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element;
|
5
|
-
advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element;
|
6
|
-
};
|
package/src/login/i18n/i18n.tsx
DELETED
@@ -1,252 +0,0 @@
|
|
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
|
-
|
9
|
-
export type KcContextLike = {
|
10
|
-
locale?: {
|
11
|
-
currentLanguageTag: string;
|
12
|
-
supported: { languageTag: string; url: string; label: string }[];
|
13
|
-
};
|
14
|
-
"x-keycloakify": {
|
15
|
-
messages: Record<string, string>;
|
16
|
-
};
|
17
|
-
};
|
18
|
-
|
19
|
-
assert<KcContext extends KcContextLike ? true : false>();
|
20
|
-
|
21
|
-
export type GenericI18n_noJsx<MessageKey extends string> = {
|
22
|
-
/**
|
23
|
-
* e.g: "en", "fr", "zh-CN"
|
24
|
-
*
|
25
|
-
* The current language
|
26
|
-
*/
|
27
|
-
currentLanguageTag: string;
|
28
|
-
/**
|
29
|
-
* Redirect to this url to change the language.
|
30
|
-
* After reload currentLanguageTag === newLanguageTag
|
31
|
-
*/
|
32
|
-
getChangeLocaleUrl: (newLanguageTag: string) => string;
|
33
|
-
/**
|
34
|
-
* e.g. "en" => "English", "fr" => "Français", ...
|
35
|
-
*
|
36
|
-
* Used to render a select that enable user to switch language.
|
37
|
-
* ex: https://user-images.githubusercontent.com/6702424/186044799-38801eec-4e89-483b-81dd-8e9233e8c0eb.png
|
38
|
-
* */
|
39
|
-
labelBySupportedLanguageTag: Record<string, string>;
|
40
|
-
/**
|
41
|
-
*
|
42
|
-
* Examples assuming currentLanguageTag === "en"
|
43
|
-
* {
|
44
|
-
* en: {
|
45
|
-
* "access-denied": "Access denied",
|
46
|
-
* "impersonateTitleHtml": "<strong>{0}</strong> Impersonate User",
|
47
|
-
* "bar": "Bar {0}"
|
48
|
-
* }
|
49
|
-
* }
|
50
|
-
*
|
51
|
-
* msgStr("access-denied") === "Access denied"
|
52
|
-
* msgStr("not-a-message-key") Throws an error
|
53
|
-
* msgStr("impersonateTitleHtml", "Foo") === "<strong>Foo</strong> Impersonate User"
|
54
|
-
* msgStr("${bar}", "<strong>c</strong>") === "Bar <strong>XXX</strong>"
|
55
|
-
* 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.
|
56
|
-
*/
|
57
|
-
msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
|
58
|
-
/**
|
59
|
-
* This is meant to be used when the key argument is variable, something that might have been configured by the user
|
60
|
-
* in the Keycloak admin for example.
|
61
|
-
*
|
62
|
-
* Examples assuming currentLanguageTag === "en"
|
63
|
-
* {
|
64
|
-
* en: {
|
65
|
-
* "access-denied": "Access denied",
|
66
|
-
* }
|
67
|
-
* }
|
68
|
-
*
|
69
|
-
* advancedMsgStr("${access-denied}") === advancedMsgStr("access-denied") === msgStr("access-denied") === "Access denied"
|
70
|
-
* advancedMsgStr("${not-a-message-key}") === advancedMsgStr("not-a-message-key") === "not-a-message-key"
|
71
|
-
*/
|
72
|
-
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
73
|
-
|
74
|
-
/**
|
75
|
-
* Initially the messages are in english (fallback language).
|
76
|
-
* The translations in the current language are being fetched dynamically.
|
77
|
-
* This property is true while the translations are being fetched.
|
78
|
-
*/
|
79
|
-
isFetchingTranslations: boolean;
|
80
|
-
};
|
81
|
-
|
82
|
-
export type MessageKey_defaultSet = keyof typeof messages_defaultSet_fallbackLanguage;
|
83
|
-
|
84
|
-
export function createGetI18n<MessageKey_themeDefined extends string = never>(messagesByLanguageTag_themeDefined: {
|
85
|
-
[languageTag: string]: { [key in MessageKey_themeDefined]: string };
|
86
|
-
}) {
|
87
|
-
type I18n = GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined>;
|
88
|
-
|
89
|
-
type Result = { i18n: I18n; prI18n_currentLanguage: Promise<I18n> | undefined };
|
90
|
-
|
91
|
-
const cachedResultByKcContext = new WeakMap<KcContextLike, Result>();
|
92
|
-
|
93
|
-
function getI18n(params: { kcContext: KcContextLike }): Result {
|
94
|
-
const { kcContext } = params;
|
95
|
-
|
96
|
-
use_cache: {
|
97
|
-
const cachedResult = cachedResultByKcContext.get(kcContext);
|
98
|
-
|
99
|
-
if (cachedResult === undefined) {
|
100
|
-
break use_cache;
|
101
|
-
}
|
102
|
-
|
103
|
-
return cachedResult;
|
104
|
-
}
|
105
|
-
|
106
|
-
const partialI18n: Pick<I18n, "currentLanguageTag" | "getChangeLocaleUrl" | "labelBySupportedLanguageTag"> = {
|
107
|
-
currentLanguageTag: kcContext.locale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG,
|
108
|
-
getChangeLocaleUrl: newLanguageTag => {
|
109
|
-
const { locale } = kcContext;
|
110
|
-
|
111
|
-
assert(locale !== undefined, "Internationalization not enabled");
|
112
|
-
|
113
|
-
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
|
114
|
-
|
115
|
-
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
|
116
|
-
|
117
|
-
return targetSupportedLocale.url;
|
118
|
-
},
|
119
|
-
labelBySupportedLanguageTag: Object.fromEntries((kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label]))
|
120
|
-
};
|
121
|
-
|
122
|
-
const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory<MessageKey_themeDefined>({
|
123
|
-
messages_themeDefined:
|
124
|
-
messagesByLanguageTag_themeDefined[partialI18n.currentLanguageTag] ??
|
125
|
-
messagesByLanguageTag_themeDefined[FALLBACK_LANGUAGE_TAG] ??
|
126
|
-
(() => {
|
127
|
-
const firstLanguageTag = Object.keys(messagesByLanguageTag_themeDefined)[0];
|
128
|
-
if (firstLanguageTag === undefined) {
|
129
|
-
return undefined;
|
130
|
-
}
|
131
|
-
return messagesByLanguageTag_themeDefined[firstLanguageTag];
|
132
|
-
})(),
|
133
|
-
messages_fromKcServer: kcContext["x-keycloakify"].messages
|
134
|
-
});
|
135
|
-
|
136
|
-
const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag === FALLBACK_LANGUAGE_TAG;
|
137
|
-
|
138
|
-
const result: Result = {
|
139
|
-
i18n: {
|
140
|
-
...partialI18n,
|
141
|
-
...createI18nTranslationFunctions({
|
142
|
-
messages_defaultSet_currentLanguage: isCurrentLanguageFallbackLanguage ? messages_defaultSet_fallbackLanguage : undefined
|
143
|
-
}),
|
144
|
-
isFetchingTranslations: !isCurrentLanguageFallbackLanguage
|
145
|
-
},
|
146
|
-
prI18n_currentLanguage: isCurrentLanguageFallbackLanguage
|
147
|
-
? undefined
|
148
|
-
: (async () => {
|
149
|
-
const messages_defaultSet_currentLanguage = await fetchMessages_defaultSet(partialI18n.currentLanguageTag);
|
150
|
-
|
151
|
-
const i18n_currentLanguage: I18n = {
|
152
|
-
...partialI18n,
|
153
|
-
...createI18nTranslationFunctions({ messages_defaultSet_currentLanguage }),
|
154
|
-
isFetchingTranslations: false
|
155
|
-
};
|
156
|
-
|
157
|
-
// NOTE: This promise.resolve is just because without it we TypeScript
|
158
|
-
// gives a Variable 'result' is used before being assigned. error
|
159
|
-
await Promise.resolve().then(() => {
|
160
|
-
result.i18n = i18n_currentLanguage;
|
161
|
-
result.prI18n_currentLanguage = undefined;
|
162
|
-
});
|
163
|
-
|
164
|
-
return i18n_currentLanguage;
|
165
|
-
})()
|
166
|
-
};
|
167
|
-
|
168
|
-
cachedResultByKcContext.set(kcContext, result);
|
169
|
-
|
170
|
-
return result;
|
171
|
-
}
|
172
|
-
|
173
|
-
return { getI18n };
|
174
|
-
}
|
175
|
-
|
176
|
-
function createI18nTranslationFunctionsFactory<MessageKey_themeDefined extends string>(params: {
|
177
|
-
messages_themeDefined: Record<MessageKey_themeDefined, string> | undefined;
|
178
|
-
messages_fromKcServer: Record<string, string>;
|
179
|
-
}) {
|
180
|
-
const { messages_themeDefined, messages_fromKcServer } = params;
|
181
|
-
|
182
|
-
function createI18nTranslationFunctions(params: {
|
183
|
-
messages_defaultSet_currentLanguage: Partial<Record<MessageKey_defaultSet, string>> | undefined;
|
184
|
-
}): Pick<GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined>, "msgStr" | "advancedMsgStr"> {
|
185
|
-
const { messages_defaultSet_currentLanguage } = params;
|
186
|
-
|
187
|
-
function resolveMsg(props: { key: string; args: (string | undefined)[] }): string | undefined {
|
188
|
-
const { key, args } = props;
|
189
|
-
|
190
|
-
const message =
|
191
|
-
id<Record<string, string | undefined>>(messages_fromKcServer)[key] ??
|
192
|
-
id<Record<string, string | undefined> | undefined>(messages_themeDefined)?.[key] ??
|
193
|
-
id<Record<string, string | undefined> | undefined>(messages_defaultSet_currentLanguage)?.[key] ??
|
194
|
-
id<Record<string, string | undefined>>(messages_defaultSet_fallbackLanguage)[key];
|
195
|
-
|
196
|
-
if (message === undefined) {
|
197
|
-
return undefined;
|
198
|
-
}
|
199
|
-
|
200
|
-
const startIndex = message
|
201
|
-
.match(/{[0-9]+}/g)
|
202
|
-
?.map(g => g.match(/{([0-9]+)}/)![1])
|
203
|
-
.map(indexStr => parseInt(indexStr))
|
204
|
-
.sort((a, b) => a - b)[0];
|
205
|
-
|
206
|
-
if (startIndex === undefined) {
|
207
|
-
// No {0} in message (no arguments expected)
|
208
|
-
return message;
|
209
|
-
}
|
210
|
-
|
211
|
-
let messageWithArgsInjected = message;
|
212
|
-
|
213
|
-
args.forEach((arg, i) => {
|
214
|
-
if (arg === undefined) {
|
215
|
-
return;
|
216
|
-
}
|
217
|
-
|
218
|
-
messageWithArgsInjected = messageWithArgsInjected.replace(
|
219
|
-
new RegExp(`\\{${i + startIndex}\\}`, "g"),
|
220
|
-
(() => {
|
221
|
-
if (key === "loginTitleHtml") {
|
222
|
-
return arg;
|
223
|
-
}
|
224
|
-
|
225
|
-
return arg.replace(/</g, "<").replace(/>/g, ">");
|
226
|
-
})()
|
227
|
-
);
|
228
|
-
});
|
229
|
-
|
230
|
-
return messageWithArgsInjected;
|
231
|
-
}
|
232
|
-
|
233
|
-
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[] }): string {
|
234
|
-
const { key, args } = props;
|
235
|
-
|
236
|
-
const match = key.match(/^\$\{(.+)\}$/);
|
237
|
-
|
238
|
-
return resolveMsg({ key: match !== null ? match[1] : key, args }) ?? key;
|
239
|
-
}
|
240
|
-
|
241
|
-
return {
|
242
|
-
msgStr: (key, ...args) => {
|
243
|
-
const resolvedMessage = resolveMsg({ key, args });
|
244
|
-
assert(resolvedMessage !== undefined, `Message with key "${key}" not found`);
|
245
|
-
return resolvedMessage;
|
246
|
-
},
|
247
|
-
advancedMsgStr: (key, ...args) => resolveMsgAdvanced({ key, args })
|
248
|
-
};
|
249
|
-
}
|
250
|
-
|
251
|
-
return { createI18nTranslationFunctions };
|
252
|
-
}
|