add translates
This commit is contained in:
parent
a6353699df
commit
b918557791
|
@ -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", {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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/"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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(
|
||||
"http://localhost:33333/api/ingredients",
|
||||
url.toString(),
|
||||
{ headers: { "content-type": "application/json" } },
|
||||
);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Lang } from "../../context.ts";
|
||||
import { Ingredient } from "../../domain/ingredient/types.ts";
|
||||
|
||||
export interface IngredientRepo {
|
||||
fetchIngredients(): Promise<Ingredient[]>;
|
||||
fetchIngredients(lang: Lang): Promise<Ingredient[]>;
|
||||
}
|
||||
|
|
|
@ -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<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);
|
||||
|
||||
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<Response> {
|
|||
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<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 {
|
||||
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,
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,11 @@
|
|||
export const rus = {
|
||||
Home: "Главная",
|
||||
Recipes: "Рецепты",
|
||||
Ingredients: "Ингредиенты",
|
||||
Source_code: "Исходный код",
|
||||
Page_not_found: "Страница не найдена",
|
||||
};
|
||||
|
||||
export default rus;
|
||||
|
||||
export type Translations = typeof rus;
|
|
@ -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),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export function IngredientsPage(
|
|||
): AnyNode {
|
||||
return PageLayout(ctx, [
|
||||
E("div", classNames("content-width"), [
|
||||
H3("Ингредиенты"),
|
||||
H3(ctx.tr.Ingredients),
|
||||
IngredientList(data.ingredients),
|
||||
]),
|
||||
]);
|
||||
|
|
|
@ -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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue