keycloakify 5.4.2 → 6.0.0-beta.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 (86) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/kcMessages.d.ts +1 -0
  3. package/kcMessages.js +14 -0
  4. package/kcMessages.js.map +1 -0
  5. package/lib/components/Error.d.ts +3 -1
  6. package/lib/components/Error.js +3 -4
  7. package/lib/components/Error.js.map +1 -1
  8. package/lib/components/Info.d.ts +3 -1
  9. package/lib/components/Info.js +3 -4
  10. package/lib/components/Info.js.map +1 -1
  11. package/lib/components/KcApp.d.ts +3 -1
  12. package/lib/components/KcApp.js +15 -15
  13. package/lib/components/KcApp.js.map +1 -1
  14. package/lib/components/Login.d.ts +3 -1
  15. package/lib/components/Login.js +3 -4
  16. package/lib/components/Login.js.map +1 -1
  17. package/lib/components/LoginIdpLinkConfirm.d.ts +3 -1
  18. package/lib/components/LoginIdpLinkConfirm.js +3 -4
  19. package/lib/components/LoginIdpLinkConfirm.js.map +1 -1
  20. package/lib/components/LoginIdpLinkEmail.d.ts +3 -1
  21. package/lib/components/LoginIdpLinkEmail.js +3 -4
  22. package/lib/components/LoginIdpLinkEmail.js.map +1 -1
  23. package/lib/components/LoginOtp.d.ts +3 -1
  24. package/lib/components/LoginOtp.js +3 -4
  25. package/lib/components/LoginOtp.js.map +1 -1
  26. package/lib/components/LoginPageExpired.d.ts +3 -1
  27. package/lib/components/LoginPageExpired.js +3 -4
  28. package/lib/components/LoginPageExpired.js.map +1 -1
  29. package/lib/components/LoginResetPassword.d.ts +3 -1
  30. package/lib/components/LoginResetPassword.js +3 -4
  31. package/lib/components/LoginResetPassword.js.map +1 -1
  32. package/lib/components/LoginUpdatePassword.d.ts +3 -1
  33. package/lib/components/LoginUpdatePassword.js +3 -4
  34. package/lib/components/LoginUpdatePassword.js.map +1 -1
  35. package/lib/components/LoginUpdateProfile.d.ts +3 -1
  36. package/lib/components/LoginUpdateProfile.js +3 -4
  37. package/lib/components/LoginUpdateProfile.js.map +1 -1
  38. package/lib/components/LoginVerifyEmail.d.ts +3 -1
  39. package/lib/components/LoginVerifyEmail.js +3 -4
  40. package/lib/components/LoginVerifyEmail.js.map +1 -1
  41. package/lib/components/Register.d.ts +3 -1
  42. package/lib/components/Register.js +3 -4
  43. package/lib/components/Register.js.map +1 -1
  44. package/lib/components/RegisterUserProfile.d.ts +3 -1
  45. package/lib/components/RegisterUserProfile.js +6 -6
  46. package/lib/components/RegisterUserProfile.js.map +1 -1
  47. package/lib/components/Template.d.ts +2 -0
  48. package/lib/components/Template.js +2 -2
  49. package/lib/components/Template.js.map +1 -1
  50. package/lib/components/Terms.d.ts +4 -2
  51. package/lib/components/Terms.js +8 -8
  52. package/lib/components/Terms.js.map +1 -1
  53. package/lib/i18n/index.d.ts +17 -5248
  54. package/lib/i18n/index.js +76 -51
  55. package/lib/i18n/index.js.map +1 -1
  56. package/lib/i18n/kcMessages.d.ts +5222 -0
  57. package/lib/i18n/kcMessages.js +19 -0
  58. package/lib/i18n/kcMessages.js.map +1 -0
  59. package/lib/tools/createObjectThatThrowsIfAccessed.d.ts +3 -0
  60. package/lib/tools/createObjectThatThrowsIfAccessed.js +39 -0
  61. package/lib/tools/createObjectThatThrowsIfAccessed.js.map +1 -0
  62. package/lib/useFormValidationSlice.d.ts +3 -0
  63. package/lib/useFormValidationSlice.js +4 -4
  64. package/lib/useFormValidationSlice.js.map +1 -1
  65. package/package.json +15 -4
  66. package/src/kcMessages.ts +1 -0
  67. package/src/lib/components/Error.tsx +4 -4
  68. package/src/lib/components/Info.tsx +5 -5
  69. package/src/lib/components/KcApp.tsx +16 -15
  70. package/src/lib/components/Login.tsx +4 -4
  71. package/src/lib/components/LoginIdpLinkConfirm.tsx +39 -37
  72. package/src/lib/components/LoginIdpLinkEmail.tsx +27 -25
  73. package/src/lib/components/LoginOtp.tsx +4 -4
  74. package/src/lib/components/LoginPageExpired.tsx +31 -29
  75. package/src/lib/components/LoginResetPassword.tsx +60 -53
  76. package/src/lib/components/LoginUpdatePassword.tsx +98 -96
  77. package/src/lib/components/LoginUpdateProfile.tsx +100 -92
  78. package/src/lib/components/LoginVerifyEmail.tsx +25 -23
  79. package/src/lib/components/Register.tsx +4 -4
  80. package/src/lib/components/RegisterUserProfile.tsx +68 -55
  81. package/src/lib/components/Template.tsx +5 -3
  82. package/src/lib/components/Terms.tsx +14 -8
  83. package/src/lib/i18n/index.tsx +76 -49
  84. package/src/lib/i18n/kcMessages.ts +24 -0
  85. package/src/lib/tools/createObjectThatThrowsIfAccessed.ts +18 -0
  86. package/src/lib/useFormValidationSlice.tsx +7 -4
@@ -2,29 +2,31 @@ import { memo } from "react";
2
2
  import { Template } from "./Template";
3
3
  import type { KcProps } from "./KcProps";
4
4
  import type { KcContextBase } from "../getKcContext/KcContextBase";
5
- import { getMsg } from "../i18n";
5
+ import type { I18n } from "../i18n";
6
6
 
7
- export const LoginVerifyEmail = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginVerifyEmail } & KcProps) => {
8
- const { msg } = getMsg(kcContext);
7
+ export const LoginVerifyEmail = memo(
8
+ ({ kcContext, useI18n, ...props }: { kcContext: KcContextBase.LoginVerifyEmail; useI18n: () => I18n } & KcProps) => {
9
+ const { msg } = useI18n();
9
10
 
10
- const { url } = kcContext;
11
+ const { url } = kcContext;
11
12
 
12
- return (
13
- <Template
14
- {...{ kcContext, ...props }}
15
- doFetchDefaultThemeResources={true}
16
- displayMessage={false}
17
- headerNode={msg("emailVerifyTitle")}
18
- formNode={
19
- <>
20
- <p className="instruction">{msg("emailVerifyInstruction1")}</p>
21
- <p className="instruction">
22
- {msg("emailVerifyInstruction2")}
23
- <a href={url.loginAction}>{msg("doClickHere")}</a>
24
- {msg("emailVerifyInstruction3")}
25
- </p>
26
- </>
27
- }
28
- />
29
- );
30
- });
13
+ return (
14
+ <Template
15
+ {...{ kcContext, useI18n, ...props }}
16
+ doFetchDefaultThemeResources={true}
17
+ displayMessage={false}
18
+ headerNode={msg("emailVerifyTitle")}
19
+ formNode={
20
+ <>
21
+ <p className="instruction">{msg("emailVerifyInstruction1")}</p>
22
+ <p className="instruction">
23
+ {msg("emailVerifyInstruction2")}
24
+ <a href={url.loginAction}>{msg("doClickHere")}</a>
25
+ {msg("emailVerifyInstruction3")}
26
+ </p>
27
+ </>
28
+ }
29
+ />
30
+ );
31
+ },
32
+ );
@@ -2,19 +2,19 @@ import { memo } from "react";
2
2
  import { Template } from "./Template";
3
3
  import type { KcProps } from "./KcProps";
4
4
  import type { KcContextBase } from "../getKcContext/KcContextBase";
5
- import { getMsg } from "../i18n";
5
+ import type { I18n } from "../i18n";
6
6
  import { useCssAndCx } from "tss-react";
7
7
 
8
- export const Register = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Register } & KcProps) => {
8
+ export const Register = memo(({ kcContext, useI18n, ...props }: { kcContext: KcContextBase.Register; useI18n: () => I18n } & KcProps) => {
9
9
  const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
10
10
 
11
- const { msg, msgStr } = getMsg(kcContext);
11
+ const { msg, msgStr } = useI18n();
12
12
 
13
13
  const { cx } = useCssAndCx();
14
14
 
15
15
  return (
16
16
  <Template
17
- {...{ kcContext, ...props }}
17
+ {...{ kcContext, useI18n, ...props }}
18
18
  doFetchDefaultThemeResources={true}
19
19
  headerNode={msg("registerTitle")}
20
20
  formNode={
@@ -2,79 +2,91 @@ import { useMemo, memo, useEffect, useState, Fragment } from "react";
2
2
  import { Template } from "./Template";
3
3
  import type { KcProps } from "./KcProps";
4
4
  import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase";
5
- import { getMsg } from "../i18n";
5
+ import type { I18n } from "../i18n";
6
6
  import { useCssAndCx } from "tss-react";
7
7
  import type { ReactComponent } from "../tools/ReactComponent";
8
8
  import { useCallbackFactory } from "powerhooks/useCallbackFactory";
9
9
  import { useFormValidationSlice } from "../useFormValidationSlice";
10
10
 
11
- export const RegisterUserProfile = memo(({ kcContext, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile } & KcProps) => {
12
- const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
11
+ export const RegisterUserProfile = memo(
12
+ ({ kcContext, useI18n, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile; useI18n: () => I18n } & KcProps) => {
13
+ const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
13
14
 
14
- const { msg, msgStr } = getMsg(kcContext);
15
+ const { msg, msgStr } = useI18n();
15
16
 
16
- const { cx, css } = useCssAndCx();
17
-
18
- const props = useMemo(
19
- () => ({
20
- ...props_,
21
- "kcFormGroupClass": cx(props_.kcFormGroupClass, css({ "marginBottom": 20 })),
22
- }),
23
- [cx, css],
24
- );
25
-
26
- const [isFomSubmittable, setIsFomSubmittable] = useState(false);
17
+ const { cx, css } = useCssAndCx();
27
18
 
28
- return (
29
- <Template
30
- {...{ kcContext, ...props }}
31
- displayMessage={messagesPerField.exists("global")}
32
- displayRequiredFields={true}
33
- doFetchDefaultThemeResources={true}
34
- headerNode={msg("registerTitle")}
35
- formNode={
36
- <form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
37
- <UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} {...props} />
38
- {recaptchaRequired && (
39
- <div className="form-group">
40
- <div className={cx(props.kcInputWrapperClass)}>
41
- <div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} />
19
+ const props = useMemo(
20
+ () => ({
21
+ ...props_,
22
+ "kcFormGroupClass": cx(props_.kcFormGroupClass, css({ "marginBottom": 20 })),
23
+ }),
24
+ [cx, css],
25
+ );
26
+
27
+ const [isFomSubmittable, setIsFomSubmittable] = useState(false);
28
+
29
+ return (
30
+ <Template
31
+ {...{ kcContext, useI18n, ...props }}
32
+ displayMessage={messagesPerField.exists("global")}
33
+ displayRequiredFields={true}
34
+ doFetchDefaultThemeResources={true}
35
+ headerNode={msg("registerTitle")}
36
+ formNode={
37
+ <form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
38
+ <UserProfileFormFields
39
+ kcContext={kcContext}
40
+ onIsFormSubmittableValueChange={setIsFomSubmittable}
41
+ useI18n={useI18n}
42
+ {...props}
43
+ />
44
+ {recaptchaRequired && (
45
+ <div className="form-group">
46
+ <div className={cx(props.kcInputWrapperClass)}>
47
+ <div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} />
48
+ </div>
42
49
  </div>
43
- </div>
44
- )}
45
- <div className={cx(props.kcFormGroupClass)}>
46
- <div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
47
- <div className={cx(props.kcFormOptionsWrapperClass)}>
48
- <span>
49
- <a href={url.loginUrl}>{msg("backToLogin")}</a>
50
- </span>
50
+ )}
51
+ <div className={cx(props.kcFormGroupClass)}>
52
+ <div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
53
+ <div className={cx(props.kcFormOptionsWrapperClass)}>
54
+ <span>
55
+ <a href={url.loginUrl}>{msg("backToLogin")}</a>
56
+ </span>
57
+ </div>
51
58
  </div>
52
- </div>
53
59
 
54
- <div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
55
- <input
56
- className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
57
- type="submit"
58
- value={msgStr("doRegister")}
59
- disabled={!isFomSubmittable}
60
- />
60
+ <div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
61
+ <input
62
+ className={cx(
63
+ props.kcButtonClass,
64
+ props.kcButtonPrimaryClass,
65
+ props.kcButtonBlockClass,
66
+ props.kcButtonLargeClass,
67
+ )}
68
+ type="submit"
69
+ value={msgStr("doRegister")}
70
+ disabled={!isFomSubmittable}
71
+ />
72
+ </div>
61
73
  </div>
62
- </div>
63
- </form>
64
- }
65
- />
66
- );
67
- });
68
-
69
- type UserProfileFormFieldsProps = { kcContext: KcContextBase.RegisterUserProfile } & KcProps &
74
+ </form>
75
+ }
76
+ />
77
+ );
78
+ },
79
+ );
80
+
81
+ type UserProfileFormFieldsProps = { kcContext: KcContextBase.RegisterUserProfile; useI18n: () => I18n } & KcProps &
70
82
  Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
71
83
  onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
72
84
  };
73
85
 
74
- const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange, ...props }: UserProfileFormFieldsProps) => {
86
+ const UserProfileFormFields = memo(({ kcContext, useI18n, onIsFormSubmittableValueChange, ...props }: UserProfileFormFieldsProps) => {
75
87
  const { cx, css } = useCssAndCx();
76
88
 
77
- const { advancedMsg } = getMsg(kcContext);
89
+ const { advancedMsg } = useI18n();
78
90
 
79
91
  const {
80
92
  formValidationState: { fieldStateByAttributeName, isFormSubmittable },
@@ -82,6 +94,7 @@ const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange,
82
94
  attributesWithPassword,
83
95
  } = useFormValidationSlice({
84
96
  kcContext,
97
+ useI18n,
85
98
  });
86
99
 
87
100
  useEffect(() => {
@@ -1,6 +1,7 @@
1
1
  import { useReducer, useEffect, memo } from "react";
2
2
  import type { ReactNode } from "react";
3
- import { getMsg, getCurrentKcLanguageTag, changeLocale, getTagLabel } from "../i18n";
3
+ import { getCurrentKcLanguageTag, changeLocale, getTagLabel } from "../i18n";
4
+ import type { I18n } from "../i18n";
4
5
  import type { KcLanguageTag } from "../i18n";
5
6
  import type { KcContextBase } from "../getKcContext/KcContextBase";
6
7
  import { assert } from "../tools/assert";
@@ -25,7 +26,7 @@ export type TemplateProps = {
25
26
  * to avoid pulling the default theme assets.
26
27
  */
27
28
  doFetchDefaultThemeResources: boolean;
28
- } & { kcContext: KcContextBase } & KcTemplateProps;
29
+ } & { kcContext: KcContextBase; useI18n: () => I18n } & KcTemplateProps;
29
30
 
30
31
  export const Template = memo((props: TemplateProps) => {
31
32
  const {
@@ -40,6 +41,7 @@ export const Template = memo((props: TemplateProps) => {
40
41
  infoNode = null,
41
42
  kcContext,
42
43
  doFetchDefaultThemeResources,
44
+ useI18n,
43
45
  } = props;
44
46
 
45
47
  const { cx } = useCssAndCx();
@@ -48,7 +50,7 @@ export const Template = memo((props: TemplateProps) => {
48
50
  console.log("Rendering this page with react using keycloakify");
49
51
  }, []);
50
52
 
51
- const { msg } = getMsg(kcContext);
53
+ const { msg } = useI18n();
52
54
 
53
55
  const onChangeLanguageClickFactory = useCallbackFactory(([kcLanguageTag]: [KcLanguageTag]) =>
54
56
  changeLocale({
@@ -2,32 +2,38 @@ import { useReducer, useEffect, memo } from "react";
2
2
  import { Template } from "./Template";
3
3
  import type { KcProps } from "./KcProps";
4
4
  import type { KcContextBase } from "../getKcContext/KcContextBase";
5
- import { getMsg } from "../i18n";
6
5
  import { useCssAndCx } from "tss-react";
7
- import { kcMessages, getCurrentKcLanguageTag } from "../i18n";
8
- import type { KcLanguageTag } from "../i18n";
6
+ import { getCurrentKcLanguageTag } from "../i18n";
7
+ import type { KcLanguageTag, I18n } from "../i18n";
9
8
 
10
9
  /** Allow to avoid bundling the terms and download it on demand*/
11
10
  export function useDownloadTerms(params: {
12
11
  kcContext: KcContextBase;
13
12
  downloadTermMarkdown: (params: { currentKcLanguageTag: KcLanguageTag }) => Promise<string>;
13
+ useI18n: () => I18n;
14
14
  }) {
15
- const { kcContext, downloadTermMarkdown } = params;
15
+ const { kcContext, downloadTermMarkdown, useI18n } = params;
16
16
 
17
17
  const [, forceUpdate] = useReducer(x => x + 1, 0);
18
18
 
19
+ const { evtKcMessages } = useI18n();
20
+
19
21
  useEffect(() => {
20
22
  const currentKcLanguageTag = getCurrentKcLanguageTag(kcContext);
21
23
 
22
24
  downloadTermMarkdown({ currentKcLanguageTag }).then(thermMarkdown => {
23
- kcMessages[currentKcLanguageTag].termsText = thermMarkdown;
25
+ evtKcMessages.$attachOnce(
26
+ kcMessages => (kcMessages !== undefined ? [kcMessages] : null),
27
+ kcMessages => (kcMessages[currentKcLanguageTag].termsText = thermMarkdown),
28
+ );
29
+
24
30
  forceUpdate();
25
31
  });
26
32
  }, []);
27
33
  }
28
34
 
29
- export const Terms = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Terms } & KcProps) => {
30
- const { msg, msgStr } = getMsg(kcContext);
35
+ export const Terms = memo(({ kcContext, useI18n, ...props }: { kcContext: KcContextBase.Terms; useI18n: () => I18n } & KcProps) => {
36
+ const { msg, msgStr } = useI18n();
31
37
 
32
38
  const { cx } = useCssAndCx();
33
39
 
@@ -35,7 +41,7 @@ export const Terms = memo(({ kcContext, ...props }: { kcContext: KcContextBase.T
35
41
 
36
42
  return (
37
43
  <Template
38
- {...{ kcContext, ...props }}
44
+ {...{ kcContext, useI18n, ...props }}
39
45
  doFetchDefaultThemeResources={true}
40
46
  displayMessage={false}
41
47
  headerNode={msg("termsTitle")}
@@ -1,35 +1,18 @@
1
1
  import "minimal-polyfills/Object.fromEntries";
2
2
  //NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
3
3
  import ReactMarkdown from "react-markdown";
4
- import memoize from "memoizee";
5
- import { kcMessages as kcMessagesBase } from "./generated_kcMessages/15.0.2/login";
4
+ import type { kcMessages as t_kcMessages } from "./kcMessages";
6
5
  import { assert } from "tsafe/assert";
7
6
  import type { Equals } from "tsafe";
7
+ import { createObjectThatThrowsIfAccessed } from "../tools/createObjectThatThrowsIfAccessed";
8
+ import { Evt } from "evt";
9
+ import { useRerenderOnStateChange } from "evt/hooks";
10
+ import { useMemo } from "react";
11
+ import type { StatefulReadonlyEvt } from "evt";
8
12
 
9
- export const kcMessages = {
10
- ...kcMessagesBase,
11
- "en": {
12
- ...kcMessagesBase["en"],
13
- "termsText": "⏳",
14
- "shouldBeEqual": "{0} should be equal to {1}",
15
- "shouldBeDifferent": "{0} should be different to {1}",
16
- "shouldMatchPattern": "Pattern should match: `/{0}/`",
17
- "mustBeAnInteger": "Must be an integer",
18
- "notAValidOption": "Not a valid option",
19
- },
20
- "fr": {
21
- ...kcMessagesBase["fr"],
22
- /* spell-checker: disable */
23
- "shouldBeEqual": "{0} doit être egale à {1}",
24
- "shouldBeDifferent": "{0} doit être différent de {1}",
25
- "shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
26
- "mustBeAnInteger": "Doit être un nombre entiers",
27
- "notAValidOption": "N'est pas une option valide",
28
- /* spell-checker: enable */
29
- },
30
- };
13
+ export type KcMessages = typeof t_kcMessages;
31
14
 
32
- export type KcLanguageTag = keyof typeof kcMessages;
15
+ export type KcLanguageTag = keyof KcMessages;
33
16
 
34
17
  export const kcLanguageTags = [
35
18
  "en",
@@ -106,15 +89,16 @@ export function changeLocale(params: {
106
89
  assert(false, "never");
107
90
  }
108
91
 
109
- export type MessageKey = keyof typeof kcMessages["en"];
92
+ export type MessageKey = keyof KcMessages["en"];
110
93
 
111
94
  function resolveMsg<Key extends string, DoRenderMarkdown extends boolean>(props: {
112
95
  key: Key;
113
96
  args: (string | undefined)[];
114
97
  kcLanguageTag: string;
115
98
  doRenderMarkdown: DoRenderMarkdown;
99
+ kcMessages: KcMessages;
116
100
  }): Key extends MessageKey ? (DoRenderMarkdown extends true ? JSX.Element : string) : undefined {
117
- const { key, args, kcLanguageTag, doRenderMarkdown } = props;
101
+ const { key, args, kcLanguageTag, doRenderMarkdown, kcMessages } = props;
118
102
 
119
103
  let str = kcMessages[kcLanguageTag as any as "en"][key as MessageKey] ?? kcMessages["en"][key as MessageKey];
120
104
 
@@ -160,8 +144,9 @@ function resolveMsgAdvanced<Key extends string, DoRenderMarkdown extends boolean
160
144
  args: (string | undefined)[];
161
145
  kcLanguageTag: string;
162
146
  doRenderMarkdown: DoRenderMarkdown;
147
+ kcMessages: KcMessages;
163
148
  }): DoRenderMarkdown extends true ? JSX.Element : string {
164
- const { key, args, kcLanguageTag, doRenderMarkdown } = props;
149
+ const { key, args, kcLanguageTag, doRenderMarkdown, kcMessages } = props;
165
150
 
166
151
  const match = key.match(/^\$\{([^{]+)\}$/);
167
152
 
@@ -172,35 +157,77 @@ function resolveMsgAdvanced<Key extends string, DoRenderMarkdown extends boolean
172
157
  args,
173
158
  kcLanguageTag,
174
159
  doRenderMarkdown,
160
+ kcMessages,
175
161
  });
176
162
 
177
163
  return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
178
164
  }
179
165
 
166
+ export type I18n = {
167
+ msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element;
168
+ msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
169
+ advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
170
+ advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element;
171
+ evtKcMessages: StatefulReadonlyEvt<KcMessages | undefined>;
172
+ };
173
+
180
174
  /**
181
- * When the language is switched the page is reloaded, this may appear
182
- * as a bug as you might notice that the language successfully switch before
183
- * reload.
184
- * However we need to tell Keycloak that the user have changed the language
185
- * during login so we can retrieve the "local" field of the JWT encoded accessToken.
186
- * https://user-images.githubusercontent.com/6702424/138096682-351bb61f-f24e-4caf-91b7-cca8cfa2cb58.mov
187
- *
188
175
  * advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied")
189
176
  * advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key"
190
- *
191
- *
192
- * NOTE: This function is memoized, it always returns the same object for a given kcContext)
193
- *
194
177
  */
195
- export const getMsg = memoize((kcContext: KcContextLike) => {
178
+ export function createUseI18n(props: { kcMessages: KcMessages | (() => Promise<KcMessages>); kcContext: KcContextLike | undefined }) {
179
+ const { kcContext, kcMessages: kcMessagesOrFetchKcMessages } = props;
180
+
181
+ if (kcContext === undefined) {
182
+ return createObjectThatThrowsIfAccessed({ "debugMessage": "Can't use Keycloakify i18n outside of keycloak" });
183
+ }
184
+
185
+ const { evtKcMessages } = (() => {
186
+ const evtKcMessages = Evt.create<KcMessages | undefined>(undefined);
187
+
188
+ if (typeof kcMessagesOrFetchKcMessages === "function") {
189
+ kcMessagesOrFetchKcMessages().then(kcMessages => (evtKcMessages.state = kcMessages));
190
+ } else {
191
+ evtKcMessages.state = kcMessagesOrFetchKcMessages;
192
+ }
193
+
194
+ return { evtKcMessages };
195
+ })();
196
+ Evt.factorize(
197
+ typeof kcMessagesOrFetchKcMessages === "function"
198
+ ? Evt.from(kcMessagesOrFetchKcMessages()).toStateful()
199
+ : Evt.create(kcMessagesOrFetchKcMessages),
200
+ );
201
+
196
202
  const kcLanguageTag = getCurrentKcLanguageTag(kcContext);
197
203
 
198
- return {
199
- "msgStr": (key: MessageKey, ...args: (string | undefined)[]): string => resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": false }),
200
- "msg": (key: MessageKey, ...args: (string | undefined)[]): JSX.Element => resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": true }),
201
- "advancedMsg": <Key extends string>(key: Key, ...args: (string | undefined)[]): JSX.Element =>
202
- resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": true }),
203
- "advancedMsgStr": <Key extends string>(key: Key, ...args: (string | undefined)[]): string =>
204
- resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": false }),
205
- };
206
- });
204
+ function useI18n() {
205
+ useRerenderOnStateChange(evtKcMessages);
206
+
207
+ const i18n = useMemo((): I18n => {
208
+ const kcMessages = evtKcMessages.state;
209
+
210
+ if (kcMessages === undefined) {
211
+ return {
212
+ "msgStr": () => "",
213
+ "msg": () => <></>,
214
+ "advancedMsg": () => <></>,
215
+ "advancedMsgStr": () => "",
216
+ evtKcMessages,
217
+ };
218
+ }
219
+
220
+ return {
221
+ "msgStr": (key, ...args) => resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": false, kcMessages }),
222
+ "msg": (key, ...args): JSX.Element => resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": true, kcMessages }),
223
+ "advancedMsg": (key, ...args) => resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": true, kcMessages }),
224
+ "advancedMsgStr": (key, ...args) => resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": false, kcMessages }),
225
+ evtKcMessages,
226
+ };
227
+ }, [evtKcMessages.state]);
228
+
229
+ return i18n;
230
+ }
231
+
232
+ return { useI18n };
233
+ }
@@ -0,0 +1,24 @@
1
+ import { kcMessages as kcMessagesBase } from "./generated_kcMessages/15.0.2/login";
2
+
3
+ export const kcMessages = {
4
+ ...kcMessagesBase,
5
+ "en": {
6
+ ...kcMessagesBase["en"],
7
+ "termsText": "⏳",
8
+ "shouldBeEqual": "{0} should be equal to {1}",
9
+ "shouldBeDifferent": "{0} should be different to {1}",
10
+ "shouldMatchPattern": "Pattern should match: `/{0}/`",
11
+ "mustBeAnInteger": "Must be an integer",
12
+ "notAValidOption": "Not a valid option",
13
+ },
14
+ "fr": {
15
+ ...kcMessagesBase["fr"],
16
+ /* spell-checker: disable */
17
+ "shouldBeEqual": "{0} doit être egale à {1}",
18
+ "shouldBeDifferent": "{0} doit être différent de {1}",
19
+ "shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
20
+ "mustBeAnInteger": "Doit être un nombre entiers",
21
+ "notAValidOption": "N'est pas une option valide",
22
+ /* spell-checker: enable */
23
+ },
24
+ };
@@ -0,0 +1,18 @@
1
+ export function createObjectThatThrowsIfAccessed<T extends object>(params?: { debugMessage?: string }): T {
2
+ if (typeof Proxy === "undefined") {
3
+ return null as any;
4
+ }
5
+
6
+ const { debugMessage = "" } = params ?? {};
7
+
8
+ const get: NonNullable<ProxyHandler<T>["get"]> = (...args) => {
9
+ const [, prop] = args;
10
+
11
+ throw new Error(`Cannot access ${String(prop)} yet ${debugMessage}`);
12
+ };
13
+
14
+ return new Proxy<T>({} as any, {
15
+ get,
16
+ "set": get,
17
+ });
18
+ }
@@ -1,11 +1,10 @@
1
1
  import "./tools/Array.prototype.every";
2
2
  import { useMemo, useReducer, Fragment } from "react";
3
3
  import type { KcContextBase, Validators, Attribute } from "./getKcContext/KcContextBase";
4
- import { getMsg } from "./i18n";
5
4
  import type { KcLanguageTag } from "./i18n";
6
5
  import { useConstCallback } from "powerhooks/useConstCallback";
7
6
  import { id } from "tsafe/id";
8
- import type { MessageKey } from "./i18n";
7
+ import type { I18n, MessageKey } from "./i18n";
9
8
  import { emailRegexp } from "./tools/emailRegExp";
10
9
 
11
10
  export function useGetErrors(params: {
@@ -16,15 +15,16 @@ export function useGetErrors(params: {
16
15
  };
17
16
  locale?: { currentLanguageTag: KcLanguageTag };
18
17
  };
18
+ useI18n: () => I18n;
19
19
  }) {
20
- const { kcContext } = params;
20
+ const { kcContext, useI18n } = params;
21
21
 
22
22
  const {
23
23
  messagesPerField,
24
24
  profile: { attributes },
25
25
  } = kcContext;
26
26
 
27
- const { msg, msgStr, advancedMsg, advancedMsgStr } = getMsg(kcContext);
27
+ const { msg, msgStr, advancedMsg, advancedMsgStr } = useI18n();
28
28
 
29
29
  const getErrors = useConstCallback((params: { name: string; fieldValueByAttributeName: Record<string, { value: string }> }) => {
30
30
  const { name, fieldValueByAttributeName } = params;
@@ -314,11 +314,13 @@ export function useFormValidationSlice(params: {
314
314
  passwordRequired: boolean;
315
315
  realm: { registrationEmailAsUsername: boolean };
316
316
  };
317
+ useI18n: () => I18n;
317
318
  /** NOTE: Try to avoid passing a new ref every render for better performances. */
318
319
  passwordValidators?: Validators;
319
320
  }) {
320
321
  const {
321
322
  kcContext,
323
+ useI18n,
322
324
  passwordValidators = {
323
325
  "length": {
324
326
  "ignore.empty.value": true,
@@ -383,6 +385,7 @@ export function useFormValidationSlice(params: {
383
385
  "attributes": attributesWithPassword,
384
386
  },
385
387
  },
388
+ useI18n,
386
389
  });
387
390
 
388
391
  const initialInternalState = useMemo(