-
Notifications
You must be signed in to change notification settings - Fork 21
#4977 Multilingual support for claims title and its value #79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,7 @@ | ||
| // Copyright 2026 The ThunderID Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { | ||
| type ConsentPurposeData, | ||
| FlowMetadataResponse, | ||
| PromptElement, | ||
| resolveFlowTemplateLiterals, | ||
| } from '@thunderid/browser'; | ||
| import {type ConsentPurposeData, PromptElement} from '@thunderid/browser'; | ||
| import {type ChangeEvent, FC, ReactNode} from 'react'; | ||
| import ConsentCheckboxList, {getConsentOptionalKey} from './ConsentCheckboxList'; | ||
| import Typography from '../primitives/Typography/Typography'; | ||
|
|
@@ -41,8 +36,10 @@ export interface ConsentRenderProps { | |
| export interface ConsentConfig { | ||
| essential?: string; | ||
| optional?: string; | ||
| permission?: string; | ||
| essentialInfo?: string; | ||
| optionalInfo?: string; | ||
| permissionInfo?: string; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -78,41 +75,29 @@ export interface ConsentProps { | |
| * Callback invoked when a user toggles an optional attribute. | ||
| */ | ||
| onInputChange: (name: string, value: string) => void; | ||
| /** | ||
| * Config to modified detail in consent page | ||
| */ | ||
| config?: Record<string, unknown>; | ||
|
|
||
| /** | ||
| * Config of meta response | ||
| */ | ||
| meta?: FlowMetadataResponse | null; | ||
|
|
||
| /** | ||
| * translation data | ||
| */ | ||
| t?: UseTranslation['t']; | ||
| } | ||
|
|
||
| const defaultConfig: Required<Pick<ConsentConfig, 'essential' | 'optional'>> = { | ||
| essential: 'Essential Attributtes', | ||
| optional: 'Optional Attributes', | ||
| // default config for consent related translation keys | ||
| const defaultConfig: ConsentConfig = { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll need to revisit this again. But okay to proceed with this for now |
||
| essential: 'essential_claims', | ||
| optional: 'optional_claims', | ||
| permission: 'authorize_scope', | ||
| essentialInfo: 'essential_claims_info', | ||
| optionalInfo: 'optional_claims_info', | ||
| permissionInfo: 'authorize_scope_info', | ||
|
Comment on lines
+86
to
+92
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These Also, the |
||
| }; | ||
|
|
||
| /** | ||
| * Consent component renders the list of purposes and their associated attributes (essential and optional) | ||
| * based on the data provided by the backend. It allows users to toggle optional attributes while essential | ||
| * attributes are displayed as read-only. | ||
| */ | ||
| const Consent: FC<ConsentProps> = ({ | ||
| consentData, | ||
| formValues, | ||
| config: suppliedConfig = {}, | ||
| onInputChange, | ||
| children, | ||
| meta, | ||
| t, | ||
| }: ConsentProps) => { | ||
| const Consent: FC<ConsentProps> = ({consentData, formValues, onInputChange, children, t}: ConsentProps) => { | ||
| // Computed per render (not at module scope): a CSP nonce configured on <ThunderIDProvider> | ||
| // isn't known until the provider renders, and Emotion needs it applied before any style | ||
| // insertion happens. These are static, so Emotion's own cache dedupes the repeat calls to a | ||
|
|
@@ -135,19 +120,27 @@ const Consent: FC<ConsentProps> = ({ | |
| }); | ||
| const optionalSectionLabelClass: string = css({alignItems: 'center', display: 'flex', gap: '4px'}); | ||
|
|
||
| /** Resolve any remaining {{t()}} or {{meta()}} template expressions in a string at render time. */ | ||
| /** Resolve i18n keys */ | ||
| const resolve = (text: string | undefined): string => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a diff in the two resolve functions. Here we're returning "" (empty) if a key is not found. However in the resolve() function in packages/react/src/components/adapters/ConsentCheckboxList.tsx, we're returning the key as it is. Shall we modify the resolve() function here also to display the key if the translation is not found?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also prefer to get rid of the duplicated resolve() functions |
||
| if (!text || (!t && !meta)) { | ||
| if (!text || !t) { | ||
| return text || ''; | ||
| } | ||
| return resolveFlowTemplateLiterals(text, {meta, t: t || ((k: string): string => k)}); | ||
|
|
||
| const key: string = `consent.${text}`; | ||
| const translated: string = t(key); | ||
| return translated === key ? '' : translated; | ||
| }; | ||
|
|
||
| const config: ConsentConfig = {...defaultConfig, ...suppliedConfig}; | ||
| const essentialInfo = typeof config.essentialInfo === 'string' ? resolve(config.essentialInfo.trim()) : ''; | ||
| const optionalInfo = typeof config.optionalInfo === 'string' ? resolve(config.optionalInfo.trim()) : ''; | ||
| const essentialLabel = resolve(config['essential']); | ||
| const optionalLabel = resolve(config['optional']); | ||
| const essentialInfo = resolve(defaultConfig['essentialInfo']); | ||
| const optionalInfo = resolve(defaultConfig['optionalInfo']); | ||
| const permissionInfo = resolve(defaultConfig['permissionInfo']); | ||
| /** | ||
| * Falls back to default config values if essential/optional | ||
| * keys cannot be resolved via translation files . | ||
| */ | ||
| const essentialLabel = resolve(defaultConfig['essential']) || 'Essential Attributes'; | ||
| const optionalLabel = resolve(defaultConfig['optional']) || 'Optional Attributes'; | ||
| const permissionLabel = resolve(defaultConfig['permission']) || 'Permissions'; | ||
|
|
||
| /** | ||
| * Method to check whether master toggle button is checked or not | ||
|
|
@@ -223,6 +216,7 @@ const Consent: FC<ConsentProps> = ({ | |
| purpose={purpose} | ||
| formValues={formValues} | ||
| onInputChange={onInputChange} | ||
| t={t} | ||
| /> | ||
| </div> | ||
| )} | ||
|
|
@@ -232,10 +226,11 @@ const Consent: FC<ConsentProps> = ({ | |
| <div className={optionalSectionHeaderClass}> | ||
| <div className={optionalSectionLabelClass}> | ||
| <Typography variant="subtitle2" fontWeight="bold"> | ||
| {purpose.type === 'permissions' ? 'Permissions' : optionalLabel} | ||
| {purpose.type === 'permissions' ? permissionLabel : optionalLabel} | ||
| </Typography> | ||
| {optionalInfo !== '' && ( | ||
| <Tooltip helperText={optionalInfo}> | ||
| {/* Show tooltip for optional claims/permissions according to their type */} | ||
| {Boolean(purpose.type === 'permissions' ? permissionInfo : optionalInfo) && ( | ||
| <Tooltip helperText={purpose.type === 'permissions' ? permissionInfo : optionalInfo}> | ||
| <Info width="1rem" height="1rem" /> | ||
| </Tooltip> | ||
| )} | ||
|
|
@@ -252,6 +247,7 @@ const Consent: FC<ConsentProps> = ({ | |
| purpose={purpose} | ||
| formValues={formValues} | ||
| onInputChange={onInputChange} | ||
| t={t} | ||
| /> | ||
| </div> | ||
| )} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import useTheme from '../../contexts/Theme/useTheme'; | |
| import {cx} from '../../styles/emotion'; | ||
| import Toggle from '../primitives/Toggle/Toggle'; | ||
| import Typography from '../primitives/Typography/Typography'; | ||
| import {UseTranslation} from '../../hooks/useTranslation'; | ||
|
|
||
| /** | ||
| * Computes the form value key for tracking an optional attribute's consent state. | ||
|
|
@@ -78,6 +79,10 @@ export interface ConsentCheckboxListProps { | |
| purpose: ConsentPurposeData; | ||
| /** Whether to render essential (disabled) or optional (toggleable) attributes */ | ||
| variant: ConsentInputVariant; | ||
| /** | ||
| * translation data | ||
| */ | ||
| t?: UseTranslation['t']; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -93,10 +98,21 @@ const ConsentCheckboxList: FC<ConsentCheckboxListProps> = ({ | |
| formValues, | ||
| onInputChange, | ||
| children, | ||
| t, | ||
| }: ConsentCheckboxListProps) => { | ||
| const {theme, colorScheme}: ReturnType<typeof useTheme> = useTheme(); | ||
| const styles: Record<string, string> = useStyles(theme, colorScheme); | ||
|
|
||
| const resolve = (text: string | undefined): string => { | ||
| if (!text || !t) { | ||
| return text || ''; | ||
| } | ||
|
|
||
| const key: string = `consent.${text}`; | ||
| const translated: string = t(key); | ||
| return translated === key ? text : translated; | ||
| }; | ||
|
|
||
| const attributes: string[] = (variant === 'ESSENTIAL' ? purpose.essential : purpose.optional).map( | ||
| (e): string => e.name, | ||
| ); | ||
|
|
@@ -153,11 +169,11 @@ const ConsentCheckboxList: FC<ConsentCheckboxListProps> = ({ | |
| styles['typography'], | ||
| )} | ||
| > | ||
| {attr} | ||
| {resolve(attr)} | ||
| </Typography> | ||
| </div> | ||
| {isEssential ? ( | ||
| <Typography variant="body2">Required</Typography> | ||
| <Typography variant="body2">{resolve('required')}</Typography> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a default translation for this? It seems fallback translation for |
||
| ) : ( | ||
| <Toggle | ||
| id={inputId} | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.