From b91855779133fb8b2449379c6ce8c70139ae0196 Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sat, 28 May 2022 23:28:07 +0300 Subject: [PATCH] add translates --- web/comp/layout.ts | 5 +-- web/comp/page_layout.ts | 29 ++++++++++------ web/context.ts | 9 +++++ web/import_map.json | 3 +- web/repo/ingredient/rest.ts | 8 +++-- web/repo/ingredient/types.ts | 3 +- web/server.ts | 65 +++++++++++++++++++++++++++++++++--- web/translates/eng.ts | 9 +++++ web/translates/rus.ts | 11 ++++++ web/views/e404.ts | 6 ++-- web/views/home.ts | 2 +- web/views/ingredients.ts | 2 +- web/views/recipes.ts | 2 +- 13 files changed, 127 insertions(+), 27 deletions(-) create mode 100644 web/translates/eng.ts create mode 100644 web/translates/rus.ts diff --git a/web/comp/layout.ts b/web/comp/layout.ts index 72838fb..d50e1c0 100644 --- a/web/comp/layout.ts +++ b/web/comp/layout.ts @@ -1,7 +1,8 @@ import { AnyNode, E } from "ren/node.ts"; +import { Context } from "../context.ts"; -export function Layout(page: AnyNode): AnyNode { - return E("html", { lang: "ru" }, [ +export function Layout(ctx: Context, page: AnyNode): AnyNode { + return E("html", { lang: ctx.lang }, [ E("head", [], [ E("meta", { charset: "utf-8" }), E("meta", { diff --git a/web/comp/page_layout.ts b/web/comp/page_layout.ts index 2f35f58..082aaa7 100644 --- a/web/comp/page_layout.ts +++ b/web/comp/page_layout.ts @@ -1,12 +1,12 @@ import { AnyNode, Attrs, E, Elem } from "ren/node.ts"; import { classNames } from "ren/attrs.ts"; -import { Context } from "../context.ts"; +import { Context, Lang } from "../context.ts"; export function PageLayout(ctx: Context, children: AnyNode[]): Elem { return E("div", { id: "main" }, [ Header(ctx), E("div", classNames("content"), children), - Footer(), + Footer(ctx), ]); } @@ -18,25 +18,29 @@ export function Header(ctx: Context): AnyNode { export function HeaderNav(ctx: Context): AnyNode { return E("nav", classNames("main-menu"), [ - Link("Главная", navLink("/", ctx)), - Link("Рецепты", navLink("/recipes", ctx)), - Link("Ингредиенты", navLink("/ingredients", ctx)), + Link(ctx.tr.Home, navLink("/", ctx)), + Link(ctx.tr.Recipes, navLink("/recipes", ctx)), + Link(ctx.tr.Ingredients, navLink("/ingredients", ctx)), ]); } -function navLink(href: string, ctx?: Context): Attrs { - const attrs: Attrs = { href }; - if (ctx?.locPath === href) attrs["aria-current"] = "true"; +function navLink(lhref: string, ctx?: Context): Attrs { + const attrs: Attrs = { lhref }; + if (ctx?.locPath === lhref) attrs["aria-current"] = "true"; return attrs; } -export function Footer(): AnyNode { +export function Footer(ctx: Context): AnyNode { return E("footer", classNames("footer gap-v-1x5"), [ E("div", classNames("content-width"), [ - Link("Исходный код", { + Link(ctx.tr.Source_code, { href: "https://notabug.org/pleshevskiy/recipes", rel: "external nofollow noopener noreferrer", }), + E("div", [], [ + ChangeLangBtn(ctx, Lang.Rus), + ChangeLangBtn(ctx, Lang.Eng), + ]), ]), ]); } @@ -44,3 +48,8 @@ export function Footer(): AnyNode { export function Link(text: string, attrs: Attrs | Attrs[]): AnyNode { return E("a", attrs, text); } + +export function ChangeLangBtn(ctx: Context, lang: Lang): AnyNode { + const prefix = lang === Lang.Rus ? "" : `/${lang}`; + return E("a", { "href": prefix + ctx.locPath }, lang); +} diff --git a/web/context.ts b/web/context.ts index 96a571b..bdec1ce 100644 --- a/web/context.ts +++ b/web/context.ts @@ -1,3 +1,12 @@ +import { Translations } from "./translates/rus.ts"; + export interface Context { locPath: string; + lang: Lang; + tr: Translations; +} + +export enum Lang { + Rus = "rus", + Eng = "eng", } diff --git a/web/import_map.json b/web/import_map.json index e47e129..9efda84 100644 --- a/web/import_map.json +++ b/web/import_map.json @@ -1,5 +1,6 @@ { "imports": { - "ren/": "https://notabug.org/pleshevskiy/ren/raw/v2/ren/" + "ren/": "https://notabug.org/pleshevskiy/ren/raw/v2/ren/", + "ren/": "../../ren/ren/" } } diff --git a/web/repo/ingredient/rest.ts b/web/repo/ingredient/rest.ts index 1c8fa15..9a9b590 100644 --- a/web/repo/ingredient/rest.ts +++ b/web/repo/ingredient/rest.ts @@ -1,11 +1,15 @@ +import { Lang } from "../../context.ts"; import { Ingredient } from "../../domain/ingredient/types.ts"; import { RestLang } from "../misc_types.ts"; import { IngredientRepo } from "./types.ts"; export class RestIngredientRepo implements IngredientRepo { - async fetchIngredients(): Promise { + async fetchIngredients(lang: Lang): Promise { + const url = new URL("http://localhost:33333/api/ingredients"); + url.searchParams.set("lang", lang); + const res = await fetch( - "http://localhost:33333/api/ingredients", + url.toString(), { headers: { "content-type": "application/json" } }, ); diff --git a/web/repo/ingredient/types.ts b/web/repo/ingredient/types.ts index 3907757..e691f78 100644 --- a/web/repo/ingredient/types.ts +++ b/web/repo/ingredient/types.ts @@ -1,5 +1,6 @@ +import { Lang } from "../../context.ts"; import { Ingredient } from "../../domain/ingredient/types.ts"; export interface IngredientRepo { - fetchIngredients(): Promise; + fetchIngredients(lang: Lang): Promise; } diff --git a/web/server.ts b/web/server.ts index dcd4eea..2367be4 100644 --- a/web/server.ts +++ b/web/server.ts @@ -1,12 +1,14 @@ import { StrRenderer } from "ren/str.ts"; import { Layout } from "./comp/layout.ts"; -import { Context } from "./context.ts"; +import { Context, Lang } from "./context.ts"; import { E404Page } from "./views/e404.ts"; import * as log from "./log.ts"; import { HomePage } from "./views/home.ts"; import { RecipesPage } from "./views/recipes.ts"; import { IngredientsPage } from "./views/ingredients.ts"; import { RestIngredientRepo } from "./repo/ingredient/rest.ts"; +import rusTranslates from "./translates/rus.ts"; +import type { Translations } from "./translates/rus.ts"; if (import.meta.main) { await main(); @@ -39,14 +41,40 @@ async function serveHttp(conn: Deno.Conn) { } async function handleRequest(req: Request): Promise { - log.debug({ url: req.url }); + log.info({ method: req.method, url: req.url }); + + if (req.method === "GET") { + return await handleGet(req); + } else { + return new Response("Method Not Allowed", { status: 405 }); + } +} + +async function handleGet(req: Request) { const ctx = createContextFromRequest(req); try { const res = await tryCreateFileResponse(ctx.locPath); return res; } catch (_) { - const ren = new StrRenderer({ wrapNode: Layout }); + if (ctx.lang !== Lang.Rus) { + await loadAndUpdateTranslations(ctx); + } + log.debug({ context: ctx }); + + const ren = new StrRenderer({ + wrapNode: Layout.bind(null, ctx), + onVisitAttr: ([key, value]) => { + if (key === "lhref") { + const prefix = ctx.lang === Lang.Rus || !value.startsWith("/") + ? "" + : `/${ctx.lang}`; + return ["href", prefix + value]; + } else { + return [key, value]; + } + }, + }); if (ctx.locPath === "/") { return createHtmlResponse(ren.render(HomePage(ctx))); @@ -54,7 +82,7 @@ async function handleRequest(req: Request): Promise { return createHtmlResponse(ren.render(RecipesPage(ctx))); } else if (ctx.locPath === "/ingredients") { const repo = new RestIngredientRepo(); - const ingredients = await repo.fetchIngredients(); + const ingredients = await repo.fetchIngredients(ctx.lang); return createHtmlResponse( ren.render(IngredientsPage(ctx, { ingredients })), @@ -65,12 +93,39 @@ async function handleRequest(req: Request): Promise { } } +async function loadAndUpdateTranslations(ctx: Context) { + try { + const translates = await import(`./translates/${ctx.lang}.ts`); + ctx.tr = Object.entries(translates.default as Partial) + .reduce( + (acc, [key, val]) => ({ + ...acc, + [key as keyof Translations]: val, + }), + { ...ctx.tr } as Translations, + ); + } catch (_e) { /* ignore */ } +} + function createContextFromRequest(req: Request): Context { + const locUrl = new URL(req.url); + const lang = langFromUrl(locUrl); + return { - locPath: new URL(req.url).pathname, + lang, + locPath: stripPrefix(`/${lang}`, locUrl.pathname), + tr: rusTranslates, }; } +function langFromUrl(url: URL): Lang { + return url.pathname.startsWith("/eng/") ? Lang.Eng : Lang.Rus; +} + +function stripPrefix(prefix: string, val: string): string { + return val.startsWith(prefix) ? val.slice(prefix.length) : val; +} + function createHtmlResponse(body: string, status = 200): Response { return new Response(body, { status, diff --git a/web/translates/eng.ts b/web/translates/eng.ts new file mode 100644 index 0000000..35cde34 --- /dev/null +++ b/web/translates/eng.ts @@ -0,0 +1,9 @@ +import { Translations } from "./rus.ts"; + +export default { + Home: "Home", + Recipes: "Recipes", + Ingredients: "Ingredients", + Source_code: "Source code", + Page_not_found: "Page not found", +} as Translations; diff --git a/web/translates/rus.ts b/web/translates/rus.ts new file mode 100644 index 0000000..162e430 --- /dev/null +++ b/web/translates/rus.ts @@ -0,0 +1,11 @@ +export const rus = { + Home: "Главная", + Recipes: "Рецепты", + Ingredients: "Ингредиенты", + Source_code: "Исходный код", + Page_not_found: "Страница не найдена", +}; + +export default rus; + +export type Translations = typeof rus; diff --git a/web/views/e404.ts b/web/views/e404.ts index 94193f9..03f9bdd 100644 --- a/web/views/e404.ts +++ b/web/views/e404.ts @@ -5,11 +5,11 @@ import { Context } from "../context.ts"; import { H3 } from "../uikit/typo.ts"; export function E404Page(ctx: Context): AnyNode { - return PageLayout(ctx, [E404()]); + return PageLayout(ctx, [E404(ctx)]); } -export function E404(): AnyNode { +export function E404(ctx: Context): AnyNode { return E("div", classNames("content-width"), [ - H3("Страница не найдена"), + H3(ctx.tr.Page_not_found), ]); } diff --git a/web/views/home.ts b/web/views/home.ts index c36e917..94f71ce 100644 --- a/web/views/home.ts +++ b/web/views/home.ts @@ -7,7 +7,7 @@ import { H3 } from "../uikit/typo.ts"; export function HomePage(ctx: Context): AnyNode { return PageLayout(ctx, [ E("div", classNames("content-width"), [ - H3("Главная"), + H3(ctx.tr.Home), ]), ]); } diff --git a/web/views/ingredients.ts b/web/views/ingredients.ts index a27971e..3be5ed0 100644 --- a/web/views/ingredients.ts +++ b/web/views/ingredients.ts @@ -15,7 +15,7 @@ export function IngredientsPage( ): AnyNode { return PageLayout(ctx, [ E("div", classNames("content-width"), [ - H3("Ингредиенты"), + H3(ctx.tr.Ingredients), IngredientList(data.ingredients), ]), ]); diff --git a/web/views/recipes.ts b/web/views/recipes.ts index 50cb9f4..a243c45 100644 --- a/web/views/recipes.ts +++ b/web/views/recipes.ts @@ -7,7 +7,7 @@ import { H3 } from "../uikit/typo.ts"; export function RecipesPage(ctx: Context): AnyNode { return PageLayout(ctx, [ E("div", classNames("content-width"), [ - H3("Рецепты"), + H3(ctx.tr.Recipes), ]), ]); }