summaryrefslogtreecommitdiff
path: root/src/utils
diff options
context:
space:
mode:
authorJoão Augusto Costa Branco Marado Torres <torres.dev@disroot.org>2025-06-24 12:08:41 -0300
committerJoão Augusto Costa Branco Marado Torres <torres.dev@disroot.org>2025-06-24 12:50:43 -0300
commitf9a77c5c27aede4e5978eb55d9b7af781b680a1d (patch)
treed545e325ba1ae756fc2eac66fac1001b6753c40d /src/utils
feat!: initial commit
Signed-off-by: João Augusto Costa Branco Marado Torres <torres.dev@disroot.org>
Diffstat (limited to 'src/utils')
-rw-r--r--src/utils/anonymous.test.ts130
-rw-r--r--src/utils/anonymous.ts25
-rw-r--r--src/utils/bases.test.ts32
-rw-r--r--src/utils/bases.ts11
-rw-r--r--src/utils/datetime.test.ts63
-rw-r--r--src/utils/datetime.ts43
-rw-r--r--src/utils/index.ts19
-rw-r--r--src/utils/iterator.test.ts122
-rw-r--r--src/utils/iterator.ts52
-rw-r--r--src/utils/lang.test.ts97
-rw-r--r--src/utils/lang.ts56
11 files changed, 650 insertions, 0 deletions
diff --git a/src/utils/anonymous.test.ts b/src/utils/anonymous.test.ts
new file mode 100644
index 0000000..2da613f
--- /dev/null
+++ b/src/utils/anonymous.test.ts
@@ -0,0 +1,130 @@
+import { assert, assertEquals, assertFalse } from "@std/assert";
+import { describe, it } from "@std/testing/bdd";
+import {
+ defined,
+ equal,
+ extremeBy,
+ get,
+ getCall,
+ identity,
+ instanciate,
+ pass,
+} from "./anonymous.ts";
+import { assertSpyCalls, spy } from "@std/testing/mock";
+import { FALSE, TRUE } from "../../tests/fixtures/test_data.ts";
+
+describe("identity", () => {
+ it("returns the same value", () => {
+ assertEquals(identity(42), 42);
+ assertEquals(identity("hello"), "hello");
+ const obj = { a: 1 };
+ assertEquals(identity(obj), obj);
+ });
+});
+
+describe("defined", () => {
+ it("returns true for non-null/undefined values", () => {
+ assert(defined(0));
+ assert(defined(""));
+ const FALSE = false;
+ assert(defined(FALSE));
+ });
+
+ it("returns false for null and undefined", () => {
+ assertFalse(defined(undefined));
+ assertFalse(defined(null));
+ });
+});
+
+describe("instanciate", () => {
+ class MyClass {
+ constructor(public value: number) {}
+ }
+
+ it("creates a new instance with the given argument", () => {
+ const create = instanciate(MyClass);
+ const instance = create(10);
+ assert(instance instanceof MyClass);
+ assertEquals(instance.value, 10);
+ });
+});
+
+describe("get", () => {
+ it("returns the value at the specified key", () => {
+ const obj = { a: 123, b: "hello" };
+ const getA = get("a");
+ const getB = get("b");
+
+ assertEquals(getA(obj), 123);
+ assertEquals(getB(obj), "hello");
+ });
+});
+
+describe("getCall", () => {
+ it("returns the return value at the specified key", () => {
+ const obj = { a: () => "a", b: (c: unknown) => c };
+ const getA = getCall("a");
+ const getB = getCall("b", "d");
+
+ assertEquals(getA(obj), "a");
+ assertEquals(getB(obj), "d");
+ });
+});
+
+describe("pass", () => {
+ it("calls the given function and returns the input", () => {
+ let a: number | null = null;
+ const f = spy((x: number) => a = x);
+
+ const result = pass(f)(5);
+ assertSpyCalls(f, 1);
+ assertEquals(f.calls[0].args[0], 5);
+ assertEquals(result, 5);
+ assertEquals(a, 5);
+ });
+});
+
+describe("equal", () => {
+ it("returns true when primitive values are strictly equal", () => {
+ const isFive = equal(5);
+ assert(isFive(5));
+ assertFalse(isFive(6));
+
+ const isHello = equal("hello");
+ assert(isHello("hello"));
+ assertFalse(isHello("world"));
+ });
+
+ it("returns true only for same object reference", () => {
+ const obj = { a: 1 };
+ const isObj = equal(obj);
+ assert(isObj(obj));
+ assertFalse(isObj({ a: 1 }));
+ });
+
+ it("handles boolean values correctly", () => {
+ const isTrue = equal(TRUE);
+ assert(isTrue(TRUE));
+ assertFalse(isTrue(FALSE));
+ });
+});
+
+describe("extremeBy", () => {
+ it("returns the maximum value from projected numbers", () => {
+ const data = [1, 3, 2];
+ const result = extremeBy(data, "max");
+ assertEquals(result, 3);
+ });
+
+ it("returns the minimum value from projected numbers", () => {
+ const data = [10, 4, 7];
+ const result = extremeBy(data, "min");
+ assertEquals(result, 4);
+ });
+
+ it("returns -Infinity/Infinity for empty array", () => {
+ const data: number[] = [];
+ assertEquals(extremeBy(data, "max"), -Infinity);
+ assertEquals(extremeBy(data, "min"), Infinity);
+ });
+});
diff --git a/src/utils/anonymous.ts b/src/utils/anonymous.ts
new file mode 100644
index 0000000..ddd28bd
--- /dev/null
+++ b/src/utils/anonymous.ts
@@ -0,0 +1,25 @@
+export const identity = <T>(x: T): T => x;
+export const defined = <T>(x: T | undefined | null): x is T =>
+ x !== undefined && x !== null;
+export const instanciate = <T, A>(C: new (arg: A) => T): (arg: A) => T => {
+ return (arg: A): T => new C(arg);
+};
+export const get = <T extends Record<K, unknown>, K extends PropertyKey>(
+ key: K,
+): (obj: T) => T[K] =>
+(obj: T): T[K] => obj[key];
+export const getCall = <
+ T extends Record<K, (...args: unknown[]) => unknown>,
+ K extends PropertyKey,
+>(
+ key: K,
+ ...args: Parameters<T[K]>
+): (obj: T) => ReturnType<T[K]> =>
+(obj: T): ReturnType<T[K]> => obj[key](...args) as ReturnType<T[K]>;
+export const pass = <T>(fn: (x: T) => void): (x: T) => T => (x: T): T => {
+ fn(x);
+ return x;
+};
+export const equal = <T>(x: T): (y: T) => boolean => (y: T): boolean => x === y;
+export const extremeBy = (arr: number[], mode: "max" | "min"): number =>
+ Math[mode](...arr);
diff --git a/src/utils/bases.test.ts b/src/utils/bases.test.ts
new file mode 100644
index 0000000..9341b18
--- /dev/null
+++ b/src/utils/bases.test.ts
@@ -0,0 +1,32 @@
+import { assertEquals, assertThrows } from "@std/assert";
+import { describe, it } from "@std/testing/bdd";
+import { bufferToBase } from "./bases.ts";
+
+describe("bufferToBase", () => {
+ it("returns an empty string for an empty Uint8Array", () => {
+ assertEquals(bufferToBase(new Uint8Array([]), 16), "");
+ });
+
+ it("converts bytes to hexadecimal (base 16)", () => {
+ const input = new Uint8Array([0, 1, 15, 16, 255]);
+ const expected = "00010f10ff";
+ assertEquals(bufferToBase(input, 16), expected);
+ });
+
+ it("converts bytes to binary (base 2)", () => {
+ const input = new Uint8Array([255, 0, 1]);
+ const expected = "111111110000000000000001";
+ assertEquals(bufferToBase(input, 2), expected);
+ });
+
+ it("converts bytes to octal (base 8)", () => {
+ const input = new Uint8Array([8, 64, 255]);
+ const expected = "010100377";
+ assertEquals(bufferToBase(input, 8), expected);
+ });
+
+ it("throws on invalid base", () => {
+ assertThrows(() => bufferToBase(new Uint8Array([1, 2]), 1), RangeError);
+ assertThrows(() => bufferToBase(new Uint8Array([1, 2]), 37), RangeError);
+ });
+});
diff --git a/src/utils/bases.ts b/src/utils/bases.ts
new file mode 100644
index 0000000..a610d13
--- /dev/null
+++ b/src/utils/bases.ts
@@ -0,0 +1,11 @@
+export const bufferToBase = (buf: Uint8Array, base = 10): string => {
+ if (base < 2 || base > 36) {
+ throw new RangeError("Base must be between 2 and 36.");
+ }
+
+ const max = Math.ceil(8 / Math.log2(base)); // Math.log2(1 << 8) = 8
+
+ return Array.from(buf, (byte) => byte.toString(base).padStart(max, "0")).join(
+ "",
+ );
+};
diff --git a/src/utils/datetime.test.ts b/src/utils/datetime.test.ts
new file mode 100644
index 0000000..dd239b2
--- /dev/null
+++ b/src/utils/datetime.test.ts
@@ -0,0 +1,63 @@
+import { assertEquals, assertMatch } from "@std/assert";
+import { describe, it } from "@std/testing/bdd";
+import { toIso8601Full, toIso8601FullUTC } from "./datetime.ts";
+import { FakeTime } from "@std/testing/time";
+
+describe("toIso8601Full", () => {
+ it("formats current local time with offset", () => {
+ const date = new Date();
+ const result = toIso8601Full(date);
+
+ assertMatch(
+ result,
+ /^[+-]\d{6}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})$/,
+ );
+ });
+
+ it("handles dates before year 0 (BC)", () => {
+ const date = new Date(-2000, 0, 1, 0, 0, 0, 0);
+ const result = toIso8601Full(date);
+
+ assertMatch(result, /^-\d{6}-01-01T00:00:00\.000(Z|[+-]\d{2}:\d{2})$/);
+ });
+
+ it("pads components correctly", () => {
+ const date = new Date(7, 0, 2, 3, 4, 5, 6);
+ const result = toIso8601Full(date);
+
+ assertMatch(result, /^\+001907-01-02T03:04:05\.006(Z|[+-]\d{2}:\d{2})$/);
+ });
+
+ it("handles positive and negative timezone offsets", () => {
+ const date = new Date("2025-06-17T12:00:00Z");
+ const result = toIso8601Full(date);
+
+ assertMatch(
+ result,
+ /^[+-]\d{6}-06-17T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})$/,
+ );
+ });
+});
+
+describe("toIso8601FullUTC", () => {
+ it("always formats in UTC with 'Z'", () => {
+ const date = new Date(Date.UTC(2025, 11, 31, 23, 59, 59, 999));
+ const result = toIso8601FullUTC(date);
+
+ assertEquals(result, "+002025-12-31T23:59:59.999Z");
+ });
+
+ it("pads milliseconds and components correctly", () => {
+ const date = new Date(Date.UTC(7, 0, 2, 3, 4, 5, 6));
+ const result = toIso8601FullUTC(date);
+
+ assertEquals(result, "+001907-01-02T03:04:05.006Z");
+ });
+
+ it("handles BC dates (negative years)", () => {
+ const date = new Date(Date.UTC(-44, 2, 15, 12, 0, 0, 0));
+ const result = toIso8601FullUTC(date);
+
+ assertMatch(result, /^-\d{6}-03-15T12:00:00\.000Z$/);
+ });
+});
diff --git a/src/utils/datetime.ts b/src/utils/datetime.ts
new file mode 100644
index 0000000..3a2cd25
--- /dev/null
+++ b/src/utils/datetime.ts
@@ -0,0 +1,43 @@
+export function toIso8601Full(date: Date): string {
+ const yearN = date.getFullYear();
+ const isNegativeYear = yearN <= 0;
+ const year = isNegativeYear ? pad(1 - yearN, 6) : pad(yearN, 6);
+ const signedYear = (isNegativeYear ? "-" : "+") + year;
+
+ const month = pad(date.getMonth() + 1);
+ const day = pad(date.getDate());
+ const hour = pad(date.getHours());
+ const minute = pad(date.getMinutes());
+ const second = pad(date.getSeconds());
+ const ms = pad(date.getMilliseconds(), 3);
+
+ const dateString =
+ `${signedYear}-${month}-${day}T${hour}:${minute}:${second}.${ms}`;
+ const tzOffset = -date.getTimezoneOffset();
+ if (tzOffset === 0) {
+ return `${dateString}Z`;
+ } else {
+ const offsetSign = tzOffset > 0 ? "+" : "-";
+ const offsetHours = pad(Math.floor(Math.abs(tzOffset) / 60));
+ const offsetMinutes = pad(Math.abs(tzOffset) % 60);
+ return `${dateString}${offsetSign}${offsetHours}:${offsetMinutes}`;
+ }
+}
+
+export function toIso8601FullUTC(date: Date): string {
+ const yearN = date.getUTCFullYear();
+ const isNegativeYear = yearN <= 0;
+ const year = isNegativeYear ? pad(1 - yearN, 6) : pad(yearN, 6);
+ const signedYear = (isNegativeYear ? "-" : "+") + year;
+
+ const month = pad(date.getUTCMonth() + 1);
+ const day = pad(date.getUTCDate());
+ const hour = pad(date.getUTCHours());
+ const minute = pad(date.getUTCMinutes());
+ const second = pad(date.getUTCSeconds());
+ const ms = pad(date.getUTCMilliseconds(), 3);
+
+ return `${signedYear}-${month}-${day}T${hour}:${minute}:${second}.${ms}Z`;
+}
+
+const pad = (num: number, len = 2) => String(Math.abs(num)).padStart(len, "0");
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 0000000..5a083d5
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,19 @@
+import { trailingSlash } from "astro:config/client";
+
+export function addForwardSlash(path: string): string {
+ if (trailingSlash === "always") {
+ return path.endsWith("/") ? path : path + "/";
+ } else {
+ return path;
+ }
+}
+
+export const enum Level {
+ OK,
+ INFO,
+ WARN,
+ DEBUG,
+ ERROR,
+}
+
+export type MaybePromise<T> = Promise<T> | T;
diff --git a/src/utils/iterator.test.ts b/src/utils/iterator.test.ts
new file mode 100644
index 0000000..dda0e0a
--- /dev/null
+++ b/src/utils/iterator.test.ts
@@ -0,0 +1,122 @@
+import { describe, it } from "@std/testing/bdd";
+import {
+ createAsyncIterator,
+ filterDuplicate,
+ findMapAsync,
+ surelyIterable,
+} from "./iterator.ts";
+import { assertEquals } from "@std/assert";
+
+describe("surelyIterable", () => {
+ it("returns the iterable as-is if input is already iterable", () => {
+ const input = [1, 2, 3];
+ const result = surelyIterable(input);
+ assertEquals([...result], [1, 2, 3]);
+ });
+
+ it("wraps a non-iterable value in an array", () => {
+ const input = 42;
+ const result = surelyIterable(input);
+ assertEquals([...result], [42]);
+ });
+
+ it("wraps null in an array", () => {
+ const input = null;
+ const result = surelyIterable(input);
+ assertEquals([...result], [null]);
+ });
+
+ it("wraps undefined in an array", () => {
+ const input = undefined;
+ const result = surelyIterable(input);
+ assertEquals([...result], [undefined]);
+ });
+
+ it("wraps an object that is not iterable", () => {
+ const input = { a: 1 };
+ const result = surelyIterable(input);
+ assertEquals([...result], [{ a: 1 }]);
+ });
+
+ it("handles a Set correctly", () => {
+ const input = new Set([1, 2, 3]);
+ const result = surelyIterable(input);
+ assertEquals([...result], [1, 2, 3]);
+ });
+});
+
+describe("createAsyncIterator", () => {
+ it("yields resolved values in order", async () => {
+ const values = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)];
+ const results: number[] = [];
+ for await (const value of createAsyncIterator(values)) {
+ results.push(value);
+ }
+ assertEquals(results, [1, 2, 3]);
+ });
+
+ it("handles empty array", async () => {
+ const results: unknown[] = [];
+ for await (const value of createAsyncIterator([])) {
+ results.push(value);
+ }
+ assertEquals(results, []);
+ });
+});
+
+describe("filterDuplicate", () => {
+ it("filters duplicate objects by key", () => {
+ const items = [
+ { id: 1, name: "a" },
+ { id: 2, name: "b" },
+ { id: 1, name: "c" },
+ ];
+ const result = filterDuplicate(items, (i) => i.id);
+ assertEquals(result.length, 2);
+ assertEquals(result[0].name, "a");
+ assertEquals(result[1].name, "b");
+ });
+
+ it("handles empty iterable", () => {
+ const result = filterDuplicate([], (x) => x);
+ assertEquals(result, []);
+ });
+
+ it("keeps first occurrence only", () => {
+ const input = [1, 2, 3, 1, 2, 4];
+ const result = filterDuplicate(input, (x) => x);
+ assertEquals(result, [1, 2, 3, 4]);
+ });
+});
+
+describe("findMapAsync", () => {
+ it("returns first successful result", async () => {
+ const arr = [1, 2, 3];
+ const i = 2;
+ const result = await findMapAsync(arr, (x) => {
+ if (x === i) return Promise.resolve(x);
+ throw new Error("not found");
+ });
+ assertEquals(result, i);
+ });
+
+ it("returns undefined if all reject", async () => {
+ const arr = [1, 2];
+ const result = await findMapAsync(arr, () => {
+ throw new Error("fail");
+ });
+ assertEquals(result, undefined);
+ });
+
+ it("short-circuits after first success", async () => {
+ const calls: number[] = [];
+ const arr = [1, 2, 3];
+ const i = arr.length - 1;
+ await findMapAsync(arr, (x) => {
+ calls.push(x);
+ if (x === i) return Promise.resolve("ok");
+ throw new Error("fail");
+ });
+ assertEquals(calls, arr.slice(0, i));
+ });
+});
diff --git a/src/utils/iterator.ts b/src/utils/iterator.ts
new file mode 100644
index 0000000..fa58fc9
--- /dev/null
+++ b/src/utils/iterator.ts
@@ -0,0 +1,52 @@
+export type MaybeIterable<T> = T | Iterable<T>;
+export type NonEmptyArray<T> = [T, ...T[]];
+export type AsyncYieldType<T> = T extends AsyncGenerator<infer U> ? U : never;
+
+export function surelyIterable<T>(maybe: MaybeIterable<T>): Iterable<T> {
+ return typeof maybe === "object" && maybe !== null && Symbol.iterator in maybe
+ ? maybe
+ : [maybe];
+}
+
+export async function* createAsyncIterator<T>(
+ promises: Promise<T>[],
+): AsyncGenerator<T, void, void> {
+ for (const promise of promises) {
+ yield promise;
+ }
+}
+
+export function filterDuplicate<T, K>(
+ array: Iterable<T>,
+ key: (i: T) => K,
+): T[] {
+ const seen = new Map<K, T>();
+ for (const i of array) {
+ const id = key(i);
+ if (!seen.has(id)) {
+ seen.set(id, i);
+ }
+ }
+ return Array.from(seen.values());
+}
+
+export async function findMapAsync<T, R>(
+ iter: Iterable<T>,
+ predicate: (value: T) => Promise<R>,
+): Promise<R | undefined> {
+ const arr = Array.from(iter);
+
+ async function tryNext(index: number): Promise<R | undefined> {
+ if (index >= arr.length) {
+ return await Promise.resolve(undefined);
+ }
+
+ try {
+ return await predicate(arr[index]);
+ } catch {
+ return tryNext(index + 1);
+ }
+ }
+
+ return await tryNext(0);
+}
diff --git a/src/utils/lang.test.ts b/src/utils/lang.test.ts
new file mode 100644
index 0000000..eac5948
--- /dev/null
+++ b/src/utils/lang.test.ts
@@ -0,0 +1,97 @@
+import { assert, assertEquals, assertFalse } from "@std/assert";
+import { describe, it } from "@std/testing/bdd";
+import {
+ getFlagEmojiFromLocale,
+ getLanguageNameFromLocale,
+ isValidLocale,
+ LANGUAGE_DEFAULTS,
+} from "./lang.ts";
+
+describe("getFlagEmojiFromLocale", () => {
+ it("returns 🇺🇸 for 'en-US'", () => {
+ assertEquals(getFlagEmojiFromLocale("en-US"), "🇺🇸");
+ });
+
+ it("returns 🇧🇷 for 'pt-BR'", () => {
+ assertEquals(getFlagEmojiFromLocale("pt-BR"), "🇧🇷");
+ });
+
+ it("returns 🇫🇷 for 'fr-FR'", () => {
+ assertEquals(getFlagEmojiFromLocale("fr-FR"), "🇫🇷");
+ });
+
+ it("uses fallback country from LANGUAGE_DEFAULTS when no region", () => {
+ for (const i in LANGUAGE_DEFAULTS) {
+ if (i in LANGUAGE_DEFAULTS) {
+ assertEquals(
+ getFlagEmojiFromLocale(i),
+ getFlagEmojiFromLocale(
+ `${i}-${LANGUAGE_DEFAULTS[i as keyof typeof LANGUAGE_DEFAULTS]}`,
+ ),
+ );
+ }
+ }
+ });
+
+ it("returns empty string for unsupported languages", () => {
+ assertEquals(getFlagEmojiFromLocale("xx"), "");
+ assertEquals(getFlagEmojiFromLocale("de"), "");
+ });
+
+ it("is case-insensitive", () => {
+ assertEquals(getFlagEmojiFromLocale("EN-us"), "🇺🇸");
+ assertEquals(getFlagEmojiFromLocale("Pt"), "🇵🇹");
+ });
+});
+
+describe("getLanguageNameFromLocale", () => {
+ it("returns 'English' for 'en'", () => {
+ const result = getLanguageNameFromLocale("en");
+ assertEquals(typeof result, "string");
+ assert(result.length > 0);
+ });
+
+ it("returns '' for invalid locale", () => {
+ assertEquals(getLanguageNameFromLocale(new Date().toLocaleString()), "");
+ });
+
+ it("returns name in the correct locale", () => {
+ const fr = getLanguageNameFromLocale("fr");
+ const pt = getLanguageNameFromLocale("pt");
+
+ assertEquals(typeof fr, "string");
+ assertEquals(typeof pt, "string");
+ assert(fr.length > 0);
+ assert(pt.length > 0);
+ });
+});
+
+describe("isValidLocale", () => {
+ it("returns true for valid simple language tags", () => {
+ assert(isValidLocale("en"));
+ assert(isValidLocale("fr"));
+ assert(isValidLocale("pt"));
+ });
+
+ it("returns true for valid language-region tags", () => {
+ assert(isValidLocale("en-US"));
+ assert(isValidLocale("pt-BR"));
+ assert(isValidLocale("fr-FR"));
+ });
+
+ it("returns true for valid locale with script", () => {
+ assert(isValidLocale("zh-Hant"));
+ assert(isValidLocale("sr-Cyrl"));
+ });
+
+ it("returns false for invalid formats", () => {
+ assertFalse(isValidLocale("EN_us"));
+ assertFalse(isValidLocale("xx-YY-ZZ"));
+ assertFalse(isValidLocale("123"));
+ assertFalse(isValidLocale(""));
+ });
+
+ it("is case-insensitive and accepts well-formed mixed cases", () => {
+ assert(isValidLocale("eN-uS"));
+ });
+});
diff --git a/src/utils/lang.ts b/src/utils/lang.ts
new file mode 100644
index 0000000..2ce8fe4
--- /dev/null
+++ b/src/utils/lang.ts
@@ -0,0 +1,56 @@
+export const LANGUAGE_DEFAULTS = Object.freeze({
+ pt: "PT",
+ en: "GB",
+ fr: "FR",
+});
+
+/**
+ * AI thought me this.
+ *
+ * Explanation:
+ * * Each letter in a 2-letter country code is converted to a Regional
+ * Indicator Symbol, which together form the emoji flag.
+ * * 'A'.charCodeAt(0) is 65, and '🇦' starts at 0x1F1E6 → offset of 127397
+ * (0x1F1A5).
+ * * So 'A' → '🇦', 'B' → '🇧', etc.
+ *
+ * The flags are the combination of those emojis making the country code like
+ * Portugal -> PT.
+ */
+export function getFlagEmojiFromLocale(locale: string): string {
+ let countryCode: string | undefined;
+
+ const parts = locale.split("-");
+ const lang = parts[0].toLowerCase();
+ if (parts.length === 2) {
+ countryCode = parts[1].toUpperCase();
+ } else if (lang in LANGUAGE_DEFAULTS) {
+ countryCode = LANGUAGE_DEFAULTS[lang as keyof typeof LANGUAGE_DEFAULTS];
+ }
+
+ if (!countryCode) return "";
+
+ return [...countryCode]
+ .map((c) => String.fromCodePoint(c.charCodeAt(0) + 127397))
+ .join("");
+}
+
+export function getLanguageNameFromLocale(locale: string): string {
+ try {
+ return new Intl.DisplayNames([locale], {
+ type: "language",
+ fallback: "code",
+ }).of(locale) ?? "";
+ } catch {
+ return "";
+ }
+}
+
+export function isValidLocale(locale: string): boolean {
+ try {
+ Intl.getCanonicalLocales(locale);
+ return true;
+ } catch {
+ return false;
+ }
+}