add translates

This commit is contained in:
Dmitriy Pleshevskiy 2022-05-28 23:28:07 +03:00
parent a6353699df
commit b918557791
13 changed files with 127 additions and 27 deletions

View File

@ -1,7 +1,8 @@
import { AnyNode, E } from "ren/node.ts"; import { AnyNode, E } from "ren/node.ts";
import { Context } from "../context.ts";
export function Layout(page: AnyNode): AnyNode { export function Layout(ctx: Context, page: AnyNode): AnyNode {
return E("html", { lang: "ru" }, [ return E("html", { lang: ctx.lang }, [
E("head", [], [ E("head", [], [
E("meta", { charset: "utf-8" }), E("meta", { charset: "utf-8" }),
E("meta", { E("meta", {

View File

@ -1,12 +1,12 @@
import { AnyNode, Attrs, E, Elem } from "ren/node.ts"; import { AnyNode, Attrs, E, Elem } from "ren/node.ts";
import { classNames } from "ren/attrs.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 { export function PageLayout(ctx: Context, children: AnyNode[]): Elem {
return E("div", { id: "main" }, [ return E("div", { id: "main" }, [
Header(ctx), Header(ctx),
E("div", classNames("content"), children), E("div", classNames("content"), children),
Footer(), Footer(ctx),
]); ]);
} }
@ -18,25 +18,29 @@ export function Header(ctx: Context): AnyNode {
export function HeaderNav(ctx: Context): AnyNode { export function HeaderNav(ctx: Context): AnyNode {
return E("nav", classNames("main-menu"), [ return E("nav", classNames("main-menu"), [
Link("Главная", navLink("/", ctx)), Link(ctx.tr.Home, navLink("/", ctx)),
Link("Рецепты", navLink("/recipes", ctx)), Link(ctx.tr.Recipes, navLink("/recipes", ctx)),
Link("Ингредиенты", navLink("/ingredients", ctx)), Link(ctx.tr.Ingredients, navLink("/ingredients", ctx)),
]); ]);
} }
function navLink(href: string, ctx?: Context): Attrs { function navLink(lhref: string, ctx?: Context): Attrs {
const attrs: Attrs = { href }; const attrs: Attrs = { lhref };
if (ctx?.locPath === href) attrs["aria-current"] = "true"; if (ctx?.locPath === lhref) attrs["aria-current"] = "true";
return attrs; return attrs;
} }
export function Footer(): AnyNode { export function Footer(ctx: Context): AnyNode {
return E("footer", classNames("footer gap-v-1x5"), [ return E("footer", classNames("footer gap-v-1x5"), [
E("div", classNames("content-width"), [ E("div", classNames("content-width"), [
Link("Исходный код", { Link(ctx.tr.Source_code, {
href: "https://notabug.org/pleshevskiy/recipes", href: "https://notabug.org/pleshevskiy/recipes",
rel: "external nofollow noopener noreferrer", 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 { export function Link(text: string, attrs: Attrs | Attrs[]): AnyNode {
return E("a", attrs, text); 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);
}

View File

@ -1,3 +1,12 @@
import { Translations } from "./translates/rus.ts";
export interface Context { export interface Context {
locPath: string; locPath: string;
lang: Lang;
tr: Translations;
}
export enum Lang {
Rus = "rus",
Eng = "eng",
} }

View File

@ -1,5 +1,6 @@
{ {
"imports": { "imports": {
"ren/": "https://notabug.org/pleshevskiy/ren/raw/v2/ren/" "ren/": "https://notabug.org/pleshevskiy/ren/raw/v2/ren/",
"ren/": "../../ren/ren/"
} }
} }

View File

@ -1,11 +1,15 @@
import { Lang } from "../../context.ts";
import { Ingredient } from "../../domain/ingredient/types.ts"; import { Ingredient } from "../../domain/ingredient/types.ts";
import { RestLang } from "../misc_types.ts"; import { RestLang } from "../misc_types.ts";
import { IngredientRepo } from "./types.ts"; import { IngredientRepo } from "./types.ts";
export class RestIngredientRepo implements IngredientRepo { export class RestIngredientRepo implements IngredientRepo {
async fetchIngredients(): Promise<Ingredient[]> { async fetchIngredients(lang: Lang): Promise<Ingredient[]> {
const url = new URL("http://localhost:33333/api/ingredients");
url.searchParams.set("lang", lang);
const res = await fetch( const res = await fetch(
"http://localhost:33333/api/ingredients", url.toString(),
{ headers: { "content-type": "application/json" } }, { headers: { "content-type": "application/json" } },
); );

View File

@ -1,5 +1,6 @@
import { Lang } from "../../context.ts";
import { Ingredient } from "../../domain/ingredient/types.ts"; import { Ingredient } from "../../domain/ingredient/types.ts";
export interface IngredientRepo { export interface IngredientRepo {
fetchIngredients(): Promise<Ingredient[]>; fetchIngredients(lang: Lang): Promise<Ingredient[]>;
} }

View File

@ -1,12 +1,14 @@
import { StrRenderer } from "ren/str.ts"; import { StrRenderer } from "ren/str.ts";
import { Layout } from "./comp/layout.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 { E404Page } from "./views/e404.ts";
import * as log from "./log.ts"; import * as log from "./log.ts";
import { HomePage } from "./views/home.ts"; import { HomePage } from "./views/home.ts";
import { RecipesPage } from "./views/recipes.ts"; import { RecipesPage } from "./views/recipes.ts";
import { IngredientsPage } from "./views/ingredients.ts"; import { IngredientsPage } from "./views/ingredients.ts";
import { RestIngredientRepo } from "./repo/ingredient/rest.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) { if (import.meta.main) {
await main(); await main();
@ -39,14 +41,40 @@ async function serveHttp(conn: Deno.Conn) {
} }
async function handleRequest(req: Request): Promise<Response> { async function handleRequest(req: Request): Promise<Response> {
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); const ctx = createContextFromRequest(req);
try { try {
const res = await tryCreateFileResponse(ctx.locPath); const res = await tryCreateFileResponse(ctx.locPath);
return res; return res;
} catch (_) { } 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 === "/") { if (ctx.locPath === "/") {
return createHtmlResponse(ren.render(HomePage(ctx))); return createHtmlResponse(ren.render(HomePage(ctx)));
@ -54,7 +82,7 @@ async function handleRequest(req: Request): Promise<Response> {
return createHtmlResponse(ren.render(RecipesPage(ctx))); return createHtmlResponse(ren.render(RecipesPage(ctx)));
} else if (ctx.locPath === "/ingredients") { } else if (ctx.locPath === "/ingredients") {
const repo = new RestIngredientRepo(); const repo = new RestIngredientRepo();
const ingredients = await repo.fetchIngredients(); const ingredients = await repo.fetchIngredients(ctx.lang);
return createHtmlResponse( return createHtmlResponse(
ren.render(IngredientsPage(ctx, { ingredients })), ren.render(IngredientsPage(ctx, { ingredients })),
@ -65,12 +93,39 @@ async function handleRequest(req: Request): Promise<Response> {
} }
} }
async function loadAndUpdateTranslations(ctx: Context) {
try {
const translates = await import(`./translates/${ctx.lang}.ts`);
ctx.tr = Object.entries(translates.default as Partial<Translations>)
.reduce(
(acc, [key, val]) => ({
...acc,
[key as keyof Translations]: val,
}),
{ ...ctx.tr } as Translations,
);
} catch (_e) { /* ignore */ }
}
function createContextFromRequest(req: Request): Context { function createContextFromRequest(req: Request): Context {
const locUrl = new URL(req.url);
const lang = langFromUrl(locUrl);
return { 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 { function createHtmlResponse(body: string, status = 200): Response {
return new Response(body, { return new Response(body, {
status, status,

9
web/translates/eng.ts Normal file
View File

@ -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;

11
web/translates/rus.ts Normal file
View File

@ -0,0 +1,11 @@
export const rus = {
Home: "Главная",
Recipes: "Рецепты",
Ingredients: "Ингредиенты",
Source_code: "Исходный код",
Page_not_found: "Страница не найдена",
};
export default rus;
export type Translations = typeof rus;

View File

@ -5,11 +5,11 @@ import { Context } from "../context.ts";
import { H3 } from "../uikit/typo.ts"; import { H3 } from "../uikit/typo.ts";
export function E404Page(ctx: Context): AnyNode { 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"), [ return E("div", classNames("content-width"), [
H3("Страница не найдена"), H3(ctx.tr.Page_not_found),
]); ]);
} }

View File

@ -7,7 +7,7 @@ import { H3 } from "../uikit/typo.ts";
export function HomePage(ctx: Context): AnyNode { export function HomePage(ctx: Context): AnyNode {
return PageLayout(ctx, [ return PageLayout(ctx, [
E("div", classNames("content-width"), [ E("div", classNames("content-width"), [
H3("Главная"), H3(ctx.tr.Home),
]), ]),
]); ]);
} }

View File

@ -15,7 +15,7 @@ export function IngredientsPage(
): AnyNode { ): AnyNode {
return PageLayout(ctx, [ return PageLayout(ctx, [
E("div", classNames("content-width"), [ E("div", classNames("content-width"), [
H3("Ингредиенты"), H3(ctx.tr.Ingredients),
IngredientList(data.ingredients), IngredientList(data.ingredients),
]), ]),
]); ]);

View File

@ -7,7 +7,7 @@ import { H3 } from "../uikit/typo.ts";
export function RecipesPage(ctx: Context): AnyNode { export function RecipesPage(ctx: Context): AnyNode {
return PageLayout(ctx, [ return PageLayout(ctx, [
E("div", classNames("content-width"), [ E("div", classNames("content-width"), [
H3("Рецепты"), H3(ctx.tr.Recipes),
]), ]),
]); ]);
} }