sourceappgrimm.js

import attic from "./attic.js";

import defaultLocale from "./locales/default.js";
import enLocale from "./locales/en.js";
import nlLocale from "./locales/nl.js";
import cyLocale from "./locales/cy.js";
import eoLocale from "./locales/eo.js";
import grcLocale from "./locales/grc.js";
import plLocale from "./locales/pl.js";
import zhLocale from "./locales/zh.js";
import zhHantLocale from "./locales/zh-Hant.js";

const getNestedObject = (nestedObj, pathArr) => {
	return pathArr.reduce(
		(obj, key) => (obj && obj[key] !== "undefined" ? obj[key] : undefined),
		nestedObj
	);
};

let grimm = {
	escape: text =>
		text.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/&/g, "&amp;"),
	enumerate: (num, system) => {
		if (system == "roman") {
			if (num == 0) {
				return "N";
			}
			const tensome = (i, v, x) => [
				"",
				`${i}`,
				`${i}${i}`,
				`${i}${i}${i}`,
				`${i}${v}`,
				`${v}`,
				`${v}${i}`,
				`${v}${i}${i}`,
				`${v}${i}${i}${i}`,
				`${i}${x}`
			];
			const numerals = [
				tensome("I", "V", "X"),
				tensome("X", "L", "C"),
				tensome("C", "D", "M"),
				tensome("M", "ↁ", "ↂ")
			];
			let output = "";

			for (let i = numerals.length - 1; i >= 0; i--) {
				output += numerals[i][Math.floor(num / 10 ** i) % 10];
			}
			return output;
		}
		if (system == "greek") {
			if (num == 0) {
				return "·ʹ";
			}
			const numerals = [
				["", "α", "β", "γ", "δ", "ε", "ϝ", "ζ", "η", "θ"],
				["", "ι", "κ", "λ", "μ", "ν", "ξ", "ο", "π", "ϟ"],
				["", "ρ", "σ", "τ", "υ", "φ", "χ", "ψ", "ω", "ϡ"]
			];
			numerals.push(numerals[0].map(x => (x == "" ? "" : ${x}`)));

			let output = "";
			for (let i = numerals.length - 1; i >= 0; i--) {
				output += numerals[i][Math.floor(num / 10 ** i) % 10];
			}
			return `${output}ʹ`;
		}
		return `${num}`;
	},
	slugify: (text, options) => {
		text = `${text}`.normalize("NFD");
		if (options?.html) {
			text = text.replace(/<(.*?)>/g, "");
		}
		return text
			.replace(/[\u0300-\u036f]/g, "")
			.toLowerCase()
			.replace(/[^a-z0-9]+/g, "-")
			.replace(/(^-|-$)+/g, "");
	},
	sentenceCase: (locale, text) =>
		text[0].toLocaleUpperCase(
			grimm.dict?.[locale]?.meta?.intlFallback ?? locale
		) + text.slice(1),
	translate: (locale, keyword, ...args) => {
		let keywordArray = keyword.split(".");

		let gazettedName = undefined;
		if (keyword.match(/^loc\.language\./)) {
			const languageGazetteer = new Intl.DisplayNames(
				[...(grimm.dict?.[locale]?.meta?.intlFallback ?? locale)],
				{
					type: "language"
				}
			);

			if (
				languageGazetteer.of(keyword.replace("loc.language.", "")) !=
				keyword.replace("loc.language.", "")
			) {
				gazettedName = languageGazetteer.of(
					keyword.replace("loc.language.", "")
				);
			}
		} else if (keyword.match(/^loc\.country\./)) {
			const localGazetteer = new Intl.DisplayNames(
				[...(grimm.dict?.[locale]?.meta?.intlFallback ?? locale)],
				{
					type: "region"
				}
			);

			if (
				keyword.replace("loc.country.", "").length == 2 &&
				localGazetteer.of(
					keyword.replace("loc.country.", "").toUpperCase()
				) != keyword.replace("loc.country.", "").toUpperCase()
			) {
				gazettedName = localGazetteer.of(
					keyword.replace("loc.country.", "").toUpperCase()
				);
			}
		}

		const firstWorkingFallback = [
			locale,
			...(grimm.dict?.[locale]?.meta?.fallback ?? []),
			"default"
		].find(
			el => getNestedObject(grimm.dict?.[el], keywordArray) !== undefined
		);

		const fallbackResult = firstWorkingFallback
			? getNestedObject(grimm.dict?.[firstWorkingFallback], keywordArray)
			: undefined;

		return fallbackResult === undefined
			? gazettedName || `[${keyword}]`
			: typeof fallbackResult == "function" && args.length
			? fallbackResult(...args)
			: fallbackResult;
	},
	translator:
		locale =>
		(...args) =>
			grimm.translate(locale, ...args),
	num: (locale, x) =>
		grimm.dict?.[locale]?.help.num
			? grimm.dict[locale].help.num(x)
			: new Intl.NumberFormat(
					grimm.dict?.[locale]?.meta?.intlFallback ?? locale
			  ).format(x),
	relativeDate: (locale, ms) => {
		let formatter = new Intl.RelativeTimeFormat(
			grimm.dict?.[locale]?.meta?.intlFallback ?? locale
		);

		if (Math.abs(ms) >= 365.2425 * 24 * 60 * 60 * 1000) {
			return formatter.format(
				Math.round(ms / (365.2425 * 24 * 60 * 60 * 1000)),
				"year"
			);
		}
		if (Math.abs(ms) >= (365.2425 / 12) * 24 * 60 * 60 * 1000) {
			return formatter.format(
				Math.round(ms / ((365.2425 / 12) * 24 * 60 * 60 * 1000)),
				"month"
			);
		}
		if (Math.abs(ms) >= 7 * 24 * 60 * 60 * 1000) {
			return formatter.format(
				Math.round(ms / (7 * 24 * 60 * 60 * 1000)),
				"week"
			);
		}
		if (Math.abs(ms) >= 24 * 60 * 60 * 1000) {
			return formatter.format(
				Math.round(ms / (24 * 60 * 60 * 1000)),
				"day"
			);
		}
		if (Math.abs(ms) >= 60 * 60 * 1000) {
			return formatter.format(Math.round(ms / (60 * 60 * 1000)), "hour");
		}
		if (Math.abs(ms) >= 60 * 1000) {
			return formatter.format(Math.round(ms / (60 * 1000)), "minute");
		}
		return formatter.format(Math.round(ms / 1000), "second");
	},
	date: {
		// inputOptions.precision
		// 1: 1969 (year)
		// 2: 1969 July (month)
		// 3: 1969 July 20 (day)
		// 4: 1969 July 20 at 16 o’clock (hour)
		// 5: 1969 July 20 at 16.29 (minute)
		// 6: 1969 July 20 at 16.29.30 (second)
		// 7: 1969 July 20 at 16.29.30.111 (millisecond)
		ce: (locale, stamp, precision, inputOptions = {}) => {
			let date;
			if (typeof stamp == "string") {
				const matched = stamp.match(
					/(c)?([+-]?[0-9]+)(?:-([0-9]{2}))?(?:-([0-9]{2}))?(?:T([0-9]{2}):?([0-9]{2})?:?([0-9]{2})?(\.[0-9]+)?)?(Z|[+-][0-9]{2}(?::?[0-9]{2})?)?/
				);
				const stampHasDashes =
					matched[2].match(/[+-]/) || matched[2].length != 8;

				date = {
					circa: matched[1] ? true : false,
					year: stampHasDashes
						? parseInt(matched[2])
						: parseInt(matched[2].slice(0, 4)),
					month: stampHasDashes
						? matched[3]
							? parseInt(matched[3])
							: 6
						: parseInt(matched[2].slice(4, 6)),
					day: stampHasDashes
						? matched[4]
							? parseInt(matched[4])
							: 3
						: parseInt(matched[2].slice(6, 8)),
					hour: matched[5] ? parseInt(matched[5]) : 12,
					minute: matched[6] ? parseInt(matched[6]) : 0,
					second: matched[7] ? parseInt(matched[7]) : 0,
					millisecond: matched[8]
						? Math.round(1000 * parseFloat(matched[8]))
						: 0,
					timezone: matched[9]
						? matched[9]
								.replace(
									/^([+-])([0-9]{2})([0-9]{2})$/,
									"$1$2:$3"
								)
								.replace(/^([+-])([0-9]{2})$/, "$1$2:00")
						: "",
					stampPrecision: matched.slice(2).includes(undefined)
						? matched.slice(2).indexOf(undefined)
						: 7
				};

				date.stamp = `${Math.max(date.year, 1)
					.toString()
					.padStart(4, "0")}-${date.month
					.toString()
					.padStart(2, "0")}-${date.day
					.toString()
					.padStart(2, "0")}T${date.hour
					.toString()
					.padStart(2, "0")}:${date.minute
					.toString()
					.padStart(2, "0")}:${date.second
					.toString()
					.padStart(2, "0")}.${date.millisecond
					.toString()
					.padStart(3, "0")}${date.timezone}`;

				date.date = new Date(date.stamp);
				if (date.year < 1) {
					date.date.setYear(date.year);
				}

				precision = Math.min(
					date.stampPrecision,
					precision ?? Infinity
				);
			} else {
				date = {
					circa: false,
					date: stamp
				};

				precision = inputOptions.precision ?? Infinity;
			}

			const options = {
				year: "numeric",
				month: precision >= 2 ? "long" : undefined,
				day: precision >= 3 ? "2-digit" : undefined,
				hour: precision >= 4 ? "2-digit" : undefined,
				minute: precision >= 5 ? "2-digit" : undefined,
				second: precision >= 6 ? "2-digit" : undefined,
				hour12: false,
				timeZone: "Europe/London"
			};
			Object.assign(options, inputOptions);

			const dateFormatter = grimm.dict?.[locale]?.help?.CustomDate
				? new grimm.dict[locale].help.CustomDate(options)
				: new Intl.DateTimeFormat(
						grimm.dict?.[locale]?.meta?.intlFallback ?? locale,
						options
				  );

			let returnDate = dateFormatter.format(date.date);
			if (date.circa) {
				returnDate = grimm.translate(locale, "pan.circa", returnDate);
			}

			return returnDate;
		},
		attic: (locale, date, inputOptions = {}) => {
			let options = {
				dateStyle: "long",
				numerals: "greek",
				signedDates: true
			};
			Object.assign(options, inputOptions);

			const obj = attic(date);

			let dateNumber = "";
			if (options.signedDates) {
				dateNumber = obj.signedDate
					? `${grimm.enumerate(
							Math.abs(obj.signedDate),
							options.numerals
					  )}${
							grimm.translate(locale, "attic.moons")[
								Math.sign(obj.signedDate) + 1
							]
					  }`
					: grimm.translate(locale, "attic.moons")[1];
			} else {
				dateNumber = grimm.enumerate(obj.rawDate, options.numerals);
			}

			return grimm.translate(
				locale,
				`attic.template.${options.dateStyle}`,
				grimm.enumerate(obj.olympiad, options.numerals),
				grimm.enumerate(obj.year, options.numerals),
				grimm.translate(locale, `attic.months.${options.dateStyle}`)[
					obj.month
				],
				dateNumber
			);
		},

		dual: (locale, date) =>
			`${grimm.date.attic(locale, date)}${grimm.translate(
				locale,
				"pan.dateSep"
			)}${grimm.date.ce(locale, date, 3)}`
	}
};

grimm.dict = {
	default: defaultLocale,
	en: enLocale(grimm),
	nl: nlLocale(grimm),
	grc: grcLocale(grimm),
	cy: cyLocale(grimm),
	pl: plLocale(grimm),
	eo: eoLocale(grimm),
	"zh-Hant": zhHantLocale(grimm),
	zh: zhLocale(grimm),
	ar: {
		meta: {
			id: "ar",
			code: "ar-SA",

			name: "العربية",
			sort: "arabiyyah",
			flag: "arableague",

			fallback: ["en"],
			intlFallback: ["ar-SA"]
		}
	},
	es: {
		meta: {
			id: "es",
			code: "es-ES",

			name: "Castellano",
			sort: "castellano",
			flag: "es",

			fallback: ["en"],
			intlFallback: ["es-ES"]
		}
	},
	de: {
		meta: {
			id: "de",
			code: "de-DE",

			name: "Deutsch",
			sort: "deutsch",
			flag: "de",

			fallback: ["en"],
			intlFallback: ["de-DE"]
		}
	},
	el: {
		meta: {
			id: "el",
			code: "el-GR",

			name: "Ελληνικά",
			sort: "ellinika",
			flag: "gr",

			fallback: ["grc", "en"],
			intlFallback: ["el-GR"]
		}
	},
	fr: {
		meta: {
			id: "fr",
			code: "fr-FR",

			name: "Français",
			sort: "francais",
			flag: "fr",

			fallback: ["en"],
			intlFallback: ["fr-FR"]
		}
	},
	hi: {
		meta: {
			id: "hi",
			code: "hi-IN",

			name: "हिन्दी",
			sort: "hindi",
			flag: "in",

			fallback: ["en"],
			intlFallback: ["hi-IN"]
		}
	},
	sw: {
		meta: {
			id: "sw",
			code: "sw-TZ",

			name: "Kiswahili",
			sort: "kiswahili",
			flag: "tz",

			fallback: ["en"],
			intlFallback: ["sw-TZ", "en"]
		}
	},
	ja: {
		meta: {
			id: "ja",
			code: "ja-JP",

			name: "日本語",
			sort: "nihongo",
			flag: "jp",

			fallback: ["en"],
			intlFallback: ["ja-JP"]
		}
	}
};

export default grimm;