Skip to content

Commit

Permalink
Support language switching in Fides JS banner (#4737)
Browse files Browse the repository at this point in the history
  • Loading branch information
gilluminate authored Mar 26, 2024
1 parent 69379bf commit e4ec5f7
Show file tree
Hide file tree
Showing 19 changed files with 422 additions and 66 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The types of changes are:
- Added models for Privacy Center configuration (for plus users) [#4716](https://github.com/ethyca/fides/pull/4716)
- Added ability to delete properties [#4708](https://github.com/ethyca/fides/pull/4708)
- Add interface for submitting privacy requests in admin UI [#4738](https://github.com/ethyca/fides/pull/4738)
- Added language switching support to the FidesJS UI based on configured translations [#4737](https://github.com/ethyca/fides/pull/4737)

### Changed

Expand Down
2 changes: 1 addition & 1 deletion clients/fides-js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Typically, you will want to make your changes within `fides-js`, then run `turbo

To recompile as you make changes to the library, you will need to run `npm run dev` from the folder `fides-js`.

Once you have the privacy center running, you can view `fides-js` in action at our demo page, located at `[privacy-center-host]:[port]/fides-js-demo.html`. This is an HTML page with helpful information about how `fides-js` is configured, and will itself load `fides-js`.
Once you have the privacy center running, you can view `fides-js` in action at our demo page, located at `[privacy-center-host]:[port]/fides-js-demo.html`. This is an HTML page with helpful information about how `fides-js` is configured, and will itself load `fides-js` and display the overlay. To get it to re-appear again on next page load, you'll need to clear your cookies, specifically the `fides_consent` cookie. You'll see a button near the top of the page makes this easy.

### From a full test environment

Expand Down
24 changes: 23 additions & 1 deletion clients/fides-js/__tests__/lib/i18n/i18n-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ import messagesEn from "~/lib/i18n/locales/en/messages.json";
import messagesEs from "~/lib/i18n/locales/es/messages.json";
import messagesTcfEn from "~/lib/tcf/i18n/locales/en/messages-tcf.json";
import messagesTcfEs from "~/lib/tcf/i18n/locales/es/messages-tcf.json";
import type { I18n, Locale, MessageDescriptor, Messages } from "~/lib/i18n";
import type {
I18n,
Locale,
Language,
MessageDescriptor,
Messages,
} from "~/lib/i18n";

import mockExperienceJSON from "../../__fixtures__/mock_experience.json";
import mockGVLTranslationsJSON from "../../__fixtures__/mock_gvl_translations.json";
Expand All @@ -34,12 +40,25 @@ describe("i18n-utils", () => {
// Define a mock implementation of the i18n singleton for tests
let mockCurrentLocale = "";
let mockDefaultLocale = DEFAULT_LOCALE;
let mockAvailableLanguages: Language[] = [
{ locale: "en", label_en: "English", label_original: "English" },
{ locale: "es", label_en: "Spanish", label_original: "Español" },
];

const mockI18n = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
activate: jest.fn((locale: Locale): void => {
mockCurrentLocale = locale;
}),

setAvailableLanguages: jest.fn((languages: Language[]): void => {
mockAvailableLanguages = languages;
}),

get availableLanguages(): Language[] {
return mockAvailableLanguages;
},

getDefaultLocale: jest.fn((): Locale => mockDefaultLocale),

setDefaultLocale: jest.fn((locale: Locale): void => {
Expand Down Expand Up @@ -127,6 +146,9 @@ describe("i18n-utils", () => {
expect(mockI18n.load).toHaveBeenCalledWith("en", messagesEn);
expect(mockI18n.load).toHaveBeenCalledWith("es", messagesEs);
expect(mockI18n.setDefaultLocale).toHaveBeenCalledWith("es");
expect(mockI18n.setAvailableLanguages).toHaveBeenCalledWith(
mockAvailableLanguages
);
expect(mockI18n.activate).toHaveBeenCalledWith("es");
});
});
Expand Down
4 changes: 3 additions & 1 deletion clients/fides-js/src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ import { ButtonType } from "../lib/consent-types";
interface ButtonProps {
buttonType: ButtonType;
label?: string;
id?: string;
onClick?: () => void;
className?: string;
}

const Button: FunctionComponent<ButtonProps> = ({
buttonType,
label,
id,
onClick,
className = "",
}) => (
<button
type="button"
id={`fides-banner-button-${buttonType.valueOf()}`}
id={id}
className={`fides-banner-button fides-banner-button-${buttonType.valueOf()} ${className}`}
onClick={onClick}
data-testid={`${label}-btn`}
Expand Down
122 changes: 86 additions & 36 deletions clients/fides-js/src/components/ConsentButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import {
ButtonType,
ConsentMechanism,
ConsentMethod,
FidesOptions,
PrivacyExperience,
PrivacyNotice,
} from "../lib/consent-types";
import PrivacyPolicyLink from "./PrivacyPolicyLink";
import type { I18n } from "../lib/i18n";
import MenuItem from "./MenuItem";
import { useI18n } from "../lib/i18n/i18n-context";
import { debugLog } from "../fides";

export const ConsentButtons = ({
i18n,
Expand All @@ -19,53 +23,96 @@ export const ConsentButtons = ({
isMobile,
includePrivacyPolicy,
saveOnly = false,
includeLanguageSelector,
options,
}: {
i18n: I18n;
onManagePreferencesClick?: () => void;
firstButton?: VNode;
onAcceptAll: () => void;
onRejectAll: () => void;
isMobile: boolean;
options: FidesOptions;
includePrivacyPolicy?: boolean;
saveOnly?: boolean;
}) => (
<div id="fides-button-group">
{onManagePreferencesClick ? (
<div style={{ display: "flex" }}>
<Button
buttonType={isMobile ? ButtonType.SECONDARY : ButtonType.TERTIARY}
label={i18n.t("exp.privacy_preferences_link_label")}
onClick={onManagePreferencesClick}
className="fides-manage-preferences-button"
/>
</div>
) : null}
{includePrivacyPolicy ? <PrivacyPolicyLink i18n={i18n} /> : null}
<div
className={
firstButton ? "fides-modal-button-group" : "fides-banner-button-group"
}
>
{firstButton || null}
{!saveOnly && (
<Fragment>
<Button
buttonType={ButtonType.PRIMARY}
label={i18n.t("exp.reject_button_label")}
onClick={onRejectAll}
className="fides-reject-all-button"
/>
includeLanguageSelector?: boolean;
}) => {
const { currentLocale, setCurrentLocale } = useI18n();

const handleLocaleSelect = (locale: string) => {
if (locale !== i18n.locale) {
i18n.activate(locale);
setCurrentLocale(locale);
document.getElementById("fides-button-group")?.focus();
debugLog(options.debug, `Fides locale updated to ${locale}`);
}
};

return (
<div id="fides-button-group" tabIndex={-1}>
{includeLanguageSelector && i18n.availableLanguages?.length > 1 && (
<div className="fides-i18n-menu">
<div role="group" className="fides-i18n-popover">
{i18n.availableLanguages.map((lang) => (
<MenuItem
key={lang.locale}
data-testid={`fides-i18n-option-${lang.locale}`}
onClick={() => handleLocaleSelect(lang.locale)}
isActive={currentLocale === lang.locale}
>
{lang.label_original}
</MenuItem>
))}
</div>
<div className="fides-i18n-pseudo-button">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512"
height="inherit"
fill="currentColor"
>
<path d="M0 128C0 92.7 28.7 64 64 64H256h48 16H576c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H320 304 256 64c-35.3 0-64-28.7-64-64V128zm320 0V384H576V128H320zM178.3 175.9c-3.2-7.2-10.4-11.9-18.3-11.9s-15.1 4.7-18.3 11.9l-64 144c-4.5 10.1 .1 21.9 10.2 26.4s21.9-.1 26.4-10.2l8.9-20.1h73.6l8.9 20.1c4.5 10.1 16.3 14.6 26.4 10.2s14.6-16.3 10.2-26.4l-64-144zM160 233.2L179 276H141l19-42.8zM448 164c11 0 20 9 20 20v4h44 16c11 0 20 9 20 20s-9 20-20 20h-2l-1.6 4.5c-8.9 24.4-22.4 46.6-39.6 65.4c.9 .6 1.8 1.1 2.7 1.6l18.9 11.3c9.5 5.7 12.5 18 6.9 27.4s-18 12.5-27.4 6.9l-18.9-11.3c-4.5-2.7-8.8-5.5-13.1-8.5c-10.6 7.5-21.9 14-34 19.4l-3.6 1.6c-10.1 4.5-21.9-.1-26.4-10.2s.1-21.9 10.2-26.4l3.6-1.6c6.4-2.9 12.6-6.1 18.5-9.8l-12.2-12.2c-7.8-7.8-7.8-20.5 0-28.3s20.5-7.8 28.3 0l14.6 14.6 .5 .5c12.4-13.1 22.5-28.3 29.8-45H448 376c-11 0-20-9-20-20s9-20 20-20h52v-4c0-11 9-20 20-20z" />
</svg>
</div>
</div>
)}
{!!onManagePreferencesClick && (
<div className="fides-banner-button-group">
<Button
buttonType={ButtonType.PRIMARY}
label={i18n.t("exp.accept_button_label")}
onClick={onAcceptAll}
className="fides-accept-all-button"
buttonType={isMobile ? ButtonType.SECONDARY : ButtonType.TERTIARY}
label={i18n.t("exp.privacy_preferences_link_label")}
onClick={onManagePreferencesClick}
className="fides-manage-preferences-button"
/>
</Fragment>
</div>
)}
{includePrivacyPolicy && <PrivacyPolicyLink i18n={i18n} />}
<div
className={
firstButton ? "fides-modal-button-group" : "fides-banner-button-group"
}
>
{firstButton}
{!saveOnly && (
<Fragment>
<Button
buttonType={ButtonType.PRIMARY}
label={i18n.t("exp.reject_button_label")}
onClick={onRejectAll}
className="fides-reject-all-button"
/>
<Button
buttonType={ButtonType.PRIMARY}
label={i18n.t("exp.accept_button_label")}
onClick={onAcceptAll}
className="fides-accept-all-button"
/>
</Fragment>
)}
</div>
</div>
</div>
);
);
};

type NoticeKeys = Array<PrivacyNotice["notice_key"]>;

Expand All @@ -76,10 +123,10 @@ interface NoticeConsentButtonProps {
onManagePreferencesClick?: () => void;
enabledKeys: NoticeKeys;
isAcknowledge: boolean;
options: FidesOptions;
isInModal?: boolean;
isMobile: boolean;
saveOnly?: boolean;
fidesPreviewMode?: boolean;
}

export const NoticeConsentButtons = ({
Expand All @@ -92,8 +139,9 @@ export const NoticeConsentButtons = ({
isAcknowledge,
isMobile,
saveOnly = false,
fidesPreviewMode = false,
options,
}: NoticeConsentButtonProps) => {
const { fidesPreviewMode } = options;
if (!experience.experience_config || !experience.privacy_notices) {
return null;
}
Expand Down Expand Up @@ -164,6 +212,8 @@ export const NoticeConsentButtons = ({
isMobile={isMobile}
includePrivacyPolicy={!isInModal}
saveOnly={saveOnly}
includeLanguageSelector={!isInModal}
options={options}
/>
);
};
24 changes: 24 additions & 0 deletions clients/fides-js/src/components/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { h, FunctionComponent } from "preact";
import { JSXInternal } from "preact/src/jsx";

interface MenuItemProps extends JSXInternal.HTMLAttributes<HTMLButtonElement> {
isActive: boolean;
}

const MenuItem: FunctionComponent<MenuItemProps> = ({
isActive,
className,
children,
...props
}) => (
<button
type="button"
aria-pressed={isActive || undefined}
{...props}
className={`fides-banner-button fides-menu-item ${className || ""}`}
>
{children}
</button>
);

export default MenuItem;
Loading

0 comments on commit e4ec5f7

Please sign in to comment.