diff --git a/context.ts b/context.ts index 6b88679..adb6136 100644 --- a/context.ts +++ b/context.ts @@ -11,7 +11,7 @@ export function getLangHref(lang: Lang, url: string): string { } export function getLangUrlPrefix(lang: Lang): string { - return lang === Lang.Rus ? "" : `/${lang}`; + return `/${lang}`; } export function iterLangs(): Lang[] { diff --git a/server.ts b/server.ts index 9bc2a95..85fe2ed 100644 --- a/server.ts +++ b/server.ts @@ -3,7 +3,7 @@ import { HtmlStrRenderer } from "ren/html_str.ts"; import * as log from "./log.ts"; import rusTranslates from "./translates/rus.ts"; import type { Translations } from "./translates/rus.ts"; -import { Context, getLangHref, Lang } from "./context.ts"; +import { Context, getLangHref, getLangUrlPrefix, Lang } from "./context.ts"; import { E404Page } from "./views/pages/e404.ts"; import { E500Page } from "./views/pages/e500.ts"; import { WorksPage } from "./views/pages/works.ts"; @@ -51,16 +51,27 @@ async function handleRequest(req: Request): Promise { } async function handleGet(req: Request) { - const ctx = createContextFromRequest(req); + const restCtx = createRestContextFromRequest(req); try { - const res = await tryCreateFileResponse(ctx.locPath); + const res = await tryCreateFileResponse(restCtx.locPath); return res; } catch (_) { - if (ctx.lang !== Lang.Rus) { + if (restCtx.lang == null && restCtx.newLang) { + return new Response(null, { + status: 301, + headers: { + location: getLangUrlPrefix(restCtx.newLang) + restCtx.locPath, + }, + }); + } + + const ctx = intoAppContext(restCtx); + + if (restCtx.lang !== Lang.Rus) { await loadAndUpdateTranslations(ctx); } - log.debug({ context: ctx }); + log.debug({ context: restCtx }); const par = new MarkdownParser(); const ren = new HtmlStrRenderer({ @@ -75,10 +86,12 @@ async function handleGet(req: Request) { }); try { - if (ctx.locPath === "/" || ctx.locPath === "/about") { - const res = par.parse(await readMarkdownFile("data/about", ctx.lang)); + if (restCtx.locPath === "/" || restCtx.locPath === "/about") { + const res = par.parse( + await readMarkdownFile("data/about", ctx.lang), + ); return createHtmlResponse(ren.render(ContentPage(ctx, res))); - } else if (ctx.locPath === "/works") { + } else if (restCtx.locPath === "/works") { return createHtmlResponse(ren.render(WorksPage(ctx))); } else { return createHtmlResponse(ren.render(E404Page(ctx)), 404); @@ -112,19 +125,63 @@ async function loadAndUpdateTranslations(ctx: Context) { } } -function createContextFromRequest(req: Request): Context { +function intoAppContext(restCtx: RestContext): Context { + return { + locPath: restCtx.locPath, + lang: restCtx.lang || Lang.Rus, + tr: restCtx.tr, + }; +} + +function createRestContextFromRequest(req: Request): RestContext { + log.debug(req.headers); + const locUrl = new URL(req.url); - const lang = langFromUrl(locUrl); + const lang = tryIntoAppLangFromUrl(locUrl); return { lang, + newLang: getPreferRequestLang(req.headers) ?? Lang.Rus, locPath: stripPrefix(`/${lang}`, locUrl.pathname), tr: rusTranslates, }; } -function langFromUrl(url: URL): Lang { - return url.pathname.startsWith("/eng/") ? Lang.Eng : Lang.Rus; +interface RestContext { + locPath: string; + lang: Lang | null; + newLang: Lang; + tr: Translations; +} + +function getPreferRequestLang(headers: Headers): Lang | null { + const acceptLanguageHeader = headers.get("accept-language"); + if (!acceptLanguageHeader) return null; + + const acceptLanguages = acceptLanguageHeader + .split(/,\s*/) + .map((part) => part.split(";q=")[0]) + .map(tryIntoAppLangFromAcceptLangCode) + .filter((lang): lang is Lang => !!lang); + return acceptLanguages[0] ?? null; +} + +function tryIntoAppLangFromAcceptLangCode(lang: string): Lang | null { + return lang === "*" + ? Lang.Rus + : lang.startsWith("en") + ? Lang.Eng + : lang.startsWith("ru") + ? Lang.Rus + : null; +} + +function tryIntoAppLangFromUrl(url: URL): Lang | null { + return url.pathname.startsWith("/eng/") + ? Lang.Eng + : url.pathname.startsWith("/rus/") + ? Lang.Rus + : null; } function stripPrefix(prefix: string, val: string): string {