import { derived, get, writable } from "svelte/store"; import { browser } from "$app/environment"; import yaml from "js-yaml"; // defines a single locale class Locale { constructor(code, icon) { // regex for "render"ing the locale this.ref_regex = /\[[0-9]*:.*?\]/gm; this.link_regex = /\[[^\]]*]\([^ ]*\)/gm; this.bold_regex = /\*\*.*?\*\*/gm; this.code = code; // BCP 47 language tag this.icon = icon; // icon for the locale this.all = {}; // all the locales } // load the locale async load() { const text = await import(`../locales/${this.code}.yaml?raw`); this.all = yaml.load(text.default); } // renders a given locale using given values and links render(locale, values, links) { // get rid of newlines and remove trailing/repeating spaces and stuff locale = locale.replaceAll("\r", "").replaceAll("\n", " "); locale = locale.trim(); // find and replace all the values for (let name in values) { // values cannot be objects if (typeof values[name] === "object") { continue; } // "{name}" will be replaced by the values locale = locale.replaceAll(`{${name}}`, values[name]); } // find all the references const refs = [...locale.matchAll(this.ref_regex)]; // and repalce them for (let i = 0; i < refs.length; i++) { let ref = refs[i][0]; let name = ref.replaceAll(/(^\[|\]$)/g, ""); let indx = parseInt(name.charAt(0)) - 1; name = name.substring(2); // check the index if (indx >= links.length) { continue; } // replace the reference with a link locale = locale.replaceAll(ref, `${name}`); } // look for []() patterns, which are used adding links, kinda like markdown // but this is shittier links = [...locale.matchAll(this.link_regex)]; // replace the found links for (let i = 0; i < links.length; i++) { let link = links[i][0]; let name = link.match(/(?<=\[).*?(?=])/g); let url = link.match(/(?<=\]\()[^ ]*(?=\))/g); // if we fail to extract the link name and/or URL, skip this match if (null === name || null === url) continue; locale = locale.replaceAll(link, `${name[0]}`); } // look for double stars which is used for bold text const bolds = [...locale.matchAll(this.bold_regex)]; // replace bold text with actual bold text for (let i = 0; i < bolds.length; i++) { let bold = bolds[i][0]; let text = bold.match(/(?<=\*\*).*?(?=\*\*)/g); // if we fail to extract the text content, skip this match if (null === text) continue; locale = locale.replaceAll(bold, `${text}`); } return locale; } // resolve the given locale resolve(key, vars = {}) { let cur = this.all; let keys = key.split("."); for (let i = 0; i < keys.length; i++) { cur = cur[keys[i]]; if (cur === undefined) { return cur; } } // locale needs to be a string if (typeof cur !== "string") { return undefined; } // extract links from the vars let links = vars.links; delete vars.links; return this.render(cur, vars, links); } } // localizer stores and defines all the locales class Localizer { constructor() { // list of supported locales this.list = [ new Locale("en", "🇬🇧󠁧󠁢󠁥󠁮󠁧󠁿"), // English new Locale("tr", "🇹🇷"), // Turkish ]; this.current = writable(this.list[0]); // current locale this.next = writable(this.list[1]); // next locale this.fallback = this.list[0]; // fallback locale } // get the name of a language using the current locale name(code) { return get(this.current).name(code); } // get the current browser locale tag browser() { if (browser) { window.navigator.language.slice(0, 2).toLowerCase(); } else { return this.fallback.code; } } /* load all the locales, attempt to set the provided locale as the current * locale, if the provided locale is not available just use the default */ async setup(code) { for (let i = 0; i < this.list.length; i++) { await this.list[i].load(); } // find the locale index by it's code let indx = this.list.findIndex(locale => { return locale.code === code; }); // check the index to see if we found the locale if (indx < 0) { return false; } // if we actually found it, set it as the current locale this.current.set(this.list[indx]); // set the next locale if (++indx >= this.list.length) { this.next.set(this.list[0]); } else { this.next.set(this.list[indx]); } } // switch to the next locale switch() { // find the next locale's index let indx = this.list.findIndex(locale => { return locale === get(this.next); }); // set next locale as the new current locale this.current.set(get(this.next)); document.cookie = `locale=${get(this.next).code};`; // get the next locale based on the index if (indx === this.list.length - 1) { this.next.set(this.list[(indx = 0)]); } else { this.next.set(this.list[++indx]); } } resolve(key, vars = {}) { // attempt to resolve the given key let res = get(this.current).resolve(key, vars); if (res !== undefined) { return res; } // if we fail to resolve the key, try to resolve it using the fallback // locale, if that fails too then we are kinda fucked so yeah we just throw // an error if (get(this.current) === this.fallback) { throw new Error(`missing key: ${key}`); } else { return this.fallback.resolve(key, vars); } } } export const localizer = new Localizer(); // global localizer export const locale = localizer.current; // current locale export const next = localizer.next; // next locale // resolve a given locale by it's key export const _ = derived( locale, () => (key, vars = {}) => localizer.resolve(key, vars) ); export default localizer;