Skip to content

Commit

Permalink
Dynamically load GVL translations (#5187)
Browse files Browse the repository at this point in the history
  • Loading branch information
gilluminate authored Aug 13, 2024
1 parent 7faafd2 commit 71ae68c
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 59 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ The types of changes are:

### Changed
- Removed PRIVACY_REQUEST_READ scope from Viewer role [#5184](https://github.com/ethyca/fides/pull/5184)
- Asynchronously load GVL translations in FidesJS instead of blocking UI rendering [#5187](https://github.com/ethyca/fides/pull/5187)


### Developer Experience
Expand Down
2 changes: 1 addition & 1 deletion clients/fides-js/docs/interfaces/Fides.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ console.log(Fides.getModalLinkLabel({ disableLocalization: true })); // "Your Pr

Applying the link text to a custom modal link element:
```html
<button class="my-custom-show-modal" id="fides-modal-link-label" onclick="Fides.showModal()" />
<button class="my-custom-show-modal" id="fides-modal-link-label" onclick="Fides.showModal()"><button>
<script>
document.getElementById('fides-modal-link-label').innerText = Fides.getModalLinkLabel();
</script>
Expand Down
12 changes: 4 additions & 8 deletions clients/fides-js/src/components/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { h } from "preact";

import {
FIDES_I18N_ICON,
FIDES_OVERLAY_WRAPPER,
} from "../lib/consent-constants";
import { FIDES_OVERLAY_WRAPPER } from "../lib/consent-constants";
import { FidesInitOptions } from "../lib/consent-types";
import { debugLog } from "../lib/consent-utils";
import {
Expand All @@ -29,19 +26,18 @@ const LanguageSelector = ({
options,
isTCF,
}: LanguageSelectorProps) => {
const { currentLocale, setCurrentLocale } = useI18n();
const { currentLocale, setCurrentLocale, setIsLoading } = useI18n();

const handleLocaleSelect = async (locale: string) => {
if (locale !== i18n.locale) {
if (isTCF) {
const icon = document.getElementById(FIDES_I18N_ICON);
icon?.style.setProperty("animation-name", "spin");
setIsLoading(true);
const gvlTranslations = await fetchGvlTranslations(
options.fidesApiUrl,
[locale],
options.debug,
);
icon?.style.removeProperty("animation-name");
setIsLoading(false);
if (gvlTranslations && Object.keys(gvlTranslations).length) {
loadMessagesFromGVLTranslations(
i18n,
Expand Down
37 changes: 32 additions & 5 deletions clients/fides-js/src/components/tcf/TcfOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import { debugLog } from "../../lib/consent-utils";
import { transformTcfPreferencesToCookieKeys } from "../../lib/cookie";
import { dispatchFidesEvent } from "../../lib/events";
import { useConsentServed } from "../../lib/hooks";
import { selectBestExperienceConfigTranslation } from "../../lib/i18n";
import {
loadMessagesFromGVLTranslations,
selectBestExperienceConfigTranslation,
} from "../../lib/i18n";
import { useI18n } from "../../lib/i18n/i18n-context";
import { updateConsentPreferences } from "../../lib/preferences";
import {
Expand All @@ -37,6 +40,7 @@ import type {
TCFVendorLegitimateInterestsRecord,
TCFVendorSave,
} from "../../lib/tcf/types";
import { fetchGvlTranslations } from "../../services/api";
import Button from "../Button";
import ConsentBanner from "../ConsentBanner";
import Overlay from "../Overlay";
Expand Down Expand Up @@ -227,13 +231,36 @@ const TcfOverlay: FunctionComponent<OverlayProps> = ({

const [draftIds, setDraftIds] = useState<EnabledIds>(initialEnabledIds);

const { currentLocale, setCurrentLocale } = useI18n();
const { currentLocale, setCurrentLocale, setIsLoading } = useI18n();

const { locale, getDefaultLocale } = i18n;
const defaultLocale = getDefaultLocale();

const loadGVLTranslations = async () => {
setIsLoading(true);
const gvlTranslations = await fetchGvlTranslations(
options.fidesApiUrl,
[locale],
options.debug,
);
setIsLoading(false);
if (gvlTranslations) {
loadMessagesFromGVLTranslations(i18n, gvlTranslations, [locale]);
debugLog(options.debug, `Fides GVL translations loaded for ${locale}`);
}
setCurrentLocale(locale);
};

useEffect(() => {
if (!currentLocale && i18n.locale) {
setCurrentLocale(i18n.locale);
if (!currentLocale && locale && defaultLocale) {
if (locale !== defaultLocale) {
loadGVLTranslations();
} else {
setCurrentLocale(locale);
}
}
}, [currentLocale, i18n.locale, setCurrentLocale]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentLocale, locale, defaultLocale, setCurrentLocale]);

// Determine which ExperienceConfig history ID should be used for the
// reporting APIs, based on the selected locale
Expand Down
2 changes: 1 addition & 1 deletion clients/fides-js/src/docs/fides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export interface Fides {
* @example
* Applying the link text to a custom modal link element:
* ```html
* <button class="my-custom-show-modal" id="fides-modal-link-label" onclick="Fides.showModal()" />
* <button class="my-custom-show-modal" id="fides-modal-link-label" onclick="Fides.showModal()"><button>
* <script>
* document.getElementById('fides-modal-link-label').innerText = Fides.getModalLinkLabel();
* </script>
Expand Down
19 changes: 17 additions & 2 deletions clients/fides-js/src/lib/i18n/i18n-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,38 @@ import {
Dispatch,
StateUpdater,
useContext,
useEffect,
useMemo,
useState,
} from "preact/hooks";

import { FIDES_I18N_ICON } from "../consent-constants";

interface I18nContextProps {
currentLocale: string | undefined;
setCurrentLocale: Dispatch<StateUpdater<string | undefined>>;
isLoading: boolean;
setIsLoading: Dispatch<StateUpdater<boolean>>;
}

const I18nContext = createContext<I18nContextProps>({} as I18nContextProps);

export const I18nProvider: FunctionComponent = ({ children }) => {
const [currentLocale, setCurrentLocale] = useState<string>();
const [isLoading, setIsLoading] = useState<boolean>(false);

useEffect(() => {
const icon = document.getElementById(FIDES_I18N_ICON);
if (isLoading) {
icon?.style.setProperty("animation-name", "spin");
} else {
icon?.style.removeProperty("animation-name");
}
}, [isLoading]);

const value: I18nContextProps = useMemo(
() => ({ currentLocale, setCurrentLocale }),
[currentLocale],
() => ({ currentLocale, setCurrentLocale, isLoading, setIsLoading }),
[currentLocale, isLoading],
);
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
};
Expand Down
31 changes: 31 additions & 0 deletions clients/fides-js/src/lib/i18n/i18n-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ComponentType,
ExperienceConfig,
ExperienceConfigTranslation,
FidesExperienceTranslationOverrides,
Expand Down Expand Up @@ -267,6 +268,24 @@ export function loadMessagesFromExperience(
});
}

/**
*
*/
export function loadGVLMessagesFromExperience(
i18n: I18n,
experience: Partial<PrivacyExperience>,
) {
if (!experience.gvl) {
return;
}
const { locale } = i18n;
const gvlTranslations: GVLTranslations = {};
gvlTranslations[locale] = experience.gvl;
const extracted: Record<Locale, Messages> =
extractMessagesFromGVLTranslations(gvlTranslations, [locale]);
i18n.load(locale, extracted[locale]);
}

/**
* Parse the provided GVLTranslations object and load all translated strings
* into the message catalog.
Expand Down Expand Up @@ -522,6 +541,18 @@ export function initializeI18n(
i18n.getDefaultLocale(),
);
i18n.activate(bestLocale);

// Now that we've activated the best locale, load the GVL messages if needed.
// First load default language messages from the experience's GVL to avoid
// delay in rendering the overlay. If a translation is needed, it will be
// loaded in the background.
if (
experience.experience_config?.component === ComponentType.TCF_OVERLAY &&
!!experience.gvl
) {
loadGVLMessagesFromExperience(i18n, experience);
}

debugLog(
options?.debug,
`Initialized Fides i18n with best locale match = ${bestLocale}`,
Expand Down
32 changes: 0 additions & 32 deletions clients/fides-js/src/lib/initOverlay.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { ContainerNode, render } from "preact";

import { OverlayProps } from "../components/types";
import { fetchGvlTranslations } from "../services/api";
import { ComponentType } from "./consent-types";
import { debugLog } from "./consent-utils";
import { DEFAULT_LOCALE } from "./i18n";
import { loadMessagesFromGVLTranslations } from "./i18n/i18n-utils";
import { LOCALE_LANGUAGE_MAP } from "./i18n/locales";
import { ColorFormat, generateLighterColor } from "./style-utils";

const FIDES_EMBED_CONTAINER_ID = "fides-embed-container";
Expand Down Expand Up @@ -38,34 +34,6 @@ export const initOverlay = async ({
}): Promise<void> => {
debugLog(options.debug, "Initializing Fides consent overlays...");

if (experience.experience_config?.component === ComponentType.TCF_OVERLAY) {
let gvlTranslations = await fetchGvlTranslations(
options.fidesApiUrl,
[i18n.locale],
options.debug,
);
if (
(!gvlTranslations || Object.keys(gvlTranslations).length === 0) &&
experience.gvl
) {
// if translations API fails or is empty, use the GVL object directly
// as a fallback, since it already contains the english version of strings
gvlTranslations = {};
gvlTranslations[DEFAULT_LOCALE] = experience.gvl;
// eslint-disable-next-line no-param-reassign
experience.available_locales = [DEFAULT_LOCALE];
i18n.setAvailableLanguages(
LOCALE_LANGUAGE_MAP.filter((lang) => lang.locale === DEFAULT_LOCALE),
);
i18n.activate(DEFAULT_LOCALE);
}
loadMessagesFromGVLTranslations(
i18n,
gvlTranslations,
experience.available_locales || [DEFAULT_LOCALE],
);
}

async function renderFidesOverlay(): Promise<void> {
try {
debugLog(
Expand Down
14 changes: 4 additions & 10 deletions clients/privacy-center/cypress/e2e/consent-i18n.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1490,8 +1490,10 @@ describe("Consent i18n", () => {
fixture,
options: { tcfEnabled: true },
});
cy.wait("@getGvlTranslations");
testTcfBannerLocalization(banner);
if (locale === SPANISH_LOCALE) {
cy.wait("@getGvlTranslations");
}
testTcfModalLocalization(modal);
});
});
Expand All @@ -1502,10 +1504,6 @@ describe("Consent i18n", () => {
fixture: "experience_tcf.json",
options: { tcfEnabled: true },
});
cy.wait("@getGvlTranslations").then((interception) => {
const { url } = interception.request;
expect(url.split("?")[1]).to.eq(`language=${ENGLISH_LOCALE}`);
});
cy.get("#fides-banner").should("be.visible");
cy.get(
`#fides-banner [data-testid='fides-i18n-option-${SPANISH_LOCALE}']`,
Expand Down Expand Up @@ -1540,12 +1538,10 @@ describe("Consent i18n", () => {
fixture: "experience_tcf.json",
options: { tcfEnabled: true },
});
cy.wait("@getGvlTranslations");
cy.get("#fides-banner").should("be.visible");
cy.get(".fides-i18n-menu").should("not.exist");
cy.get(".fides-notice-toggle")
.first()
.contains(/^Selection of personalised(.*)/);
.contains(/^Selection of personalised(.*)/); // english fallback
});
});
});
Expand Down Expand Up @@ -1758,7 +1754,6 @@ describe("Consent i18n", () => {
fixture: "experience_tcf.json",
options: { tcfEnabled: true },
});
cy.wait("@getGvlTranslations");
cy.get("#fides-modal-link").click();
cy.getByTestId("records-list-purposes").within(() => {
cy.get(".fides-toggle:first").contains("Off");
Expand Down Expand Up @@ -1788,7 +1783,6 @@ describe("Consent i18n", () => {
fixture: "experience_tcf.json",
options: { tcfEnabled: true },
});
cy.wait("@getGvlTranslations");
cy.get("#fides-modal-link").click();
cy.getByTestId("records-list-purposes").within(() => {
cy.get(".fides-toggle:first").contains("Off").should("not.exist");
Expand Down

0 comments on commit 71ae68c

Please sign in to comment.