UNPKG

edges-svelte-translations

Version:

Elegant, type-safe and SSR-friendly internationalization for edges-svelte

151 lines (150 loc) 6.6 kB
import { createStore } from 'edges-svelte'; import { RequestContext } from 'edges-svelte/context'; import { browser } from '$app/environment'; export const createTranslations = (params) => { const translations = {}; const channels = new Map(); const pluralize = (num, titles) => { if (titles.length !== 2 && titles.length !== 3) { throw new Error('Titles array in plural func must have 2 or 3 elements'); } const number = Math.abs(num); if (titles.length === 2) { return number === 1 ? titles[0] : titles[1]; } else { const cases = [2, 0, 1, 1, 1, 2]; if (number % 100 > 4 && number % 100 < 20) { return titles[2]; } return titles[cases[number % 10 < 5 ? number % 10 : 5]]; } }; return createStore('CreateTranslationsProviderEdgesTranslatePackage', ({ createState, createDerivedState }) => { const locale = createState(undefined); const t = createDerivedState([locale], ([$locale]) => { return (translation, vars) => { const key = typeof translation === 'object' ? translation.key : translation; const variables = typeof translation === 'object' ? translation.vars : vars; return key ? translate(String($locale), key, variables ?? {}) : ''; }; }); const locales = Object.keys(params.messages).reduce((acc, curr) => { acc[curr] = curr; return acc; }, {}); const checkLang = (lang) => { return lang && lang in locales ? locales[lang] : undefined; }; const getLocaleToSet = (event) => { const cookieLang = params.cookieName ? checkLang(event?.cookies.get(params.cookieName)) : undefined; const requestLang = params.initLangFromAcceptLanguage ? event?.request.headers.get('accept-language')?.split(',')[0].substring(0, 2) : undefined; const baseLang = checkLang(requestLang) ?? params.initLang; return cookieLang ?? baseLang; }; const preloadTranslation = async (event, callback) => { const lang = callback ? callback() : getLocaleToSet(event); if (event && !event.locals.translations && !event.locals.lang) { event.locals.translations = event.locals.translations ?? (await params.messages[lang]()); event.locals.lang = lang; locale.set(lang); } }; const syncTranslation = async (data, fromEvent = true) => { const langToSync = checkLang(data.lang) ?? params.initLang; if (fromEvent && data.translations) { translations[langToSync] = data.translations; } else { await loadTranslation(langToSync); } }; const getTranslations = (locale) => { return !browser ? RequestContext.current().event?.locals.translations : translations[locale]; }; const translate = (locale, messageKey, vars = {}) => { if (!messageKey) return ''; if (!locale) return messageKey; const currentTranslation = getTranslations(locale); if (!currentTranslation) return messageKey; const messageKeys = messageKey.split('.'); let result = messageKeys.reduce((acc, key) => { if (typeof acc === 'string') { return acc; } else if (typeof acc === 'object' && key in acc) { return acc[key]; } return messageKey; }, currentTranslation); if (typeof result === 'string') { result = result.replace(/{{\s*(\w+)\s*\|\s*plural\s*:(.*?)}}/g, (_, varName, titlesStr) => { const titles = titlesStr.split(',').map((t) => t.trim()); const count = Number(vars[varName]); if (Number.isNaN(count)) { console.warn(`Pluralization failed: variable "${varName}" is not a number.`); return ''; } return pluralize(count, titles); }); result = result.replace(/{{\s*(\w+)\s*}}/g, (_, k) => { return vars[k] !== undefined ? String(vars[k]) : ''; }); return result; } return messageKey; }; const loadTranslation = async (lang) => { if (!lang || !(lang in params.messages)) { throw Error('Lang is missing in messages'); } if (!translations[lang]) { translations[lang] = await params.messages[lang](); } }; const localeChangeEvent = (locale) => { const listeners = channels.get('localeChanged'); if (!listeners || listeners.length === 0) { return; } listeners.forEach((listener) => listener(locale)); }; const subscribeLocaleChangeEvent = (listener) => { if (!channels.has('localeChanged')) { channels.set('localeChanged', []); } channels.get('localeChanged').push(listener); }; const switchLocale = async (newLocale) => { if (browser) { await loadTranslation(newLocale); locale.set(newLocale); if (params.cookieName) { document.cookie = `${encodeURIComponent(params.cookieName)}=${encodeURIComponent(String(newLocale))}; path=/`; } const html = document.querySelector('html'); if (html) { html.lang = String(newLocale); } localeChangeEvent(newLocale); } else { throw Error('Do not switch locale on server side'); } }; const applyHtmlLocaleAttr = (html) => { const event = RequestContext.current().event; const lang = checkLang(event?.locals?.lang); if (lang) { return html.replace('%lang%', String(lang)); } return html; }; return { preloadTranslation, syncTranslation, switchLocale, applyHtmlLocaleAttr, subscribeLocaleChangeEvent, t, locale, locales }; }); };