diff --git a/js/apps/account-ui/public/locales/en/translation.json b/js/apps/account-ui/public/locales/en/translation.json index b0e1f275b88..79b8918f5b5 100644 --- a/js/apps/account-ui/public/locales/en/translation.json +++ b/js/apps/account-ui/public/locales/en/translation.json @@ -115,6 +115,7 @@ "resourceSharedWith_one": "Resource is shared with <0>{{username}}", "resourceSharedWith_other": "Resource is shared with <0>{{username}} and <1>{{other}} other users", "resourceSharedWith_zero": "This resource is not shared.", + "selectALocale": "Select a locale", "selectOne": "Select an option", "setUpNew": "Set up {{0}}", "share": "Share", diff --git a/js/apps/account-ui/src/api/methods.ts b/js/apps/account-ui/src/api/methods.ts index ed9053f5df1..af8bca3ffd2 100644 --- a/js/apps/account-ui/src/api/methods.ts +++ b/js/apps/account-ui/src/api/methods.ts @@ -29,6 +29,13 @@ export async function getPersonalInfo({ return parseResponse(response); } +export async function supportedLocales({ signal }: CallOptions = {}): Promise< + string[] +> { + const response = await request("/supportedLocales", { signal }); + return parseResponse(response); +} + export async function savePersonalInfo( info: UserRepresentation ): Promise { diff --git a/js/apps/account-ui/src/personal-info/FormField.tsx b/js/apps/account-ui/src/personal-info/FormField.tsx index ce41c9383be..0a1b37a67fe 100644 --- a/js/apps/account-ui/src/personal-info/FormField.tsx +++ b/js/apps/account-ui/src/personal-info/FormField.tsx @@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next"; import { KeycloakTextInput } from "ui-shared"; import { UserProfileAttributeMetadata } from "../api/representations"; import { TFuncKey } from "../i18n"; +import { LocaleSelector } from "./LocaleSelector"; import { fieldName, isBundleKey, unWrap } from "./PersonalInfo"; type FormFieldProps = { @@ -25,6 +26,7 @@ export const FormField = ({ attribute }: FormFieldProps) => { const isSelect = (attribute: UserProfileAttributeMetadata) => Object.hasOwn(attribute.validators, "options"); + if (attribute.name === "locale") return ; return ( { + try { + return new Intl.DisplayNames([locale], { type: "language" }).of(locale); + } catch (error) { + return locale; + } +}; + +export const LocaleSelector = () => { + const { t } = useTranslation(); + const [locales, setLocales] = useState([]); + + usePromise( + (signal) => supportedLocales({ signal }), + (locales) => + setLocales( + locales.map( + (l) => + ({ + key: l, + value: localeToDisplayName(l), + } as Option) + ) + ) + ); + + return ( + + ); +}; diff --git a/js/libs/ui-shared/src/controls/SelectControl.tsx b/js/libs/ui-shared/src/controls/SelectControl.tsx index a3e76982135..a8b78309e27 100644 --- a/js/libs/ui-shared/src/controls/SelectControl.tsx +++ b/js/libs/ui-shared/src/controls/SelectControl.tsx @@ -15,7 +15,7 @@ import { } from "@patternfly/react-core"; import { FormLabel } from "./FormLabel"; -type Option = { +export type Option = { key: string; value: string; }; @@ -42,6 +42,7 @@ export const SelectControl = < label, options, controller, + variant, ...rest }: SelectControlProps) => { const { @@ -65,13 +66,23 @@ export const SelectControl = < {...rest} toggleId={name} onToggle={(isOpen) => setOpen(isOpen)} - selections={value} + selections={ + typeof options[0] !== "string" + ? (options as Option[]).find((o) => o.key === value[0]) + ?.value || value + : value + } onSelect={(_, v) => { - const option = v.toString(); - if (value.includes(option)) { - onChange(value.filter((item: string) => item !== option)); + if (variant === "typeaheadmulti") { + const option = v.toString(); + if (value.includes(option)) { + onChange(value.filter((item: string) => item !== option)); + } else { + onChange([...value, option]); + } } else { - onChange([...value, option]); + onChange([v]); + setOpen(false); } }} onClear={(event) => { @@ -79,6 +90,7 @@ export const SelectControl = < onChange([]); }} isOpen={open} + variant={variant} validated={ errors[name] ? ValidatedOptions.error : ValidatedOptions.default } diff --git a/js/libs/ui-shared/src/main.ts b/js/libs/ui-shared/src/main.ts index 1f7124fa5ec..9a56f3140ce 100644 --- a/js/libs/ui-shared/src/main.ts +++ b/js/libs/ui-shared/src/main.ts @@ -1,5 +1,6 @@ export { ContinueCancelModal } from "./continue-cancel/ContinueCancelModal"; export { SelectControl } from "./controls/SelectControl"; +export type { Option } from "./controls/SelectControl"; export { SwitchControl } from "./controls/SwitchControl"; export { TextControl } from "./controls/TextControl"; export { TextAreaControl } from "./controls/TextAreaControl"; diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java index 1b8189a8c0a..d57f7590640 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java @@ -284,6 +284,12 @@ public class AccountRestService { return new ResourcesService(session, user, auth, request); } + @Path("supportedLocales") + @GET + public List supportedLocales() { + return auth.getRealm().getSupportedLocalesStream().collect(Collectors.toList()); + } + private ClientRepresentation modelToRepresentation(ClientModel model, List inUseClients, List offlineClients, Map consents) { ClientRepresentation representation = new ClientRepresentation(); representation.setClientId(model.getClientId());