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.
Files changed (198) hide show
  1. package/account/KcContext/kcContextMocks.js +18 -6
  2. package/account/KcContext/kcContextMocks.js.map +1 -1
  3. package/account/Template.js +8 -26
  4. package/account/Template.js.map +1 -1
  5. package/account/Template.useInitialize.d.ts +12 -0
  6. package/account/Template.useInitialize.js +20 -0
  7. package/account/Template.useInitialize.js.map +1 -0
  8. package/account/i18n/index.d.ts +5 -5
  9. package/account/i18n/index.js +1 -1
  10. package/account/i18n/index.js.map +1 -1
  11. package/account/i18n/messages_defaultSet/types.d.ts +3 -0
  12. package/account/i18n/messages_defaultSet/types.js +26 -0
  13. package/account/i18n/messages_defaultSet/types.js.map +1 -0
  14. package/account/i18n/noJsx/GenericI18n_noJsx.d.ts +63 -0
  15. package/account/i18n/noJsx/GenericI18n_noJsx.js +2 -0
  16. package/account/i18n/noJsx/GenericI18n_noJsx.js.map +1 -0
  17. package/account/i18n/noJsx/getI18n.d.ts +41 -0
  18. package/account/i18n/noJsx/getI18n.js +207 -0
  19. package/account/i18n/noJsx/getI18n.js.map +1 -0
  20. package/account/i18n/noJsx/i18nBuilder.d.ts +18 -0
  21. package/account/i18n/noJsx/i18nBuilder.js +27 -0
  22. package/account/i18n/noJsx/i18nBuilder.js.map +1 -0
  23. package/account/i18n/noJsx/index.d.ts +3 -0
  24. package/account/i18n/noJsx/index.js +2 -0
  25. package/account/i18n/noJsx/index.js.map +1 -0
  26. package/account/i18n/withJsx/GenericI18n.d.ts +72 -0
  27. package/account/i18n/withJsx/GenericI18n.js +5 -0
  28. package/account/i18n/withJsx/GenericI18n.js.map +1 -0
  29. package/account/i18n/withJsx/i18nBuilder.d.ts +18 -0
  30. package/account/i18n/withJsx/i18nBuilder.js +27 -0
  31. package/account/i18n/withJsx/i18nBuilder.js.map +1 -0
  32. package/account/i18n/withJsx/index.d.ts +3 -0
  33. package/account/i18n/withJsx/index.js +2 -0
  34. package/account/i18n/withJsx/index.js.map +1 -0
  35. package/account/i18n/withJsx/useI18n.d.ts +27 -0
  36. package/account/i18n/{useI18n.js → withJsx/useI18n.js} +6 -4
  37. package/account/i18n/withJsx/useI18n.js.map +1 -0
  38. package/account/index.d.ts +1 -1
  39. package/account/index.js +1 -1
  40. package/account/index.js.map +1 -1
  41. package/account/pages/Totp.js +3 -2
  42. package/account/pages/Totp.js.map +1 -1
  43. package/bin/246.index.js +191 -191
  44. package/bin/499.index.js +252 -68
  45. package/bin/main.js +1 -1
  46. package/lib/kcSanitize/HtmlPolicyBuilder.d.ts +28 -0
  47. package/lib/kcSanitize/HtmlPolicyBuilder.js +209 -0
  48. package/lib/kcSanitize/HtmlPolicyBuilder.js.map +1 -0
  49. package/lib/kcSanitize/KcSanitizer.d.ts +12 -0
  50. package/lib/kcSanitize/KcSanitizer.js +46 -0
  51. package/lib/kcSanitize/KcSanitizer.js.map +1 -0
  52. package/lib/kcSanitize/KcSanitizerPolicy.d.ts +24 -0
  53. package/lib/kcSanitize/KcSanitizerPolicy.js +149 -0
  54. package/lib/kcSanitize/KcSanitizerPolicy.js.map +1 -0
  55. package/lib/kcSanitize/index.d.ts +1 -0
  56. package/lib/kcSanitize/index.js +5 -0
  57. package/lib/kcSanitize/index.js.map +1 -0
  58. package/login/KcContext/kcContextMocks.js +24 -6
  59. package/login/KcContext/kcContextMocks.js.map +1 -1
  60. package/login/Template.js +7 -7
  61. package/login/Template.js.map +1 -1
  62. package/login/{Template.useStylesAndScripts.d.ts → Template.useInitialize.d.ts} +1 -4
  63. package/login/{Template.useStylesAndScripts.js → Template.useInitialize.js} +5 -23
  64. package/login/Template.useInitialize.js.map +1 -0
  65. package/login/i18n/index.d.ts +5 -5
  66. package/login/i18n/index.js +1 -1
  67. package/login/i18n/index.js.map +1 -1
  68. package/login/i18n/messages_defaultSet/types.d.ts +3 -0
  69. package/login/i18n/messages_defaultSet/types.js +33 -0
  70. package/login/i18n/messages_defaultSet/types.js.map +1 -0
  71. package/login/i18n/noJsx/GenericI18n_noJsx.d.ts +63 -0
  72. package/login/i18n/noJsx/GenericI18n_noJsx.js +2 -0
  73. package/login/i18n/noJsx/GenericI18n_noJsx.js.map +1 -0
  74. package/login/i18n/noJsx/getI18n.d.ts +41 -0
  75. package/login/i18n/noJsx/getI18n.js +207 -0
  76. package/login/i18n/noJsx/getI18n.js.map +1 -0
  77. package/login/i18n/noJsx/i18nBuilder.d.ts +18 -0
  78. package/login/i18n/noJsx/i18nBuilder.js +27 -0
  79. package/login/i18n/noJsx/i18nBuilder.js.map +1 -0
  80. package/login/i18n/noJsx/index.d.ts +3 -0
  81. package/login/i18n/noJsx/index.js +2 -0
  82. package/login/i18n/noJsx/index.js.map +1 -0
  83. package/login/i18n/withJsx/GenericI18n.d.ts +72 -0
  84. package/login/i18n/withJsx/GenericI18n.js +5 -0
  85. package/login/i18n/withJsx/GenericI18n.js.map +1 -0
  86. package/login/i18n/withJsx/i18nBuilder.d.ts +18 -0
  87. package/login/i18n/withJsx/i18nBuilder.js +27 -0
  88. package/login/i18n/withJsx/i18nBuilder.js.map +1 -0
  89. package/login/i18n/withJsx/index.d.ts +3 -0
  90. package/login/i18n/withJsx/index.js +2 -0
  91. package/login/i18n/withJsx/index.js.map +1 -0
  92. package/login/i18n/withJsx/useI18n.d.ts +27 -0
  93. package/login/i18n/{useI18n.js → withJsx/useI18n.js} +6 -4
  94. package/login/i18n/withJsx/useI18n.js.map +1 -0
  95. package/login/index.d.ts +1 -1
  96. package/login/index.js +1 -1
  97. package/login/index.js.map +1 -1
  98. package/login/lib/useUserProfileForm.d.ts +1 -1
  99. package/login/lib/useUserProfileForm.js +2 -1
  100. package/login/lib/useUserProfileForm.js.map +1 -1
  101. package/login/pages/Error.js +2 -1
  102. package/login/pages/Error.js.map +1 -1
  103. package/login/pages/Info.js +4 -3
  104. package/login/pages/Info.js.map +1 -1
  105. package/login/pages/Login.js +4 -3
  106. package/login/pages/Login.js.map +1 -1
  107. package/login/pages/LoginConfigTotp.js +3 -2
  108. package/login/pages/LoginConfigTotp.js.map +1 -1
  109. package/login/pages/LoginOtp.js +2 -1
  110. package/login/pages/LoginOtp.js.map +1 -1
  111. package/login/pages/LoginPassword.js +2 -1
  112. package/login/pages/LoginPassword.js.map +1 -1
  113. package/login/pages/LoginRecoveryAuthnCodeInput.js +2 -1
  114. package/login/pages/LoginRecoveryAuthnCodeInput.js.map +1 -1
  115. package/login/pages/LoginResetPassword.js +2 -1
  116. package/login/pages/LoginResetPassword.js.map +1 -1
  117. package/login/pages/LoginUpdatePassword.js +3 -2
  118. package/login/pages/LoginUpdatePassword.js.map +1 -1
  119. package/login/pages/Register.js +2 -1
  120. package/login/pages/Register.js.map +1 -1
  121. package/package.json +110 -31
  122. package/src/account/KcContext/kcContextMocks.ts +49 -29
  123. package/src/account/Template.tsx +11 -32
  124. package/src/account/Template.useInitialize.ts +35 -0
  125. package/src/account/i18n/index.ts +5 -5
  126. package/src/account/i18n/messages_defaultSet/types.ts +30 -0
  127. package/src/account/i18n/noJsx/GenericI18n_noJsx.ts +64 -0
  128. package/src/account/i18n/noJsx/getI18n.tsx +341 -0
  129. package/src/account/i18n/noJsx/i18nBuilder.ts +117 -0
  130. package/src/account/i18n/noJsx/index.ts +3 -0
  131. package/src/account/i18n/withJsx/GenericI18n.tsx +81 -0
  132. package/src/account/i18n/withJsx/i18nBuilder.ts +117 -0
  133. package/src/account/i18n/withJsx/index.ts +3 -0
  134. package/src/{login/i18n → account/i18n/withJsx}/useI18n.tsx +43 -11
  135. package/src/account/index.ts +1 -1
  136. package/src/account/pages/Totp.tsx +3 -2
  137. package/src/bin/initialize-account-theme/src/multi-page/i18n.ts +10 -3
  138. package/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl +15 -0
  139. package/src/bin/keycloakify/generateResources/generateMessageProperties.ts +371 -121
  140. package/src/bin/keycloakify/generateResources/generateResources.ts +8 -6
  141. package/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts +61 -29
  142. package/src/bin/keycloakify/generateResources/generateResourcesForThemeVariant.ts +15 -9
  143. package/src/lib/kcSanitize/HtmlPolicyBuilder.ts +252 -0
  144. package/src/lib/kcSanitize/KcSanitizer.ts +60 -0
  145. package/src/lib/kcSanitize/KcSanitizerPolicy.ts +294 -0
  146. package/src/lib/kcSanitize/index.ts +5 -0
  147. package/src/login/KcContext/kcContextMocks.ts +54 -29
  148. package/src/login/Template.tsx +11 -17
  149. package/src/login/{Template.useStylesAndScripts.ts → Template.useInitialize.ts} +5 -29
  150. package/src/login/i18n/index.ts +5 -5
  151. package/src/login/i18n/messages_defaultSet/types.ts +37 -0
  152. package/src/login/i18n/noJsx/GenericI18n_noJsx.ts +64 -0
  153. package/src/login/i18n/noJsx/getI18n.tsx +341 -0
  154. package/src/login/i18n/noJsx/i18nBuilder.ts +117 -0
  155. package/src/login/i18n/noJsx/index.ts +3 -0
  156. package/src/login/i18n/withJsx/GenericI18n.tsx +81 -0
  157. package/src/login/i18n/withJsx/i18nBuilder.ts +117 -0
  158. package/src/login/i18n/withJsx/index.ts +3 -0
  159. package/src/{account/i18n → login/i18n/withJsx}/useI18n.tsx +43 -11
  160. package/src/login/index.ts +1 -1
  161. package/src/login/lib/useUserProfileForm.tsx +3 -2
  162. package/src/login/pages/Error.tsx +2 -1
  163. package/src/login/pages/Info.tsx +13 -10
  164. package/src/login/pages/Login.tsx +4 -3
  165. package/src/login/pages/LoginConfigTotp.tsx +3 -2
  166. package/src/login/pages/LoginOtp.tsx +2 -1
  167. package/src/login/pages/LoginPassword.tsx +2 -1
  168. package/src/login/pages/LoginRecoveryAuthnCodeInput.tsx +2 -1
  169. package/src/login/pages/LoginResetPassword.tsx +2 -1
  170. package/src/login/pages/LoginUpdatePassword.tsx +3 -2
  171. package/src/login/pages/Register.tsx +2 -1
  172. package/src/tools/useInsertScriptTags.ts +1 -1
  173. package/src/tools/vendor/dompurify.ts +3 -0
  174. package/stories/intro/intro.stories.tsx +0 -1
  175. package/tools/useInsertScriptTags.d.ts +1 -1
  176. package/tools/vendor/dompurify.d.ts +2 -0
  177. package/tools/vendor/dompurify.js +2 -0
  178. package/account/i18n/GenericI18n.d.ts +0 -6
  179. package/account/i18n/GenericI18n.js +0 -2
  180. package/account/i18n/GenericI18n.js.map +0 -1
  181. package/account/i18n/i18n.d.ts +0 -87
  182. package/account/i18n/i18n.js +0 -111
  183. package/account/i18n/i18n.js.map +0 -1
  184. package/account/i18n/useI18n.d.ts +0 -14
  185. package/account/i18n/useI18n.js.map +0 -1
  186. package/login/Template.useStylesAndScripts.js.map +0 -1
  187. package/login/i18n/GenericI18n.d.ts +0 -6
  188. package/login/i18n/GenericI18n.js +0 -2
  189. package/login/i18n/GenericI18n.js.map +0 -1
  190. package/login/i18n/i18n.d.ts +0 -87
  191. package/login/i18n/i18n.js +0 -113
  192. package/login/i18n/i18n.js.map +0 -1
  193. package/login/i18n/useI18n.d.ts +0 -14
  194. package/login/i18n/useI18n.js.map +0 -1
  195. package/src/account/i18n/GenericI18n.tsx +0 -6
  196. package/src/account/i18n/i18n.tsx +0 -250
  197. package/src/login/i18n/GenericI18n.tsx +0 -6
  198. 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 &lt;strong&gt;XXX&lt;/strong&gt;"
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, "&lt;").replace(/>/g, "&gt;")
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
- };
@@ -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 &lt;strong&gt;XXX&lt;/strong&gt;"
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, "&lt;").replace(/>/g, "&gt;");
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
- }