Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into intl-purposes-desc
Browse files Browse the repository at this point in the history
  • Loading branch information
kate-kazantseva committed Oct 22, 2024
2 parents 1dffe31 + 58b2119 commit b737572
Show file tree
Hide file tree
Showing 17 changed files with 20,831 additions and 24,447 deletions.
35,542 changes: 16,119 additions & 19,423 deletions .pnp.cjs

Large diffs are not rendered by default.

1,250 changes: 667 additions & 583 deletions .pnp.loader.mjs

Large diffs are not rendered by default.

Binary file not shown.
546 changes: 0 additions & 546 deletions .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs

This file was deleted.

9 changes: 0 additions & 9 deletions .yarn/plugins/@yarnpkg/plugin-typescript.cjs

This file was deleted.

873 changes: 0 additions & 873 deletions .yarn/releases/yarn-3.4.1.cjs

This file was deleted.

934 changes: 934 additions & 0 deletions .yarn/releases/yarn-4.5.1.cjs

Large diffs are not rendered by default.

12 changes: 5 additions & 7 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
npmPublishAccess: public
compressionLevel: mixed

enableGlobalCache: false

plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: "@yarnpkg/plugin-typescript"
npmPublishAccess: public

yarnPath: .yarn/releases/yarn-3.4.1.cjs
yarnPath: .yarn/releases/yarn-4.5.1.cjs
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/transcend-io/consent-manager-ui.git"
},
"homepage": "https://github.com/transcend-io/consent-manager-ui",
"version": "4.18.1",
"version": "4.21.1",
"license": "MIT",
"main": "build/ui",
"files": [
Expand Down Expand Up @@ -83,5 +83,5 @@
"ts-node": "^10.5.0",
"typescript": "^4.7.4"
},
"packageManager": "yarn@3.4.1"
"packageManager": "yarn@4.5.1"
}
21 changes: 11 additions & 10 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,16 @@ export function App({
});

// Language setup
const { language, handleChangeLanguage, messages } = useLanguage({
supportedLanguages,
translationsLocation:
// Order of priority:
// 1. Take airgap.js data-messages
// 2. Take consentManagerConfig.messages
// 3. Look for translations locally
settings.messages || config.messages || './translations',
});
const { language, handleChangeLanguage, messages, htmlTagVariables } =
useLanguage({
supportedLanguages,
translationsLocation:
// Order of priority:
// 1. Take airgap.js data-messages
// 2. Take consentManagerConfig.messages
// 3. Look for translations locally
settings.messages || config.messages || './translations',
});

// Create the `transcend` API
const consentManagerAPI = makeConsentManagerAPI({
Expand Down Expand Up @@ -121,7 +122,7 @@ export function App({
{/** Ensure messages are loaded before any UI is displayed */}
{messages ? (
<Main
globalUiVariables={currentVariables}
globalUiVariables={{ ...currentVariables, ...htmlTagVariables }}
airgap={airgap}
modalOpenAuth={auth}
viewState={viewState}
Expand Down
14 changes: 14 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const {
secondaryPolicy,
languages,
dismissedViewState = 'Hidden',
nonce,
inlineCss,
} = settings;

/**
Expand Down Expand Up @@ -200,3 +202,15 @@ function validateConfig(config: ConsentManagerConfig): boolean {

return true;
}

export const CSP_NONCE = nonce;
if (CSP_NONCE) {
const currentScriptDataset = document.currentScript?.dataset;
if (currentScriptDataset) {
// hide nonce from other scripts
delete currentScriptDataset.nonce;
}
}

export const ALLOW_INLINE_CSS = inlineCss !== 'off';
export const EXTERNALIZE_INLINE_CSS = inlineCss === 'data:';
52 changes: 41 additions & 11 deletions src/consent-manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import type {
import { App } from './components/App';
import { logger } from './logger';
import { createHTMLElement } from './utils/create-html-element';
import { getMergedConfig } from './config';
import {
ALLOW_INLINE_CSS,
CSP_NONCE,
EXTERNALIZE_INLINE_CSS,
getMergedConfig,
} from './config';
import { CSS_RESET } from './constants';

// The `transcend` API: methods which we'll create inside Preact and pass back out here via callback
let consentManagerAPI: ConsentManagerAPI | null = null;
Expand All @@ -31,6 +37,11 @@ export const injectConsentManagerApp = async (
consentManager.style.zIndex = mergedConfig.config.uiZIndex ?? '2147483647';
consentManager.id = 'transcend-consent-manager';

const attachToDoc = (): void => {
// Append UI container to doc to activate style.sheet
(document.documentElement || document).append(consentManager);
};

try {
const shadowRoot =
consentManager?.attachShadow?.({
Expand All @@ -46,18 +57,37 @@ export const injectConsentManagerApp = async (
appContainer ??= createHTMLElement('div');
shadowRoot.appendChild(appContainer);

// Don't inherit global styles
const style = appContainer.appendChild(
createHTMLElement<HTMLStyleElement>('style'),
);
if (ALLOW_INLINE_CSS) {
// Don't inherit global styles
const style = createHTMLElement<HTMLStyleElement | HTMLLinkElement>(
EXTERNALIZE_INLINE_CSS ? 'link' : 'style',
);

// Append UI container to doc to activate style.sheet
(document.documentElement || document).append(consentManager);
if (CSP_NONCE) {
style.nonce = CSP_NONCE;
}

if (EXTERNALIZE_INLINE_CSS) {
(style as HTMLLinkElement).rel = 'stylesheet';
(style as HTMLLinkElement).href = `data:text/css,${encodeURIComponent(
CSS_RESET,
)}`;
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
style
.sheet! // 1st rule so subsequent properties are reset
.insertRule(':host { all: initial }');
// activate stylesheet
// we want to activate AFTER setup for external and BEFORE setup for inline
appContainer.appendChild(style);
attachToDoc();

if (!EXTERNALIZE_INLINE_CSS) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(style as HTMLStyleElement)
.sheet! // 1st rule so subsequent properties are reset
.insertRule(CSS_RESET);
}
} else {
attachToDoc();
}

// Wait for the instantiated Consent Manager API from Preact
consentManagerAPI = await new Promise<ConsentManagerAPI>((resolve) => {
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const VERSION = process.env.VERSION as string;
export const CONSENT_OPTIONS = { confirmed: true, prompted: true };
export const CSS_RESET = ':host{all:initial}';
4 changes: 4 additions & 0 deletions src/css.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CSP_NONCE } from './config';
import { getAppContainer } from './consent-manager';
import { logger } from './logger';
import { createHTMLElement } from './utils/create-html-element';
Expand All @@ -13,6 +14,9 @@ export const injectCss = (stylesheetUrl: string): Promise<void> =>
const root = getAppContainer();
if (root && stylesheetUrl) {
const link = createHTMLElement<HTMLLinkElement>('link');
if (CSP_NONCE) {
link.nonce = CSP_NONCE;
}
link.type = 'text/css';
link.rel = 'stylesheet';
link.id = stylesheetUrl;
Expand Down
67 changes: 42 additions & 25 deletions src/hooks/useLanguage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Translations,
} from '@transcend-io/internationalization';
import { settings } from '../settings';
import { substituteHtml } from '../utils/substitute-html';

export const loadedTranslations: Translations = Object.create(null);

Expand Down Expand Up @@ -74,26 +75,6 @@ export const getNearestSupportedLanguage = (
),
);

/**
* Picks a default language for the user
*
* @param supportedLanguages - Set of supported languages
* @returns the language key of the best default language for this user
*/
export function pickDefaultLanguage(
supportedLanguages: ConsentManagerLanguageKey[],
): ConsentManagerLanguageKey {
if (settings.locale && supportedLanguages.includes(settings.locale)) {
return settings.locale;
}

const preferredLanguages = getUserLanguages();
return (
getNearestSupportedLanguage(preferredLanguages, supportedLanguages) ||
ConsentManagerLanguageKey.En
);
}

/**
* Sorts the supported languages by the user's preferences
*
Expand All @@ -119,6 +100,28 @@ export const sortSupportedLanguagesByPreference = (
return rank(a) - rank(b);
});

/**
* Picks a default language for the user
*
* @param supportedLanguages - Set of supported languages
* @returns the language key of the best default language for this user
*/
export function pickDefaultLanguage(
supportedLanguages: ConsentManagerLanguageKey[],
): ConsentManagerLanguageKey {
if (settings.locale && supportedLanguages.includes(settings.locale)) {
return settings.locale;
}

const preferredLanguages = getUserLanguages();
return (
getNearestSupportedLanguage(
preferredLanguages,
sortSupportedLanguagesByPreference(supportedLanguages),
) || ConsentManagerLanguageKey.En
);
}

/**
* Fetch message translations
*
Expand Down Expand Up @@ -163,6 +166,8 @@ export function useLanguage({
handleChangeLanguage: (language: ConsentManagerLanguageKey) => void;
/** Message translations */
messages: TranslatedMessages | undefined;
/** HTML opening/closing tab variables */
htmlTagVariables: Record<string, string>;
} {
// The current language
const [language, setLanguage] = useState<ConsentManagerLanguageKey>(() =>
Expand All @@ -173,22 +178,34 @@ export function useLanguage({
// Hold the translations for that language (fetched async)
const [messages, setMessages] = useState<TranslatedMessages | undefined>();

// Store the HTML opening/closing tags we need to replace our tag variables with
const [htmlTagVariables, setHtmlTagVariables] = useState<
Record<string, string>
>({});

// Load the default translations
useEffect(() => {
getTranslations(translationsLocation, language).then((messages) =>
setMessages(messages),
);
getTranslations(translationsLocation, language).then((messages) => {
// Replace raw HTML tags with variables bc raw HTML causes parsing errors
const { substitutedMessages, tagVariables } = substituteHtml(messages);
setHtmlTagVariables(tagVariables);
setMessages(substitutedMessages);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const handleChangeLanguage = useCallback(
async (language: ConsentManagerLanguageKey) => {
const newMessages = await getTranslations(translationsLocation, language);
setMessages(newMessages);

// Replace raw HTML tags with variables bc raw HTML causes parsing errors
const { substitutedMessages, tagVariables } = substituteHtml(newMessages);
setMessages(substitutedMessages);
setHtmlTagVariables(tagVariables);
setLanguage(language);
},
[setLanguage, translationsLocation],
);

return { language, handleChangeLanguage, messages };
return { language, handleChangeLanguage, messages, htmlTagVariables };
}
33 changes: 33 additions & 0 deletions src/utils/substitute-html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { TranslatedMessages } from '@transcend-io/internationalization';

/**
* Takes in a set of messages which may or may not contain HTML and replaces any
* HTML tags so that format.js can successfully internationalize them without
* running into parsing errors and using the fallback option
*
* @param messages - Raw precompiled messages (sometimes containing html tags)
* @returns Messages with all HTML opening/closing tags substituted and the variables to replace those substitutions
*/
export function substituteHtml(messages: TranslatedMessages): {
/** The set of messages with their HTML opening/closing tags substituted */
substitutedMessages: TranslatedMessages;
/** The set of variables used to replace the substitutions with their corresponding HTML opening/closing tags */
tagVariables: Record<string, string>;
} {
const substitutedMessages = { ...messages };
const tagVariables: Record<string, string> = {};
Object.entries(substitutedMessages).forEach(([key, rawMessage]) => {
let placeholderMessage = rawMessage;
const htmlTags = [...rawMessage.matchAll(/<[^>]+>/g)].flat();
htmlTags.forEach((tag, idx) => {
const uniqKey = key.replaceAll('.', '_');
placeholderMessage = placeholderMessage.replace(
tag,
`{tcm_${uniqKey}_tag_match_${idx}}`,
);
tagVariables[`tcm_${uniqKey}_tag_match_${idx}`] = tag;
});
substitutedMessages[key] = placeholderMessage;
});
return { substitutedMessages, tagVariables };
}
Loading

0 comments on commit b737572

Please sign in to comment.