Files
website/app/src/lib/locale.js
2025-07-24 18:30:42 +03:00

226 lines
6.0 KiB
JavaScript

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, `<a href="${links[indx]}">${name}</a>`);
}
// 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, `<a href="${url[0]}">${name[0]}</a>`);
}
// 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, `<b>${text}</b>`);
}
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;