Add locale resolver with Language type and URL prefix parsing

This commit is contained in:
2026-04-14 23:17:08 +03:00
parent 33d4c94298
commit bf3087d45e
2 changed files with 107 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
import { describe, expect, it } from "vitest";
import {
LANGUAGES,
isLanguage,
resolveLocaleFromPath,
stripLocaleFromPath,
} from "./resolver.js";
describe("LANGUAGES", () => {
it("contains exactly 9 supported languages", () => {
expect(LANGUAGES).toHaveLength(9);
expect(LANGUAGES).toContain("ru");
expect(LANGUAGES).toContain("en");
expect(LANGUAGES).toContain("de");
});
});
describe("isLanguage", () => {
it("returns true for valid languages", () => {
expect(isLanguage("ru")).toBe(true);
expect(isLanguage("en")).toBe(true);
expect(isLanguage("zh")).toBe(true);
});
it("returns false for invalid strings", () => {
expect(isLanguage("xx")).toBe(false);
expect(isLanguage("")).toBe(false);
expect(isLanguage("RU")).toBe(false);
expect(isLanguage("russian")).toBe(false);
});
});
describe("resolveLocaleFromPath", () => {
it("extracts locale from the first path segment", () => {
expect(resolveLocaleFromPath("/ru/onlineboard")).toBe("ru");
expect(resolveLocaleFromPath("/en/onlineboard/flight/SU100")).toBe("en");
expect(resolveLocaleFromPath("/de/schedule")).toBe("de");
});
it("returns null for paths without a valid locale prefix", () => {
expect(resolveLocaleFromPath("/onlineboard")).toBeNull();
expect(resolveLocaleFromPath("/xx/onlineboard")).toBeNull();
expect(resolveLocaleFromPath("/")).toBeNull();
expect(resolveLocaleFromPath("")).toBeNull();
});
it("handles bare locale path (e.g., /ru)", () => {
expect(resolveLocaleFromPath("/ru")).toBe("ru");
expect(resolveLocaleFromPath("/ru/")).toBe("ru");
});
});
describe("stripLocaleFromPath", () => {
it("strips locale and returns the rest", () => {
expect(stripLocaleFromPath("/ru/onlineboard")).toEqual({
locale: "ru",
rest: "/onlineboard",
});
expect(stripLocaleFromPath("/en/onlineboard/flight/SU100")).toEqual({
locale: "en",
rest: "/onlineboard/flight/SU100",
});
});
it("returns / as rest for bare locale path", () => {
expect(stripLocaleFromPath("/ru")).toEqual({ locale: "ru", rest: "/" });
expect(stripLocaleFromPath("/ru/")).toEqual({ locale: "ru", rest: "/" });
});
it("returns null for paths without a valid locale prefix", () => {
expect(stripLocaleFromPath("/onlineboard")).toBeNull();
expect(stripLocaleFromPath("/xx/schedule")).toBeNull();
});
});
+33
View File
@@ -0,0 +1,33 @@
export type Language = "ru" | "en" | "es" | "fr" | "it" | "ja" | "ko" | "zh" | "de";
export const LANGUAGES: readonly Language[] = [
"ru", "en", "es", "fr", "it", "ja", "ko", "zh", "de",
] as const;
const languageSet: ReadonlySet<string> = new Set(LANGUAGES);
export function isLanguage(x: string): x is Language {
return languageSet.has(x);
}
export function resolveLocaleFromPath(pathname: string): Language | null {
const segments = pathname.split("/").filter(Boolean);
const first = segments[0];
if (first !== undefined && isLanguage(first)) {
return first;
}
return null;
}
export function stripLocaleFromPath(
pathname: string,
): { locale: Language; rest: string } | null {
const locale = resolveLocaleFromPath(pathname);
if (locale === null) return null;
const rest = pathname.slice(`/${locale}`.length);
return {
locale,
rest: rest === "" || rest === "/" ? "/" : rest,
};
}