@redacto.io/consent-sdk-react 10.0.0-beta.0 → 10.0.0-beta.2

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 (66) hide show
  1. package/.turbo/turbo-build.log +22 -25
  2. package/CHANGELOG.md +20 -0
  3. package/dist/index.d.mts +13 -6
  4. package/dist/index.d.ts +13 -6
  5. package/dist/index.js +6 -8
  6. package/dist/index.mjs +6 -8
  7. package/dist/privacy-center.d.mts +62 -27
  8. package/dist/privacy-center.d.ts +62 -27
  9. package/dist/privacy-center.js +1940 -1365
  10. package/dist/privacy-center.mjs +1864 -1284
  11. package/package.json +1 -1
  12. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +1 -0
  13. package/src/RedactoNoticeConsent/api/index.ts +4 -4
  14. package/src/RedactoNoticeConsent/api/types.ts +3 -2
  15. package/src/RedactoNoticeConsent/types.ts +1 -1
  16. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +1 -0
  17. package/src/RedactoNoticeConsentInline/api/index.ts +4 -4
  18. package/src/RedactoNoticeConsentInline/api/types.ts +2 -1
  19. package/src/RedactoNoticeConsentInline/types.ts +1 -1
  20. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +8 -5
  21. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +1 -1
  22. package/src/RedactoPrivacyCenter/api/actions.ts +24 -5
  23. package/src/RedactoPrivacyCenter/api/client.ts +101 -21
  24. package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +64 -35
  25. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +7 -0
  26. package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +19 -2
  27. package/src/RedactoPrivacyCenter/components/Form.tsx +7 -3
  28. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +1 -1
  29. package/src/RedactoPrivacyCenter/components/SelectUser.tsx +83 -0
  30. package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +193 -0
  31. package/src/RedactoPrivacyCenter/components/constant.ts +11 -0
  32. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +1 -1
  33. package/src/RedactoPrivacyCenter/context/ProfileSwitchContext.tsx +15 -0
  34. package/src/RedactoPrivacyCenter/context/types.ts +1 -1
  35. package/src/RedactoPrivacyCenter/lib/types.ts +63 -2
  36. package/src/RedactoPrivacyCenter/locales/as/translation.json +9 -1
  37. package/src/RedactoPrivacyCenter/locales/bho/translation.json +9 -1
  38. package/src/RedactoPrivacyCenter/locales/bn/translation.json +9 -1
  39. package/src/RedactoPrivacyCenter/locales/brx/translation.json +9 -1
  40. package/src/RedactoPrivacyCenter/locales/doi/translation.json +9 -1
  41. package/src/RedactoPrivacyCenter/locales/en/translation.json +9 -1
  42. package/src/RedactoPrivacyCenter/locales/gom/translation.json +9 -1
  43. package/src/RedactoPrivacyCenter/locales/gu/translation.json +9 -1
  44. package/src/RedactoPrivacyCenter/locales/hi/translation.json +9 -1
  45. package/src/RedactoPrivacyCenter/locales/kn/translation.json +9 -1
  46. package/src/RedactoPrivacyCenter/locales/ks/translation.json +9 -1
  47. package/src/RedactoPrivacyCenter/locales/mai/translation.json +9 -1
  48. package/src/RedactoPrivacyCenter/locales/ml/translation.json +9 -1
  49. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +9 -1
  50. package/src/RedactoPrivacyCenter/locales/mr/translation.json +9 -1
  51. package/src/RedactoPrivacyCenter/locales/ne/translation.json +9 -1
  52. package/src/RedactoPrivacyCenter/locales/or/translation.json +9 -1
  53. package/src/RedactoPrivacyCenter/locales/pa/translation.json +9 -1
  54. package/src/RedactoPrivacyCenter/locales/sa/translation.json +9 -1
  55. package/src/RedactoPrivacyCenter/locales/sat/translation.json +9 -1
  56. package/src/RedactoPrivacyCenter/locales/sd/translation.json +9 -1
  57. package/src/RedactoPrivacyCenter/locales/ta/translation.json +9 -1
  58. package/src/RedactoPrivacyCenter/locales/te/translation.json +9 -1
  59. package/src/RedactoPrivacyCenter/locales/ur/translation.json +9 -1
  60. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +105 -0
  61. package/tests/Form.test.tsx +53 -7
  62. package/tests/ManageConsentErrorMapping.test.tsx +85 -0
  63. package/tests/RevocablePurpose.test.tsx +146 -0
  64. package/tests/RevokeWarningMessage.test.tsx +151 -0
  65. package/tests/consentClient.test.ts +171 -0
  66. package/tests/manageConsentServer.test.ts +38 -0
@@ -32,7 +32,7 @@ export const ModifyConsentModal = ({
32
32
  action: 'revoke' as ConsentAction,
33
33
  buttonLabel: t('revokeConsent'),
34
34
  buttonVariant: 'danger' as const,
35
- warningMessage: t('revokeConsentWarning'),
35
+ warningMessage: selectedRecord.revoke_warning_message || t('revokeConsentWarning'),
36
36
  description: nominatorLabel
37
37
  ? t('aboutToModifyConsentOnBehalf', { nominator: nominatorLabel })
38
38
  : t('aboutToModifyConsent'),
@@ -0,0 +1,83 @@
1
+ import { ChevronRight } from 'lucide-react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { PCText, PCTitle } from '../ui';
4
+ import type { SelectUserProps } from '../lib/types';
5
+
6
+ /**
7
+ * "Choose your profile" picker, shown when the signed-in contact maps to more
8
+ * than one data principal (shared contact). Selecting one re-scopes the session
9
+ * to that principal uuid. Purely presentational — SelectUserGate owns the fetch
10
+ * and the re-scope. Structural styles live in styles/injectStyles.ts
11
+ * (.pc-select-user*).
12
+ */
13
+ export const SelectUser = ({
14
+ email,
15
+ candidates,
16
+ onSelect,
17
+ busyOrgUserId,
18
+ error,
19
+ }: SelectUserProps) => {
20
+ const { t } = useTranslation();
21
+ const subtitle = email
22
+ ? t('emailLinkedToMultipleProfiles', { email })
23
+ : t('contactLinkedToMultipleProfiles');
24
+
25
+ // A candidate with no uuid can't be selected (it's the value we submit),
26
+ // so drop it rather than render an unclickable row.
27
+ const selectable = candidates.filter((candidate) => candidate.uuid);
28
+
29
+ return (
30
+ <div className="pc-select-user">
31
+ <div className="pc-select-user__header">
32
+ <PCTitle as="h2">{t('chooseYourProfile')}</PCTitle>
33
+ <PCText as="p" size="base" color="secondary" align="center">
34
+ {subtitle}
35
+ </PCText>
36
+ </div>
37
+
38
+ <div className="pc-select-user__list">
39
+ {selectable.map((candidate) => {
40
+ const principalUuid = candidate.uuid || '';
41
+ const primaryLabel =
42
+ candidate.primary_email || candidate.primary_mobile || candidate.uuid;
43
+ const secondaryLabel =
44
+ candidate.primary_mobile || candidate.primary_email || '';
45
+ const isBusy = busyOrgUserId === principalUuid;
46
+ return (
47
+ <button
48
+ key={candidate.uuid}
49
+ type="button"
50
+ className="pc-select-user__item"
51
+ onClick={() => onSelect(principalUuid)}
52
+ disabled={!principalUuid || busyOrgUserId !== null}
53
+ >
54
+ <span className="pc-select-user__item-text">
55
+ <span className="pc-select-user__item-primary">
56
+ {primaryLabel}
57
+ </span>
58
+ {secondaryLabel ? (
59
+ <span className="pc-select-user__item-secondary">
60
+ {secondaryLabel}
61
+ </span>
62
+ ) : null}
63
+ </span>
64
+ {isBusy ? (
65
+ <span className="pc-select-user__item-status">
66
+ {t('signingInEllipsis')}
67
+ </span>
68
+ ) : (
69
+ <ChevronRight size={18} className="pc-select-user__item-chevron" />
70
+ )}
71
+ </button>
72
+ );
73
+ })}
74
+ </div>
75
+
76
+ {error ? (
77
+ <p className="pc-select-user__error" role="alert">
78
+ {error}
79
+ </p>
80
+ ) : null}
81
+ </div>
82
+ );
83
+ };
@@ -0,0 +1,193 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useState,
6
+ type ReactNode,
7
+ } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { useOrganization } from '../context/OrganizationContext';
10
+ import { useBaseUrl } from '../context/BaseUrlContext';
11
+ import { useAuth } from '../context/AuthContext';
12
+ import { ProfileSwitchProvider } from '../context/ProfileSwitchContext';
13
+ import { PCLoader } from '../ui';
14
+ import { decodeJwtToken } from '../lib/utils';
15
+ import type {
16
+ GatePhase,
17
+ IdentityCandidate,
18
+ SelectUserGateProps,
19
+ } from '../lib/types';
20
+ import { PICKED_ORG_USER_KEY } from './constant';
21
+ import { SelectUser } from './SelectUser';
22
+
23
+ /**
24
+ * Shared-contact profile gate. On mount it asks the backend which principals
25
+ * share the signed-in contact; when more than one exists (and the session isn't
26
+ * already pinned to a chosen one), it renders the "choose your profile" picker
27
+ * instead of the consent view. Picking re-scopes the session via updateTokens,
28
+ * then renders children. It sits OUTSIDE DataAvailabilityProvider so no consent
29
+ * fetch runs against an unscoped token, and degrades to children on any error.
30
+ *
31
+ * Once resolved it also exposes `switchProfile` via ProfileSwitchContext, so a
32
+ * signed-in shared contact can re-open the picker and move to another profile.
33
+ */
34
+ export const SelectUserGate = ({ children }: SelectUserGateProps) => {
35
+ const { t } = useTranslation();
36
+ const { organization } = useOrganization();
37
+ const organizationId = organization?.uuid || '';
38
+ const workspaceId = organization?.workspaces?.[0]?.uuid || '';
39
+ const { consentClient } = useBaseUrl();
40
+ const { accessToken, updateTokens } = useAuth();
41
+
42
+ const [phase, setPhase] = useState<GatePhase>('checking');
43
+ const [candidates, setCandidates] = useState<IdentityCandidate[]>([]);
44
+ const [busyOrgUserId, setBusyOrgUserId] = useState<string | null>(null);
45
+ const [selectError, setSelectError] = useState<string | null>(null);
46
+
47
+ // The token's principal. Routine refreshes (checkAndRefreshAuth,
48
+ // refreshOtpToken) hand back a fresh token string for the SAME principal;
49
+ // keying the gate check off this decoded id rather than the raw token keeps
50
+ // those refreshes from re-running the check and flashing the picker/spinner.
51
+ const tokenOrgUserId =
52
+ (decodeJwtToken(accessToken)?.user_data?.org_user_id as string) || '';
53
+
54
+ useEffect(() => {
55
+ let cancelled = false;
56
+
57
+ if (!organizationId || !workspaceId || !accessToken) {
58
+ setPhase('resolved');
59
+ return;
60
+ }
61
+
62
+ // Already pinned to a chosen principal this session? Pass straight through
63
+ // to children — but STILL resolve the sibling list in the background so the
64
+ // user menu can offer "switch profile". The default/unscoped token has
65
+ // org_user_id = the contact and no stored pick, so the picker still shows.
66
+ const picked =
67
+ typeof window !== 'undefined'
68
+ ? sessionStorage.getItem(PICKED_ORG_USER_KEY)
69
+ : null;
70
+ const isPinned = Boolean(picked) && picked === tokenOrgUserId;
71
+
72
+ // Only block on the spinner when the picker might actually open; a pinned
73
+ // session renders children immediately and resolves siblings quietly.
74
+ setPhase(isPinned ? 'resolved' : 'checking');
75
+ (async () => {
76
+ try {
77
+ const res = await consentClient.listIdentities({
78
+ organisationUuid: organizationId,
79
+ workspaceUuid: workspaceId,
80
+ authToken: accessToken,
81
+ });
82
+ if (cancelled) {
83
+ return;
84
+ }
85
+ const list = res?.identities ?? [];
86
+ setCandidates(list);
87
+ if (list.length > 1 && !isPinned) {
88
+ setPhase('picking');
89
+ } else {
90
+ setPhase('resolved');
91
+ }
92
+ } catch {
93
+ // Ledger/endpoint unavailable — never block the Privacy Center.
94
+ if (!cancelled) {
95
+ setCandidates([]);
96
+ setPhase('resolved');
97
+ }
98
+ }
99
+ })();
100
+
101
+ return () => {
102
+ cancelled = true;
103
+ };
104
+ // consentClient is a stable factory result; re-running on its identity would
105
+ // risk a loop. Re-evaluate only when the tenant or the token's principal
106
+ // changes — not on every raw-token refresh. accessToken is read inside for
107
+ // the fetch but intentionally omitted so same-principal refreshes are no-ops.
108
+ // eslint-disable-next-line react-hooks/exhaustive-deps
109
+ }, [organizationId, workspaceId, tokenOrgUserId]);
110
+
111
+ const handleSelect = async (principalUuid: string) => {
112
+ if (!principalUuid || busyOrgUserId) {
113
+ return;
114
+ }
115
+ setBusyOrgUserId(principalUuid);
116
+ setSelectError(null);
117
+ try {
118
+ const res = await consentClient.selectPrincipal(
119
+ {
120
+ organisationUuid: organizationId,
121
+ workspaceUuid: workspaceId,
122
+ authToken: accessToken,
123
+ },
124
+ principalUuid,
125
+ );
126
+ if (!res?.token || !res?.refresh_token) {
127
+ throw new Error('select-principal returned no tokens');
128
+ }
129
+ // The picker selects by opaque uuid, but the "already pinned" check
130
+ // compares against the token's own org_user_id. Derive the pin from the
131
+ // NEW token so it matches on later reloads — it's the signed-in
132
+ // principal's own id, not a co-principal's, so nothing leaks.
133
+ if (typeof window !== 'undefined') {
134
+ const pinnedOrgUserId = decodeJwtToken(res.token)?.user_data
135
+ ?.org_user_id as string | undefined;
136
+ if (pinnedOrgUserId) {
137
+ sessionStorage.setItem(PICKED_ORG_USER_KEY, pinnedOrgUserId);
138
+ }
139
+ }
140
+ // Resolve before swapping the token so the picker doesn't flash back.
141
+ setPhase('resolved');
142
+ updateTokens(res.token, res.refresh_token);
143
+ } catch {
144
+ setBusyOrgUserId(null);
145
+ setSelectError(t('couldNotSwitchProfile'));
146
+ }
147
+ };
148
+
149
+ // Re-open the picker so a shared contact can move to another of its profiles.
150
+ // Drops the session pin and shows the picker with the already-loaded
151
+ // candidates; picking re-scopes the token exactly like the first choice.
152
+ const switchProfile = useCallback(() => {
153
+ if (typeof window !== 'undefined') {
154
+ sessionStorage.removeItem(PICKED_ORG_USER_KEY);
155
+ }
156
+ setSelectError(null);
157
+ setBusyOrgUserId(null);
158
+ setPhase('picking');
159
+ }, []);
160
+
161
+ const switchValue = useMemo(
162
+ () => ({ canSwitch: candidates.length > 1, switchProfile }),
163
+ [candidates.length, switchProfile],
164
+ );
165
+
166
+ let inner: ReactNode;
167
+ if (phase === 'checking') {
168
+ inner = (
169
+ <div className="pc-select-user-loading">
170
+ <PCLoader variant="spinner" color="info" size="lg" />
171
+ </div>
172
+ );
173
+ } else if (phase === 'picking') {
174
+ const decoded = decodeJwtToken(accessToken);
175
+ const email =
176
+ (decoded?.user_data?.primary_email as string) ||
177
+ (decoded?.primary_email as string) ||
178
+ null;
179
+ inner = (
180
+ <SelectUser
181
+ email={email}
182
+ candidates={candidates}
183
+ onSelect={handleSelect}
184
+ busyOrgUserId={busyOrgUserId}
185
+ error={selectError}
186
+ />
187
+ );
188
+ } else {
189
+ inner = <>{children}</>;
190
+ }
191
+
192
+ return <ProfileSwitchProvider value={switchValue}>{inner}</ProfileSwitchProvider>;
193
+ };
@@ -39,3 +39,14 @@ export const MARKER_TINT: Record<BadgeVariant, MarkerTint> = {
39
39
  // Internal error thrown when an activity page fails to load (surfaced to the
40
40
  // user via the i18n `failedToFetchActivityData` toast).
41
41
  export const ACTIVITY_FETCH_ERROR_MESSAGE = 'Failed to fetch activity data';
42
+
43
+ // ─── Shared-contact profile picker ───────────────────────────────────────────
44
+
45
+ // sessionStorage key pinning the principal the user chose this session, so
46
+ // routine token refreshes skip the picker.
47
+ export const PICKED_ORG_USER_KEY = 'pc_picked_org_user_id';
48
+
49
+ // The shared-contact picker/switch user-facing strings are localized via i18n —
50
+ // see the chooseYourProfile / emailLinkedToMultipleProfiles /
51
+ // contactLinkedToMultipleProfiles / signingInEllipsis / couldNotSwitchProfile /
52
+ // switchProfile keys in locales/*/translation.json.
@@ -13,7 +13,7 @@ export const BaseUrlProvider = ({
13
13
  }: {
14
14
  children: ReactNode;
15
15
  baseUrl: string; // Consent server base URL
16
- ledgerBaseUrl: string; // Go ledger base URL — serves user-consents
16
+ ledgerBaseUrl?: string; // Go ledger base URL — serves user-consents
17
17
  }) => {
18
18
  const consentClient = useMemo(
19
19
  () => createConsentClient(baseUrl, ledgerBaseUrl),
@@ -0,0 +1,15 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { ProfileSwitchValue } from '../lib/types';
3
+
4
+ // Lets any descendant of SelectUserGate (e.g. the user menu) re-open the
5
+ // shared-contact profile picker. Defaults to a no-op so consumers rendered
6
+ // outside the gate degrade gracefully.
7
+ const ProfileSwitchContext = createContext<ProfileSwitchValue>({
8
+ canSwitch: false,
9
+ switchProfile: () => {},
10
+ });
11
+
12
+ export const ProfileSwitchProvider = ProfileSwitchContext.Provider;
13
+
14
+ export const useProfileSwitch = (): ProfileSwitchValue =>
15
+ useContext(ProfileSwitchContext);
@@ -14,6 +14,6 @@ export type AuthContextValue = Readonly<{
14
14
 
15
15
  export type BaseUrlContextValue = Readonly<{
16
16
  baseUrl: string;
17
- ledgerBaseUrl: string;
17
+ ledgerBaseUrl?: string;
18
18
  consentClient: ReturnType<typeof createConsentClient>;
19
19
  }>;
@@ -256,6 +256,15 @@ export type PurposeItem = {
256
256
  method: string;
257
257
  data_elements: ConsentDataElement[];
258
258
  link_reason: string | null;
259
+ // When explicitly false, the purpose cannot be self-service withdrawn/revoked
260
+ // in the Privacy Center — the user must raise a data-subject request instead.
261
+ // Optional for backwards-compat: older backends omit it (treated as revocable).
262
+ revocable?: boolean;
263
+ // Per-purpose override for the revoke-confirmation warning message. When set,
264
+ // the revoke modal shows this text instead of the localized default; when
265
+ // null/empty/absent, the default `revokeConsentWarning` copy is used.
266
+ // Optional for backwards-compat: older backends omit it.
267
+ revoke_warning_message?: string | null;
259
268
  // UUID of the notice version this purpose was consented under. Served by the
260
269
  // Go ledger's user-consents response; required to revoke/regrant via the
261
270
  // ledger's submit-consent endpoint (which has no manage-consent route).
@@ -349,6 +358,12 @@ export type ManageConsentResponse = {
349
358
  };
350
359
  };
351
360
 
361
+ // Unwrapped `detail` of the legacy Python manage-consent response; the Go
362
+ // ledger returns a ConsentEventResponse with no `success` flag instead.
363
+ export type LegacyManageConsentResponse = {
364
+ success: boolean;
365
+ };
366
+
352
367
  export type ManageConsentParams = {
353
368
  organizationId: string;
354
369
  workspaceId: string;
@@ -411,8 +426,10 @@ export type Receipt = {
411
426
 
412
427
  // Mirrors the Go ledger's `ConsentReceiptListResponse` (offset-paginated page
413
428
  // of the principal's receipts): flat `total`/`skip`/`limit`, not a nested
414
- // `pagination` object. `normalizeReceipts` in ReceiptsList tolerates either
415
- // envelope, but this type documents the ledger contract the SDK now calls.
429
+ // `pagination` object. Without a configured ledgerBaseUrl the legacy Python
430
+ // list is called instead, which returns a `{ data, pagination }` envelope —
431
+ // `normalizeReceipts` in ReceiptsList tolerates either shape; this type
432
+ // documents the ledger contract.
416
433
  export type ReceiptListResponse = {
417
434
  data: Receipt[];
418
435
  total: number;
@@ -814,3 +831,47 @@ export type NotificationRefreshButtonProps = Readonly<{
814
831
  onRefresh: () => void;
815
832
  iconSize?: number;
816
833
  }>;
834
+
835
+ export type IdentityCandidate = Readonly<{
836
+ uuid: string;
837
+ org_user_id?: string | null;
838
+ primary_email?: string | null;
839
+ primary_mobile?: string | null;
840
+ created_at?: string | null;
841
+ }>;
842
+
843
+ export type IdentityListResponse = Readonly<{
844
+ identities: IdentityCandidate[];
845
+ }>;
846
+
847
+ export type SelectPrincipalResponse = Readonly<{
848
+ token: string;
849
+ refresh_token: string;
850
+ expires_at?: string;
851
+ }>;
852
+
853
+ // Discrete phases of the shared-contact profile gate: `checking` probes the
854
+ // backend for sibling principals, `picking` shows the profile picker, and
855
+ // `resolved` passes through to children.
856
+ export type GatePhase = 'checking' | 'picking' | 'resolved';
857
+
858
+ export type SelectUserProps = Readonly<{
859
+ email: string | null;
860
+ candidates: IdentityCandidate[];
861
+ onSelect: (orgUserId: string) => void;
862
+ busyOrgUserId: string | null;
863
+ error?: string | null;
864
+ }>;
865
+
866
+ export type SelectUserGateProps = Readonly<{
867
+ children: React.ReactNode;
868
+ }>;
869
+
870
+ // Exposed by SelectUserGate so a signed-in session can re-open the profile
871
+ // picker (shared contact). `canSwitch` is true only when the contact maps to
872
+ // more than one principal; `switchProfile` drops the pinned choice and shows
873
+ // the picker again.
874
+ export type ProfileSwitchValue = Readonly<{
875
+ canSwitch: boolean;
876
+ switchProfile: () => void;
877
+ }>;
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "প্ৰ'ফাইল সলনি কৰক",
335
+ "chooseYourProfile": "আপোনাৰ প্ৰ'ফাইল বাছনি কৰক",
336
+ "emailLinkedToMultipleProfiles": "{{email}} একাধিক প্ৰ'ফাইলৰ সৈতে সংযুক্ত। আপুনি চাব বিচৰাটো বাছনি কৰক।",
337
+ "contactLinkedToMultipleProfiles": "এই যোগাযোগটো একাধিক প্ৰ'ফাইলৰ সৈতে সংযুক্ত। আপুনি চাব বিচৰাটো বাছনি কৰক।",
338
+ "signingInEllipsis": "ছাইন ইন হৈ আছে…",
339
+ "couldNotSwitchProfile": "সেই প্ৰ'ফাইললৈ সলনি কৰিব পৰা নগ'ল। অনুগ্ৰহ কৰি পুনৰ চেষ্টা কৰক।",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "प्रोफाइल बदलीं",
335
+ "chooseYourProfile": "आपन प्रोफाइल चुनीं",
336
+ "emailLinkedToMultipleProfiles": "{{email}} एक से ढेर प्रोफाइल से जुड़ल बा। जवन रउआ देखल चाहत बानी ओकरा के चुनीं।",
337
+ "contactLinkedToMultipleProfiles": "ई संपर्क एक से ढेर प्रोफाइल से जुड़ल बा। जवन रउआ देखल चाहत बानी ओकरा के चुनीं।",
338
+ "signingInEllipsis": "साइन इन हो रहल बा…",
339
+ "couldNotSwitchProfile": "ओह प्रोफाइल पर स्विच ना हो सकल। कृपया फेर से कोसिस करीं।",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "প্রোফাইল পরিবর্তন করুন",
335
+ "chooseYourProfile": "আপনার প্রোফাইল বেছে নিন",
336
+ "emailLinkedToMultipleProfiles": "{{email}} একাধিক প্রোফাইলের সাথে যুক্ত। আপনি যেটি দেখতে চান সেটি বেছে নিন।",
337
+ "contactLinkedToMultipleProfiles": "এই যোগাযোগটি একাধিক প্রোফাইলের সাথে যুক্ত। আপনি যেটি দেখতে চান সেটি বেছে নিন।",
338
+ "signingInEllipsis": "সাইন ইন হচ্ছে…",
339
+ "couldNotSwitchProfile": "সেই প্রোফাইলে স্যুইচ করা যায়নি। অনুগ্রহ করে আবার চেষ্টা করুন।",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "प्रोफाइल सोलाय",
335
+ "chooseYourProfile": "नोंथांनि प्रफाइल सायख",
336
+ "emailLinkedToMultipleProfiles": "{{email}} आ मोनसेनिख्रुइ बांसिन प्रफाइलजों नांजाब दं। नोंथाङा नायनो लुबैयोब्ला बेखौ सायख।",
337
+ "contactLinkedToMultipleProfiles": "बे सोमोन्दोआ मोनसेनिख्रुइ बांसिन प्रफाइलजों नांजाब दं। नोंथाङा नायनो लुबैयोब्ला बेखौ सायख।",
338
+ "signingInEllipsis": "साइन इन जायगासिनो दं…",
339
+ "couldNotSwitchProfile": "बै प्रफाइलआव स्विच खालामनो हायाखै। अननानै फिन नाजा।",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "प्रोफाइल बदलो",
335
+ "chooseYourProfile": "अपनी प्रोफाइल चुनो",
336
+ "emailLinkedToMultipleProfiles": "{{email}} इक कोला मता प्रोफाइलें कन्नै जुड़्या दा ऐ। जेह्ड़ी तुसें दिक्खनी ऐ ओह् चुनो।",
337
+ "contactLinkedToMultipleProfiles": "एह् संपर्क इक कोला मता प्रोफाइलें कन्नै जुड़्या दा ऐ। जेह्ड़ी तुसें दिक्खनी ऐ ओह् चुनो।",
338
+ "signingInEllipsis": "साइन इन होआ करदा ऐ…",
339
+ "couldNotSwitchProfile": "ओह् प्रोफाइल पर स्विच नेईं होई सकेआ। कृपा करियै दुबारा कोशिश करो।",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -344,5 +344,13 @@
344
344
  "forProduct": "For {{productName}}",
345
345
  "actingOnBehalfOfShort": "acting on behalf of {{nominator}}",
346
346
  "nominationChipSuffix": "· {{nominator}}",
347
- "selectPurposesForThisProduct": "Select purposes within this product."
347
+ "selectPurposesForThisProduct": "Select purposes within this product.",
348
+ "switchProfile": "Switch profile",
349
+ "chooseYourProfile": "Choose your profile",
350
+ "emailLinkedToMultipleProfiles": "{{email}} is linked to more than one profile. Choose the one you want to view.",
351
+ "contactLinkedToMultipleProfiles": "This contact is linked to more than one profile. Choose the one you want to view.",
352
+ "signingInEllipsis": "Signing in…",
353
+ "couldNotSwitchProfile": "Could not switch to that profile. Please try again.",
354
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
355
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
348
356
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "प्रोफायल बदला",
335
+ "chooseYourProfile": "तुमचें प्रोफायल निवडात",
336
+ "emailLinkedToMultipleProfiles": "{{email}} एकापरस चड प्रोफायलांक जोडलां. तुमकां जें पळोवपाक जाय तें निवडात.",
337
+ "contactLinkedToMultipleProfiles": "हो संपर्क एकापरस चड प्रोफायलांक जोडला. तुमकां जें पळोवपाक जाय तें निवडात.",
338
+ "signingInEllipsis": "साइन इन जावन आसा…",
339
+ "couldNotSwitchProfile": "त्या प्रोफायलाचेर स्विच करूंक जालें ना. उपकार करून परत यत्न करात.",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "પ્રોફાઇલ બદલો",
335
+ "chooseYourProfile": "તમારી પ્રોફાઇલ પસંદ કરો",
336
+ "emailLinkedToMultipleProfiles": "{{email}} એક કરતાં વધુ પ્રોફાઇલ સાથે જોડાયેલ છે. તમે જે જોવા માંગો છો તે પસંદ કરો.",
337
+ "contactLinkedToMultipleProfiles": "આ સંપર્ક એક કરતાં વધુ પ્રોફાઇલ સાથે જોડાયેલ છે. તમે જે જોવા માંગો છો તે પસંદ કરો.",
338
+ "signingInEllipsis": "સાઇન ઇન થઈ રહ્યું છે…",
339
+ "couldNotSwitchProfile": "તે પ્રોફાઇલ પર સ્વિચ કરી શકાયું નહીં. કૃપા કરીને ફરી પ્રયાસ કરો.",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -333,5 +333,13 @@
333
333
  "casesCreatedCount_other": "{{count}} cases created",
334
334
  "someRequestsFailed": "Some requests failed",
335
335
  "retry": "Retry",
336
- "retrying": "Retrying..."
336
+ "retrying": "Retrying...",
337
+ "switchProfile": "प्रोफ़ाइल बदलें",
338
+ "chooseYourProfile": "अपनी प्रोफ़ाइल चुनें",
339
+ "emailLinkedToMultipleProfiles": "{{email}} एक से अधिक प्रोफ़ाइल से जुड़ा है। जिसे आप देखना चाहते हैं उसे चुनें।",
340
+ "contactLinkedToMultipleProfiles": "यह संपर्क एक से अधिक प्रोफ़ाइल से जुड़ा है। जिसे आप देखना चाहते हैं उसे चुनें।",
341
+ "signingInEllipsis": "साइन इन हो रहा है…",
342
+ "couldNotSwitchProfile": "उस प्रोफ़ाइल पर स्विच नहीं किया जा सका। कृपया पुनः प्रयास करें।",
343
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
344
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
337
345
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "ಪ್ರೊಫೈಲ್ ಬದಲಾಯಿಸಿ",
335
+ "chooseYourProfile": "ನಿಮ್ಮ ಪ್ರೊಫೈಲ್ ಆಯ್ಕೆಮಾಡಿ",
336
+ "emailLinkedToMultipleProfiles": "{{email}} ಒಂದಕ್ಕಿಂತ ಹೆಚ್ಚು ಪ್ರೊಫೈಲ್‌ಗಳಿಗೆ ಲಿಂಕ್ ಆಗಿದೆ. ನೀವು ವೀಕ್ಷಿಸಲು ಬಯಸುವುದನ್ನು ಆಯ್ಕೆಮಾಡಿ.",
337
+ "contactLinkedToMultipleProfiles": "ಈ ಸಂಪರ್ಕವು ಒಂದಕ್ಕಿಂತ ಹೆಚ್ಚು ಪ್ರೊಫೈಲ್‌ಗಳಿಗೆ ಲಿಂಕ್ ಆಗಿದೆ. ನೀವು ವೀಕ್ಷಿಸಲು ಬಯಸುವುದನ್ನು ಆಯ್ಕೆಮಾಡಿ.",
338
+ "signingInEllipsis": "ಸೈನ್ ಇನ್ ಆಗುತ್ತಿದೆ…",
339
+ "couldNotSwitchProfile": "ಆ ಪ್ರೊಫೈಲ್‌ಗೆ ಬದಲಾಯಿಸಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ. ದಯವಿಟ್ಟು ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "پروفائل بدلاو",
335
+ "chooseYourProfile": "پننہٕ پروفائل ژارٕو",
336
+ "emailLinkedToMultipleProfiles": "{{email}} چھُ اکہ کھۄتہ زیادٕ پروفائلن سٟتؠ جوڑنہ آمُت۔ یُس توہہِ وُچھُن چھِو یژھان سُ ژارٕو۔",
337
+ "contactLinkedToMultipleProfiles": "یہ رابطہ چھُ اکہ کھۄتہ زیادٕ پروفائلن سٟتؠ جوڑنہ آمُت۔ یُس توہہِ وُچھُن چھِو یژھان سُ ژارٕو۔",
338
+ "signingInEllipsis": "سائن اِن گژھان چھُ…",
339
+ "couldNotSwitchProfile": "تہِ پروفائلس منٛز سوِچ کرُن آو نہ۔ مہربانی کرِتھ دوبارٕ کوشش کریو۔",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "प्रोफाइल बदलू",
335
+ "chooseYourProfile": "अपन प्रोफाइल चुनू",
336
+ "emailLinkedToMultipleProfiles": "{{email}} एकाधिक प्रोफाइलसँ जुड़ल अछि। जे अहाँ देखय चाहैत छी ओकरा चुनू।",
337
+ "contactLinkedToMultipleProfiles": "ई संपर्क एकाधिक प्रोफाइलसँ जुड़ल अछि। जे अहाँ देखय चाहैत छी ओकरा चुनू।",
338
+ "signingInEllipsis": "साइन इन भ' रहल अछि…",
339
+ "couldNotSwitchProfile": "ओ प्रोफाइल पर स्विच नहि भ' सकल। कृपया फेर सँ प्रयास करू।",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }
@@ -330,5 +330,13 @@
330
330
  "casesCreatedCount_other": "{{count}} cases created",
331
331
  "someRequestsFailed": "Some requests failed",
332
332
  "retry": "Retry",
333
- "retrying": "Retrying..."
333
+ "retrying": "Retrying...",
334
+ "switchProfile": "പ്രൊഫൈൽ മാറ്റുക",
335
+ "chooseYourProfile": "നിങ്ങളുടെ പ്രൊഫൈൽ തിരഞ്ഞെടുക്കുക",
336
+ "emailLinkedToMultipleProfiles": "{{email}} ഒന്നിലധികം പ്രൊഫൈലുകളുമായി ബന്ധിപ്പിച്ചിരിക്കുന്നു. നിങ്ങൾ കാണാൻ ആഗ്രഹിക്കുന്നത് തിരഞ്ഞെടുക്കുക.",
337
+ "contactLinkedToMultipleProfiles": "ഈ കോൺടാക്റ്റ് ഒന്നിലധികം പ്രൊഫൈലുകളുമായി ബന്ധിപ്പിച്ചിരിക്കുന്നു. നിങ്ങൾ കാണാൻ ആഗ്രഹിക്കുന്നത് തിരഞ്ഞെടുക്കുക.",
338
+ "signingInEllipsis": "സൈൻ ഇൻ ചെയ്യുന്നു…",
339
+ "couldNotSwitchProfile": "ആ പ്രൊഫൈലിലേക്ക് മാറാൻ കഴിഞ്ഞില്ല. വീണ്ടും ശ്രമിക്കുക.",
340
+ "purposeNotRevocable": "This purpose can't be withdrawn directly.",
341
+ "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it."
334
342
  }