Skip to content

Commit

Permalink
Fix cached translation issues with Minimal TCF experience (#5306)
Browse files Browse the repository at this point in the history
  • Loading branch information
gilluminate authored Sep 20, 2024
1 parent ec2d33b commit 1b1ab1f
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 45 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ The types of changes are:
- Fix wording in tooltip for Yotpo Reviews [#5274](https://github.com/ethyca/fides/pull/5274)
- Hardcode ConnectionConfigurationResponse.secrets [#5283](https://github.com/ethyca/fides/pull/5283)
- Fix Fides.shouldShouldShowExperience() to return false for modal-only experiences [#5281](https://github.com/ethyca/fides/pull/5281)
- Fix issues with cached or `window.fides_overrides` languages in the Minimal TCF banner [#5306](https://github.com/ethyca/fides/pull/5306)


## [2.44.0](https://github.com/ethyca/fides/compare/2.43.1...2.44.0)
Expand Down
26 changes: 25 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 @@ -189,6 +189,17 @@ describe("i18n-utils", () => {
);
expect(mockI18n.activate).toHaveBeenCalledWith("es");
});

it("handles i18n initialization when translation isn't available (yet)", () => {
const mockNavigator: Partial<Navigator> = {
language: "fr",
};
const mockExpMinimalCached = JSON.parse(JSON.stringify(mockExperience));
mockExpMinimalCached.experience_config.translations.splice(0, 1);
mockExpMinimalCached.available_locales.push("fr");
initializeI18n(mockI18n, mockNavigator, mockExpMinimalCached);
expect(mockI18n.setDefaultLocale).toHaveBeenCalledWith("es");
});
});

describe("loadMessagesFromFiles", () => {
Expand Down Expand Up @@ -531,7 +542,20 @@ describe("i18n-utils", () => {
const mockOptions: Partial<FidesInitOptions> = {
fidesLocale: "fr",
};
expect(detectUserLocale(mockNavigator, mockOptions)).toEqual("fr");
expect(detectUserLocale(mockNavigator, mockOptions.fidesLocale)).toEqual(
"fr",
);
});

it("returns the browser locale if locale is provided but undefined", () => {
const mockOptions: Partial<FidesInitOptions> = {};
expect(detectUserLocale(mockNavigator, mockOptions.fidesLocale)).toEqual(
"es",
);
});

it("returns the default locale if provided and browser locale is missing", () => {
expect(detectUserLocale({}, undefined, "fr")).toEqual("fr");
});
});

Expand Down
40 changes: 21 additions & 19 deletions clients/fides-js/src/components/tcf/TcfOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { debugLog, isPrivacyExperience } from "../../lib/consent-utils";
import { dispatchFidesEvent } from "../../lib/events";
import { useNoticesServed } from "../../lib/hooks";
import {
DEFAULT_LOCALE,
detectUserLocale,
loadGVLMessagesFromExperience,
loadMessagesFromExperience,
loadMessagesFromGVLTranslations,
Expand Down Expand Up @@ -64,8 +66,13 @@ export const TcfOverlay = ({
const minExperienceLocale =
experienceMinimal?.experience_config?.translations?.[0]?.language;
const defaultLocale = i18n.getDefaultLocale();
const userlocale = detectUserLocale(
navigator,
options.fidesLocale,
i18n.getDefaultLocale(),
);
const bestLocale = matchAvailableLocales(
options.fidesLocale || i18n.locale,
userlocale,
experienceMinimal.available_locales || [],
i18n.getDefaultLocale(),
);
Expand Down Expand Up @@ -100,33 +107,29 @@ export const TcfOverlay = ({
const [experience, setExperience] = useState<PrivacyExperience>();

useEffect(() => {
// We need to track if the experience and GVL translations are loading separately
// because they are loaded asynchronously and we need to know when both are done
// in order to set the i18n loading state.
let isExperienceLoading = false;
let isGVLLangLoading = false;

if (!!options.fidesLocale && options.fidesLocale !== minExperienceLocale) {
// the minimal experience language is different from the configured language.
if (!!userlocale && bestLocale !== minExperienceLocale) {
// The minimal experience translation is different from the user's language.
// This occurs when the customer has set their overrides on the window object
// which isn't available to us until the experience is fetched. In this case,
// which isn't available to us until the experience is fetched or when the
// browser has cached the experience from a previous userLocale. In these cases,
// we'll get the translations for the banner from the full experience.
debugLog(
options.debug,
`Best locale does not match minimal experience locale (${minExperienceLocale})\nLoading translations from full experience = ${bestLocale}`,
);
setIsI18nLoading(true);
}
if (!!options.fidesLocale && options.fidesLocale !== defaultLocale) {
// We can only get default locale (English) GVL translations from the experience.
// If the configured locale is not the default, we need to load the translations
// from the api.
setIsI18nLoading(true);
if (!!userlocale && bestLocale !== DEFAULT_LOCALE) {
// We can only get English GVL translations from the experience.
// If the user's locale is not English, we need to load them from the api.
// This only affects the modal.
isGVLLangLoading = true;
loadGVLTranslations(bestLocale).then(() => {
isGVLLangLoading = false;
if (!isExperienceLoading) {
setIsI18nLoading(false);
}
});
}
isExperienceLoading = true;
fetchExperience({
userLocationString: fidesRegionString,
fidesApiUrl: options.fidesApiUrl,
Expand All @@ -142,8 +145,7 @@ export const TcfOverlay = ({

setExperience(fullExperience);
loadMessagesFromExperience(i18n, fullExperience);
isExperienceLoading = false;
if (!options.fidesLocale || options.fidesLocale === defaultLocale) {
if (!userlocale || bestLocale === defaultLocale) {
// English (default) GVL translations are part of the full experience, so we load them here.
loadGVLMessagesFromExperience(i18n, fullExperience);
} else {
Expand Down
2 changes: 1 addition & 1 deletion clients/fides-js/src/lib/consent-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ interface ExperienceConfigTranslationMinimal
privacy_experience_config_history_id: string;
}

interface ExperienceConfigMinimal
export interface ExperienceConfigMinimal
extends Pick<
ExperienceConfig,
"component" | "auto_detect_language" | "dismissable"
Expand Down
69 changes: 48 additions & 21 deletions clients/fides-js/src/lib/i18n/i18n-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ComponentType,
ExperienceConfig,
ExperienceConfigMinimal,
ExperienceConfigTranslation,
FidesExperienceTranslationOverrides,
FidesInitOptions,
Expand Down Expand Up @@ -57,7 +58,7 @@ export function areLocalesEqual(a: Locale, b: Locale): boolean {
* }
*/
function extractMessagesFromExperienceConfig(
experienceConfig: ExperienceConfig,
experienceConfig: ExperienceConfig | ExperienceConfigMinimal,
experienceTranslationOverrides?: Partial<FidesExperienceTranslationOverrides>,
): Record<Locale, Messages> {
const extracted: Record<Locale, Messages> = {};
Expand Down Expand Up @@ -115,7 +116,8 @@ function extractMessagesFromExperienceConfig(
// For backwards-compatibility, when "translations" doesn't exist, look for
// the fields on the ExperienceConfig itself; and since that's deprecated,
// default to the "en" locale
const locale = experienceConfig.language || DEFAULT_LOCALE;
const locale =
(experienceConfig as ExperienceConfig).language || DEFAULT_LOCALE;
const messages: Messages = {};
EXPERIENCE_TRANSLATION_FIELDS.forEach((key) => {
// @ts-expect-error EXPERIENCE_TRANSLATION_FIELDS is a const array
Expand Down Expand Up @@ -241,7 +243,7 @@ export function loadMessagesFromFiles(i18n: I18n): Locale[] {
*/
export function loadMessagesFromExperience(
i18n: I18n,
experience: Partial<PrivacyExperience>,
experience: Partial<PrivacyExperience | PrivacyExperienceMinimal>,
experienceTranslationOverrides?: Partial<FidesExperienceTranslationOverrides>,
) {
const allMessages: Record<Locale, Messages> = {};
Expand Down Expand Up @@ -317,11 +319,11 @@ export function getCurrentLocale(i18n: I18n): Locale {
*/
export function detectUserLocale(
navigator: Partial<Navigator>,
options?: Partial<FidesInitOptions>,
fidesLocaleOverride?: string | undefined,
defaultLocale: Locale = DEFAULT_LOCALE,
): Locale {
const browserLocale = navigator?.language;
const fidesLocaleOverride = options?.fidesLocale;
return fidesLocaleOverride || browserLocale || DEFAULT_LOCALE;
return fidesLocaleOverride || browserLocale || defaultLocale;
}

/**
Expand Down Expand Up @@ -440,32 +442,32 @@ export function selectBestNoticeTranslation(
*/
export function selectBestExperienceConfigTranslation(
i18n: I18n,
experience: Partial<ExperienceConfig>,
experienceConfig: Partial<ExperienceConfig>,
): ExperienceConfigTranslation | null {
// Defensive checks
if (!experience || !experience.translations) {
if (!experienceConfig || !experienceConfig.translations) {
return null;
}

// 1) Look for an exact match for the current locale
const currentLocale = getCurrentLocale(i18n);
const matchTranslation = experience.translations.find((e) =>
const matchTranslation = experienceConfig.translations.find((e) =>
areLocalesEqual(e.language, currentLocale),
);
if (matchTranslation) {
return matchTranslation;
}

// 2) Fallback to default locale, if an exact match isn't found
const defaultTranslation = experience.translations.find((e) =>
const defaultTranslation = experienceConfig.translations.find((e) =>
areLocalesEqual(e.language, i18n.getDefaultLocale()),
);
if (defaultTranslation) {
return defaultTranslation;
}

// 3) Fallback to first translation in the list, if the default locale isn't found
return experience.translations[0] || null;
return experienceConfig.translations[0] || null;
}

/**
Expand Down Expand Up @@ -526,24 +528,54 @@ export function initializeI18n(
);

// Detect the user's locale, unless it's been *explicitly* disabled in the experience config
let userLocale = i18n.getDefaultLocale();
let userLocale = defaultLocale;
if (experience.experience_config?.auto_detect_language === false) {
debugLog(
options?.debug,
"Auto-detection of Fides i18n user locale disabled!",
);
} else {
userLocale = detectUserLocale(navigator, options);
userLocale = detectUserLocale(
navigator,
options?.fidesLocale,
defaultLocale,
);
debugLog(options?.debug, `Detected Fides i18n user locale = ${userLocale}`);
}

// Match the user locale to the "best" available locale from the experience API and activate it!
// Match the user locale to the "best" available locale from the experience API
const bestLocale = matchAvailableLocales(
userLocale,
availableLocales,
availableLocales || [],
i18n.getDefaultLocale(),
);
i18n.activate(bestLocale);

// If best locale's language wasn't returned--but is available from the server
// (eg. a cached version of the minimal TCF)-- we need to initialize with a language
// that was returned to later get the best one, as possible (eg. full TCF response).
// Otherwise, we can simply initialize with the best locale.
const isBestLocaleInTranslations: boolean =
!!experience.experience_config?.translations?.find(
(translation) => translation.language === bestLocale,
);
if (!isBestLocaleInTranslations) {
const bestTranslation = selectBestExperienceConfigTranslation(
i18n,
experience.experience_config!,
);
const bestAvailableLocale = bestTranslation?.language || bestLocale;
i18n.activate(bestTranslation?.language || bestLocale);
debugLog(
options?.debug,
`Initialized Fides i18n with available translations = ${bestAvailableLocale}`,
);
} else {
i18n.activate(bestLocale);
debugLog(
options?.debug,
`Initialized Fides i18n with best locale match = ${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
Expand All @@ -555,11 +587,6 @@ export function initializeI18n(
) {
loadGVLMessagesFromExperience(i18n, experience);
}

debugLog(
options?.debug,
`Initialized Fides i18n with best locale match = ${bestLocale}`,
);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion clients/fides-js/src/lib/initOverlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const initOverlay = async ({
try {
debugLog(
options.debug,
"Rendering Fides overlay CSS & HTML into the DOM...",
"Injecting Fides overlay CSS & HTML into the DOM...",
);

// If this function is called multiple times (e.g. due to calling
Expand Down
5 changes: 3 additions & 2 deletions clients/privacy-center/pages/api/fides-js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export default async function handler(
options,
},
options: {
debug: environment.settings.DEBUG,
debug: environment.settings.DEBUG || req.query.debug === "true",
geolocationApiUrl: environment.settings.GEOLOCATION_API_URL,
isGeolocationEnabled: environment.settings.IS_GEOLOCATION_ENABLED,
isOverlayEnabled: environment.settings.IS_OVERLAY_ENABLED,
Expand Down Expand Up @@ -338,7 +338,8 @@ export default async function handler(
// Allow CORS since this is a static file we do not need to lock down
.setHeader("Access-Control-Allow-Origin", "*")
.setHeader("Cache-Control", stringify(cacheHeaders))
.setHeader("Vary", LOCATION_HEADERS)
// Ignore cache if user's geolocation or language changes
.setHeader("Vary", [...LOCATION_HEADERS, "Accept-Language"])
.send(script);
}

Expand Down

0 comments on commit 1b1ab1f

Please sign in to comment.