feat(legal): bilingual (RU + EN) legal pages with a language + theme switcher
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m14s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m48s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m14s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m48s
Add English versions of the privacy policy, EULA and offer (ui/legal/*_en.md) and render each legal page as one self-contained bilingual document: both language bodies plus a header language toggle and theme toggle (no back), a small inline ES5 script that switches language client-side (no reload, default Russian + persisted) and applies the theme (system default + persisted override) — mirroring the landing. The offer's English view transliterates the Russian product names spliced from the live catalog. renderLegalHtml now takes both language sources; renderOffer splices the price list into both; the renderer bakes and reads the _en sources and pre-renders the static pages at boot. Also wrap the /offer/ contour probe in the same retry+timeout as the legal probe (the offer page is now bilingual and larger). Docs + renderer/ui tests updated.
This commit is contained in:
+15
-12
@@ -1,21 +1,24 @@
|
||||
// The public legal pages (GET /privacy/, GET /eula/): the owner-edited markdown under ui/legal/,
|
||||
// rendered by the SAME shared renderLegalHtml the offer uses (bundled from ui/src/lib/offer). Unlike
|
||||
// the offer, these carry no dynamic data — no backend fetch, no marker splice. Kept out of server.mjs
|
||||
// so the per-page title/canonical presets are unit-testable without HTTP (test/legal.test.mjs).
|
||||
// The public legal pages (GET /privacy/, GET /eula/): the owner-edited RU + EN markdown under
|
||||
// ui/legal/, rendered by the SAME shared renderLegalHtml the offer uses (bundled from ui/src/lib/offer)
|
||||
// into one bilingual page with a language + theme switcher. Unlike the offer, these carry no dynamic
|
||||
// data — no backend fetch, no marker splice. Kept out of server.mjs so the per-page title/canonical
|
||||
// presets are unit-testable without HTTP (test/legal.test.mjs).
|
||||
import { renderLegalHtml } from '../dist/gameimage.mjs';
|
||||
|
||||
// renderPrivacy renders the privacy-policy markdown as the standalone /privacy/ page.
|
||||
export function renderPrivacy(markdown) {
|
||||
return renderLegalHtml(markdown, {
|
||||
title: 'Политика конфиденциальности — Эрудит',
|
||||
// renderPrivacy renders the privacy-policy markdown (RU + EN) as the standalone /privacy/ page.
|
||||
export function renderPrivacy(ruMd, enMd) {
|
||||
return renderLegalHtml({
|
||||
ru: { md: ruMd, title: 'Политика конфиденциальности — Эрудит' },
|
||||
en: { md: enMd, title: 'Privacy Policy — Erudit' },
|
||||
canonical: 'https://erudit-game.ru/privacy/',
|
||||
});
|
||||
}
|
||||
|
||||
// renderEula renders the end-user licence agreement markdown as the standalone /eula/ page.
|
||||
export function renderEula(markdown) {
|
||||
return renderLegalHtml(markdown, {
|
||||
title: 'Пользовательское соглашение — Эрудит',
|
||||
// renderEula renders the end-user licence agreement markdown (RU + EN) as the standalone /eula/ page.
|
||||
export function renderEula(ruMd, enMd) {
|
||||
return renderLegalHtml({
|
||||
ru: { md: ruMd, title: 'Пользовательское соглашение — Эрудит' },
|
||||
en: { md: enMd, title: 'Terms of Use — Erudit' },
|
||||
canonical: 'https://erudit-game.ru/eula/',
|
||||
});
|
||||
}
|
||||
|
||||
+13
-9
@@ -1,17 +1,21 @@
|
||||
// The public-offer page renderer: splices the live catalog price list into the owner-edited offer
|
||||
// markdown and renders it with the SAME renderOfferHtml the landing build used to invoke (bundled
|
||||
// from ui/src/lib/offer). Kept out of server.mjs so the substitution is unit-testable without HTTP.
|
||||
// The public-offer page renderer: splices the live catalog price list into BOTH language sources of
|
||||
// the owner-edited offer markdown at the pricing marker, and renders the bilingual /offer/ page with
|
||||
// the SAME renderOfferHtml the browser build uses (bundled from ui/src/lib/offer). The English view
|
||||
// transliterates the Russian product names client-side (renderLegalHtml). Kept out of server.mjs so
|
||||
// the substitution is unit-testable without HTTP.
|
||||
import { renderOfferHtml } from '../dist/gameimage.mjs';
|
||||
|
||||
// pricingMarker is the token ui/legal/offer_ru.md carries at §4.4 where the price list belongs. It
|
||||
// pricingMarker is the token ui/legal/offer_{ru,en}.md carry at §4.4 where the price list belongs. It
|
||||
// mirrors the backend marker (backend/internal/payments/offer.go) and is replaced with the fetched
|
||||
// tables before rendering.
|
||||
export const pricingMarker = '<#pricing_template#>';
|
||||
|
||||
// renderOffer returns the standalone /offer/ HTML: the offer markdown with the pricing marker
|
||||
// renderOffer returns the standalone /offer/ HTML: the RU + EN offer markdown with the pricing marker
|
||||
// replaced by pricingTables (the two markdown tables the backend projects from the active catalog),
|
||||
// rendered to a self-contained document. The replacement is literal (a function replacer) so a "$"
|
||||
// in a product title is never read as a replacement pattern; a missing marker leaves the text as is.
|
||||
export function renderOffer(offerMarkdown, pricingTables) {
|
||||
return renderOfferHtml(offerMarkdown.replace(pricingMarker, () => pricingTables));
|
||||
// rendered to a self-contained bilingual document. The replacement is literal (a function replacer)
|
||||
// so a "$" in a product title is never read as a replacement pattern; a missing marker leaves the
|
||||
// text as is.
|
||||
export function renderOffer(ruMarkdown, enMarkdown, pricingTables) {
|
||||
const splice = (md) => md.replace(pricingMarker, () => pricingTables);
|
||||
return renderOfferHtml(splice(ruMarkdown), splice(enMarkdown));
|
||||
}
|
||||
|
||||
+17
-16
@@ -24,21 +24,22 @@ const PORT = Number(process.env.RENDERER_PORT || 8090);
|
||||
// A render request is a finished game's journal — generously capped.
|
||||
const MAX_BODY = 2 * 1024 * 1024;
|
||||
|
||||
// The offer prose is baked into the image (renderer/Dockerfile) and read once at boot; only the
|
||||
// price list is dynamic (fetched per request). The backend endpoint is internal (off the edge
|
||||
// allow-list) and serves the tables from its in-memory cache. RENDERER_OFFER_MD overrides the baked
|
||||
// path for a local run (point it at ../ui/legal/offer_ru.md).
|
||||
const OFFER_MD = readFileSync(process.env.RENDERER_OFFER_MD || new URL('../legal/offer_ru.md', import.meta.url), 'utf8');
|
||||
// The privacy policy (GET /privacy/) and EULA (GET /eula/): static owner-edited markdown, read once at
|
||||
// boot, no dynamic data. RENDERER_PRIVACY_MD / RENDERER_EULA_MD override the baked path for a local run.
|
||||
const PRIVACY_MD = readFileSync(process.env.RENDERER_PRIVACY_MD || new URL('../legal/privacy_ru.md', import.meta.url), 'utf8');
|
||||
const EULA_MD = readFileSync(process.env.RENDERER_EULA_MD || new URL('../legal/eula_ru.md', import.meta.url), 'utf8');
|
||||
// Render the static legal pages once at boot and serve the cached HTML: they carry no dynamic data,
|
||||
// and re-parsing the large EULA markdown on every request is slow enough under deploy-time host
|
||||
// contention (the renderer is CPU/memory-capped) to flake the contour probe. The offer stays
|
||||
// per-request because it splices in the live price list.
|
||||
const PRIVACY_HTML = renderPrivacy(PRIVACY_MD);
|
||||
const EULA_HTML = renderEula(EULA_MD);
|
||||
// Each legal page is bilingual (RU + EN); both markdown sources are baked into the image
|
||||
// (renderer/Dockerfile) and read once at boot. The offer's price list is the only dynamic part
|
||||
// (fetched per request from the backend's internal endpoint and spliced in); privacy + eula carry no
|
||||
// dynamic data. The RENDERER_*_MD / RENDERER_*_EN_MD env vars override the baked paths for a local run.
|
||||
const rd = (p) => readFileSync(p, 'utf8');
|
||||
const OFFER_MD = rd(process.env.RENDERER_OFFER_MD || new URL('../legal/offer_ru.md', import.meta.url));
|
||||
const OFFER_EN_MD = rd(process.env.RENDERER_OFFER_EN_MD || new URL('../legal/offer_en.md', import.meta.url));
|
||||
const PRIVACY_MD = rd(process.env.RENDERER_PRIVACY_MD || new URL('../legal/privacy_ru.md', import.meta.url));
|
||||
const PRIVACY_EN_MD = rd(process.env.RENDERER_PRIVACY_EN_MD || new URL('../legal/privacy_en.md', import.meta.url));
|
||||
const EULA_MD = rd(process.env.RENDERER_EULA_MD || new URL('../legal/eula_ru.md', import.meta.url));
|
||||
const EULA_EN_MD = rd(process.env.RENDERER_EULA_EN_MD || new URL('../legal/eula_en.md', import.meta.url));
|
||||
// Render the static legal pages once at boot and serve the cached HTML: re-parsing the large EULA on
|
||||
// every request is slow enough under deploy-time host contention (the renderer is CPU/memory-capped)
|
||||
// to flake the contour probe. The offer stays per-request because it splices in the live price list.
|
||||
const PRIVACY_HTML = renderPrivacy(PRIVACY_MD, PRIVACY_EN_MD);
|
||||
const EULA_HTML = renderEula(EULA_MD, EULA_EN_MD);
|
||||
const OFFER_PRICING_URL =
|
||||
(process.env.RENDERER_BACKEND_URL || 'http://backend:8080') + '/api/v1/internal/offer/pricing';
|
||||
|
||||
@@ -82,7 +83,7 @@ const server = createServer(async (req, res) => {
|
||||
return;
|
||||
}
|
||||
if (req.method === 'GET' && path === '/offer/') {
|
||||
const html = renderOffer(OFFER_MD, await fetchOfferPricing());
|
||||
const html = renderOffer(OFFER_MD, OFFER_EN_MD, await fetchOfferPricing());
|
||||
res
|
||||
.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' })
|
||||
.end(html);
|
||||
|
||||
Reference in New Issue
Block a user