Some checks failed
CI / Detect changes (push) Has been cancelled
CI / API Lint (push) Has been cancelled
CI / Admin UI Tests (push) Has been cancelled
CI / Admin UI Build (push) Has been cancelled
CI / API Tests (push) Has been cancelled
CI / Scanner Lint (push) Has been cancelled
CI / Scanner Tests (push) Has been cancelled
CI / Banner Lint & Typecheck (push) Has been cancelled
CI / Banner Tests (push) Has been cancelled
CI / Banner Build (push) Has been cancelled
CI / Admin UI Typecheck (push) Has been cancelled
188 lines
8.9 KiB
TypeScript
188 lines
8.9 KiB
TypeScript
/**
|
|
* Banner i18n — locale detection and string translation.
|
|
*
|
|
* Loads translations from CDN or uses built-in defaults.
|
|
* Supports string interpolation via {{key}} placeholders.
|
|
*/
|
|
|
|
export interface TranslationStrings {
|
|
title: string;
|
|
description: string;
|
|
acceptAll: string;
|
|
rejectAll: string;
|
|
managePreferences: string;
|
|
savePreferences: string;
|
|
privacyPolicyLink: string;
|
|
closeLabel: string;
|
|
categoryNecessary: string;
|
|
categoryNecessaryDesc: string;
|
|
categoryFunctional: string;
|
|
categoryFunctionalDesc: string;
|
|
categoryAnalytics: string;
|
|
categoryAnalyticsDesc: string;
|
|
categoryMarketing: string;
|
|
categoryMarketingDesc: string;
|
|
categoryPersonalisation: string;
|
|
categoryPersonalisationDesc: string;
|
|
cookieCount: string;
|
|
}
|
|
|
|
/** Built-in English (GB) translations — used as fallback. */
|
|
export const DEFAULT_TRANSLATIONS: TranslationStrings = {
|
|
title: 'We use cookies',
|
|
description:
|
|
'We use cookies and similar technologies to enhance your browsing experience, analyse site traffic, and personalise content. You can choose which categories to allow. [Privacy Policy]({{privacy_policy}}) [Terms & Conditions]({{terms}})',
|
|
acceptAll: 'Accept all',
|
|
rejectAll: 'Reject all',
|
|
managePreferences: 'Manage preferences',
|
|
savePreferences: 'Save preferences',
|
|
privacyPolicyLink: 'Privacy Policy',
|
|
closeLabel: 'Close',
|
|
categoryNecessary: 'Necessary',
|
|
categoryNecessaryDesc: 'Essential for the website to function. Always active.',
|
|
categoryFunctional: 'Functional',
|
|
categoryFunctionalDesc: 'Enable enhanced functionality and personalisation.',
|
|
categoryAnalytics: 'Analytics',
|
|
categoryAnalyticsDesc: 'Help us understand how visitors interact with the site.',
|
|
categoryMarketing: 'Marketing',
|
|
categoryMarketingDesc: 'Used to deliver personalised advertisements.',
|
|
categoryPersonalisation: 'Personalisation',
|
|
categoryPersonalisationDesc: 'Enable content personalisation based on your profile.',
|
|
cookieCount: '{{count}} cookies used on this site',
|
|
};
|
|
|
|
/** Built-in Thai translations — no API call needed. */
|
|
export const THAI_TRANSLATIONS: TranslationStrings = {
|
|
title: 'เรามีการใช้คุ๊กกี้เพื่อเก็บข้อมูล',
|
|
description:
|
|
'เว็บไซต์นี้ใช้คุกกี้เพื่อปรับปรุงประสบการณ์การใช้งาน วิเคราะห์การเข้าชม และแสดงโฆษณาที่เหมาะกับคุณ คุณสามารถเลือกได้ว่าจะอนุญาตคุกกี้ประเภทใด คุกกี้ที่จำเป็นจะถูกเปิดใช้งานเสมอเพื่อให้เว็บไซต์ทำงานได้ [นโยบายความเป็นส่วนตัว]({{privacy_policy}}) [ข้อกำหนดและเงื่อนไข]({{terms}})',
|
|
acceptAll: 'ยอมรับทั้งหมด',
|
|
rejectAll: 'ปฏิเสธทั้งหมด',
|
|
managePreferences: 'ตั้งค่า',
|
|
savePreferences: 'บันทึก',
|
|
privacyPolicyLink: 'นโยบายความเป็นส่วนตัว',
|
|
closeLabel: 'ปิด',
|
|
categoryNecessary: 'จำเป็น',
|
|
categoryNecessaryDesc: 'คุกกี้ที่จำเป็นสำหรับการทำงานพื้นฐานของเว็บไซต์ เช่น การจดจำตะกร้าสินค้า การเข้าสู่ระบบ และความปลอดภัย — เปิดใช้งานเสมอเพราะเว็บไซต์ไม่สามารถทำงานได้ถ้าไม่มี',
|
|
categoryFunctional: 'ฟังก์ชั่น',
|
|
categoryFunctionalDesc: 'คุกกี้การตลาดใช้เพื่อให้เว็บไชต์มีฟังก์ชั่นพิเศษ ถ้าปิดการทำงานอาจจะทำให้บางฟังก์ชั่นของเว็บไซต์ใช้งานไม่ได้',
|
|
categoryAnalytics: 'การวิเคราะห์',
|
|
categoryAnalyticsDesc: 'คุกกี้เหล่านี้ช่วยให้เราเข้าใจว่าผู้เข้าชมใช้งานเว็บไซต์อย่างไร เช่น หน้าไหนที่เข้าบ่อย คลิกที่ไหน - ข้อมูลเหล่านี้ช่วยเราพัฒนาเว็บไซต์ให้ดีขึ้นเรื่อยๆ',
|
|
categoryMarketing: 'การตลาด',
|
|
categoryMarketingDesc: 'คุกกี้การตลาดใช้ติดตามพฤติกรรมการเข้าชมเว็บไซต์เพื่อแสดงโฆษณาที่เกี่ยวข้องกับความสนใจของคุณบนแพลตฟอร์มอื่นด้วย เช่น Facebook และ Google',
|
|
categoryPersonalisation: 'ส่วนตัว',
|
|
categoryPersonalisationDesc: 'คุกกี้การตลาดใช้สำหรับเก็บข้อมูลส่วนตัว',
|
|
cookieCount: 'มีคุกกี้ {{count}} บนเว็บไซต์นี้',
|
|
};
|
|
|
|
/** Built-in translations that don't require an API call. */
|
|
const BUILT_IN_TRANSLATIONS: Record<string, TranslationStrings> = {
|
|
en: DEFAULT_TRANSLATIONS,
|
|
th: THAI_TRANSLATIONS,
|
|
};
|
|
|
|
/**
|
|
* Detect the user's preferred locale.
|
|
*
|
|
* Priority: 1) explicit data-locale attribute, 2) navigator.language,
|
|
* 3) document lang attribute, 4) 'en'.
|
|
*/
|
|
export function detectLocale(): string {
|
|
// Check for explicit override on the script tag
|
|
const scriptEl = document.querySelector('script[data-site-id]');
|
|
const explicit = scriptEl?.getAttribute('data-locale');
|
|
if (explicit) return normaliseLocale(explicit);
|
|
|
|
// Browser language
|
|
if (typeof navigator !== 'undefined' && navigator.language) {
|
|
return normaliseLocale(navigator.language);
|
|
}
|
|
|
|
// Document lang attribute
|
|
const docLang = document.documentElement.lang;
|
|
if (docLang) return normaliseLocale(docLang);
|
|
|
|
return 'en';
|
|
}
|
|
|
|
/**
|
|
* Normalise a locale string to a two-letter language code.
|
|
* e.g. 'en-GB' → 'en', 'fr-FR' → 'fr'
|
|
*/
|
|
export function normaliseLocale(locale: string): string {
|
|
return locale.split('-')[0].toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* Fetch translations for a locale from the public API endpoint.
|
|
* Returns null if not found or on error.
|
|
*/
|
|
export async function fetchTranslations(
|
|
apiBase: string,
|
|
siteId: string,
|
|
locale: string,
|
|
): Promise<Partial<TranslationStrings> | null> {
|
|
try {
|
|
const resp = await fetch(`${apiBase}/api/v1/translations/${siteId}/${locale}`);
|
|
if (!resp.ok) return null;
|
|
// Public API returns the raw strings dict. Accept a wrapped `{ strings }`
|
|
// shape too for backwards compatibility with older mocks/clients.
|
|
const data = await resp.json() as Partial<TranslationStrings> | { strings?: Partial<TranslationStrings> };
|
|
return 'strings' in data && data.strings ? data.strings : data as Partial<TranslationStrings>;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load translations: use built-in if available, otherwise fetch from API.
|
|
*/
|
|
export async function loadTranslations(
|
|
apiBase: string,
|
|
siteId: string,
|
|
locale: string,
|
|
): Promise<TranslationStrings> {
|
|
// Built-in translations (en, th, etc.) — no API call needed
|
|
const builtIn = BUILT_IN_TRANSLATIONS[locale];
|
|
if (builtIn) {
|
|
return { ...builtIn };
|
|
}
|
|
|
|
const remote = await fetchTranslations(apiBase, siteId, locale);
|
|
if (!remote) {
|
|
return { ...DEFAULT_TRANSLATIONS };
|
|
}
|
|
|
|
// Merge remote over defaults so missing keys fall back to English
|
|
return { ...DEFAULT_TRANSLATIONS, ...remote };
|
|
}
|
|
|
|
/**
|
|
* Interpolate placeholders in a translation string.
|
|
* e.g. interpolate('{{count}} cookies', { count: '12' }) → '12 cookies'
|
|
*/
|
|
export function interpolate(
|
|
template: string,
|
|
values: Record<string, string>,
|
|
): string {
|
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key: string) => values[key] ?? '');
|
|
}
|
|
|
|
/**
|
|
* Render markdown-style links as HTML anchor tags and strip orphaned links.
|
|
*
|
|
* Converts `[text](url)` to `<a href="url" ...>text</a>`.
|
|
* If the URL is empty (because the config value wasn't set), the entire
|
|
* `[text]()` fragment is removed so no broken links appear.
|
|
*/
|
|
export function renderLinks(html: string, linkClass: string = 'consentos-banner__link'): string {
|
|
// Remove links with empty URLs (including surrounding whitespace)
|
|
let result = html.replace(/\s*\[([^\]]*)\]\(\s*\)\s*/g, '');
|
|
// Convert remaining markdown links to <a> tags
|
|
result = result.replace(
|
|
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
`<a href="$2" target="_blank" rel="noopener" class="${linkClass}">$1</a>`,
|
|
);
|
|
return result;
|
|
}
|