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
@@ -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
- import type { GenericI18n } from "./GenericI18n";
2
- import type { MessageKey_defaultSet, KcContextLike } from "./i18n";
3
- export type { MessageKey_defaultSet, KcContextLike };
4
- export type I18n = GenericI18n<MessageKey_defaultSet>;
5
- export { createUseI18n } from "./useI18n";
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 &lt;strong&gt;XXX&lt;/strong&gt;"
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,3 @@
1
+ export { i18nBuilder } from "./i18nBuilder";
2
+ export type { KcContextLike } from "./getI18n";
3
+ export type { MessageKey as MessageKey_defaultSet } from "../messages_defaultSet/types";
@@ -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 &lt;strong&gt;XXX&lt;/strong&gt;"
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
+ }