feat(home): rebuild the landing page on a shared panel system
CI / check (push) Successful in 32s
CI / tests-unit (push) Successful in 2m12s
CI / tests-integration (push) Successful in 2m43s
CI / tests-ui (push) Successful in 4m22s
CI / tests-imaging-browsers (push) Successful in 2m10s
CI / tests-quality (push) Successful in 1m32s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 3m1s

The homepage mixed four different translucent washes (surface 44-60% plus a
backdrop-filter each), rendered the same four counters twice, and showed a
dangling "Welcome to " eyebrow because the localized string expected the hotel
name. A missing theme wallpaper replaced the whole sky gradient with flat
white, and stale CMS image references produced broken images and 404s.

Replace the ad-hoc panels with one opaque, theme-derived `.panel` token set
(surface, border, shadow, header, radius) and a `.panel-float` variant for the
counter strip that straddles the hero edge. The strip is now the single home
of the four headline counters; a failed read renders "unavailable" instead of
silently dropping a cell. Add the fourth feature card (Games) that already had
translated copy, link recent users to their profiles, and fall back to themed
placeholders for news covers and camera photos without an upload.

Add `pages.home.heroEyebrow` to all 25 locales, layer the CMS wallpaper over
`--site-bg-gradient` instead of replacing it, and keep the sky in dark mode.
Add `publicAssetExists` so local uploads that no longer exist are skipped
(wallpaper, help topics) rather than rendered broken. Fix the `/help` topic
icons to use the `/assets/images/help-center/` base the detail page already
used.

Throttle the spotlight glow to one update per animation frame, drop the dead
`.glass-chip` rules, define the missing `.panel-float`, and fix the stat
divider and reduced-motion selectors. Stat labels use the readable text token
so they clear WCAG AA, and the language button's accessible name now contains
its visible label.

Verified: tsc, biome, 3590 unit tests, i18n audit unchanged, axe 0 violations
at 390/768/1024/1280/1440 in light and dark, quality e2e 16/16.
This commit is contained in:
openhands committed 2026-10-22 17:39:30 +02:00
1 parent ecd8e5852c
commit 97454f361b
35 files changed
+933 -659

No files matched your search

+5 -1
View File
@@ -6,6 +6,7 @@ import { ContentCard } from "@/components/public/ui";
import { SanitizedHtml } from "@/components/shared/sanitized-html"; import { SanitizedHtml } from "@/components/shared/sanitized-html";
import { db, WebsiteHelpCenterCategories } from "@/lib/db"; import { db, WebsiteHelpCenterCategories } from "@/lib/db";
import { resolveHotelName } from "@/lib/hotel-name"; import { resolveHotelName } from "@/lib/hotel-name";
import { publicAssetExists } from "@/lib/public-asset";
import { sanitize } from "@/lib/sanitize"; import { sanitize } from "@/lib/sanitize";
// Asset path matches AtomCMS's Blade view: asset('/assets/images/help-center/<image_url>'). // Asset path matches AtomCMS's Blade view: asset('/assets/images/help-center/<image_url>').
@@ -99,7 +100,10 @@ export default async function HelpCategoryPage({
const title = withHotel(cat.name, hotelName); const title = withHotel(cat.name, hotelName);
const content = withHotel(cat.content, hotelName); const content = withHotel(cat.content, hotelName);
const buttonText = withHotel(cat.buttonText, hotelName); const buttonText = withHotel(cat.buttonText, hotelName);
const imageSrc = cat.imageUrl ? `${HELP_IMAGE_BASE}/${cat.imageUrl}` : ""; const imageSrc =
cat.imageUrl && publicAssetExists(`${HELP_IMAGE_BASE}/${cat.imageUrl}`)
? `${HELP_IMAGE_BASE}/${cat.imageUrl}`
: "";
const hasButton = Boolean(cat.buttonText && cat.buttonText.trim() !== ""); const hasButton = Boolean(cat.buttonText && cat.buttonText.trim() !== "");
return ( return (
File diff suppressed because it is too large. Load diff
+530 -504
View File
File diff suppressed because it is too large. Load diff
+177 -81
View File
File diff suppressed because it is too large. Load diff
+2 -8
View File
@@ -26,16 +26,10 @@ export function AuthTopBar({
return ( return (
<Reveal> <Reveal>
<div <div
className="relative flex items-center justify-between gap-3 rounded-full border px-3 py-2.5 sm:px-5" className="panel relative flex items-center justify-between gap-3 rounded-full px-3 py-2.5 sm:px-5"
style={{ style={{
background: background:
"linear-gradient(90deg, color-mix(in srgb, var(--color-surface) 62%, transparent), color-mix(in srgb, var(--color-primary) 7%, transparent))", "linear-gradient(90deg, var(--panel-surface), color-mix(in srgb, var(--color-primary) 7%, var(--panel-surface)))",
backdropFilter: "blur(14px)",
WebkitBackdropFilter: "blur(14px)",
borderColor:
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
boxShadow:
"0 1px 3px rgba(0,0,0,0.04), 0 0 0 0.5px color-mix(in srgb, var(--color-text-muted) 6%, transparent)",
}} }}
> >
<div className="flex min-w-0 items-center gap-2 sm:gap-3"> <div className="flex min-w-0 items-center gap-2 sm:gap-3">
+1 -1
View File
@@ -267,7 +267,7 @@ export function LanguageSwitcher({
type="button" type="button"
onClick={() => setOpen(!open)} onClick={() => setOpen(!open)}
disabled={pending} disabled={pending}
aria-label="Change language" aria-label={`Change language — ${current.code.toUpperCase()}`}
title="Language" title="Language"
aria-expanded={open} aria-expanded={open}
aria-haspopup="listbox" aria-haspopup="listbox"
+36 -4
View File
@@ -1,6 +1,12 @@
"use client"; "use client";
import type { CSSProperties, MouseEvent, ReactNode } from "react"; import {
type CSSProperties,
type MouseEvent,
type ReactNode,
useEffect,
useRef,
} from "react";
interface SpotlightCardProps { interface SpotlightCardProps {
children: ReactNode; children: ReactNode;
@@ -12,22 +18,48 @@ interface SpotlightCardProps {
* Wraps a card with a mouse-tracking radial glow. The pointer position is * Wraps a card with a mouse-tracking radial glow. The pointer position is
* written to `--spot-x` / `--spot-y` and consumed by the `.spotlight::before` * written to `--spot-x` / `--spot-y` and consumed by the `.spotlight::before`
* overlay in globals.css. * overlay in globals.css.
*
* The pointer move handler is throttled to one update per animation frame:
* a pointermove fires far more often than the browser paints, and every
* extra write re-styles the card (and its ::before gradient) for nothing.
*/ */
export function SpotlightCard({ export function SpotlightCard({
children, children,
className, className,
style, style,
}: SpotlightCardProps) { }: SpotlightCardProps) {
const frame = useRef(0);
const pending = useRef<{ x: number; y: number } | null>(null);
// Drop the queued frame if the card is removed mid-gesture.
useEffect(
() => () => {
if (frame.current) cancelAnimationFrame(frame.current);
},
[],
);
const handlePointerMove = (event: MouseEvent<HTMLDivElement>) => { const handlePointerMove = (event: MouseEvent<HTMLDivElement>) => {
const el = event.currentTarget; const el = event.currentTarget;
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
el.style.setProperty("--spot-x", `${event.clientX - rect.left}px`); pending.current = {
el.style.setProperty("--spot-y", `${event.clientY - rect.top}px`); x: event.clientX - rect.left,
y: event.clientY - rect.top,
};
if (frame.current) return;
frame.current = requestAnimationFrame(() => {
frame.current = 0;
const point = pending.current;
if (!point) return;
el.style.setProperty("--spot-x", `${point.x}px`);
el.style.setProperty("--spot-y", `${point.y}px`);
});
}; };
return ( return (
<div <div
className={className} className={className ? `spotlight ${className}` : "spotlight"}
style={ style={
{ "--spot-x": "50%", "--spot-y": "50%", ...style } as CSSProperties { "--spot-x": "50%", "--spot-y": "50%", ...style } as CSSProperties
} }
+13 -2
View File
@@ -1,4 +1,5 @@
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { publicAssetExists } from "@/lib/public-asset";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
import { import {
BACKGROUND_DEFAULTS, BACKGROUND_DEFAULTS,
@@ -240,9 +241,19 @@ export async function ThemeVars({ nonce }: { nonce?: string } = {}) {
? Math.min(parseInt(backgroundOverlayOpacity as string, 10), 90) ? Math.min(parseInt(backgroundOverlayOpacity as string, 10), 90)
: 0; : 0;
// The wallpaper is painted as the *first* background layer on top of the
// theme gradient (`--site-bg-gradient`). It used to be the only layer,
// which meant a missing upload wiped the sky entirely and left the site on
// flat white. Layering keeps the gradient as a guaranteed fallback.
//
// A local wallpaper that no longer exists on disk (deleted upload, stale
// theme setting) would also fire a 404 on every page view, so only emit the
// layer when the file is actually there. Remote URLs are trusted as-is.
const backgroundCss = const backgroundCss =
backgroundImage && isSafeBackgroundUrl(backgroundImage) backgroundImage &&
? `body.site-bg{background-image:url("${backgroundImage}");background-position:${safeBgPosition};background-size:${safeBgSize};background-repeat:${safeBgRepeat}}` isSafeBackgroundUrl(backgroundImage) &&
publicAssetExists(backgroundImage)
? `body.site-bg{background-image:url("${backgroundImage}"),var(--site-bg-gradient);background-position:${safeBgPosition},center top;background-size:${safeBgSize},cover;background-repeat:${safeBgRepeat},no-repeat}`
: ""; : "";
const backdropStyle: CSSProperties | undefined = safeOverlay const backdropStyle: CSSProperties | undefined = safeOverlay
+51
View File
@@ -0,0 +1,51 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { publicAssetExists } from "./public-asset";
describe("publicAssetExists", () => {
const originalCwd = process.cwd();
let root: string;
beforeAll(() => {
root = mkdtempSync(join(tmpdir(), "public-asset-"));
mkdirSync(join(root, "public", "assets"), { recursive: true });
writeFileSync(join(root, "public", "assets", "present.png"), "");
process.chdir(root);
});
afterAll(() => {
process.chdir(originalCwd);
rmSync(root, { recursive: true, force: true });
});
it("accepts a local file that exists", () => {
expect(publicAssetExists("/assets/present.png")).toBe(true);
});
it("ignores query strings and hashes", () => {
expect(publicAssetExists("/assets/present.png?v=2#x")).toBe(true);
});
it("rejects a local file that is missing", () => {
expect(publicAssetExists("/assets/gone.png")).toBe(false);
});
it("rejects empty input", () => {
expect(publicAssetExists(null)).toBe(false);
expect(publicAssetExists(undefined)).toBe(false);
expect(publicAssetExists("")).toBe(false);
});
it("trusts remote and protocol-relative URLs", () => {
expect(publicAssetExists("https://cdn.example.com/a.png")).toBe(true);
expect(publicAssetExists("http://cdn.example.com/a.png")).toBe(true);
expect(publicAssetExists("//cdn.example.com/a.png")).toBe(true);
});
it("rejects non-http schemes", () => {
expect(publicAssetExists("javascript:alert(1)")).toBe(false);
expect(publicAssetExists("data:image/png;base64,AAAA")).toBe(false);
});
});
+26
View File
@@ -0,0 +1,26 @@
import { existsSync } from "node:fs";
import { join } from "node:path";
/**
* True when a site-relative URL points at a file that really exists under
* `public/`. CMS records can keep pointing at an upload that has since been
* deleted, and a stale reference renders a broken image (or a 404 background
* layer) on every page view. Server-only: it stats the filesystem.
*
* Remote URLs (`http:`, `https:`, protocol-relative) are trusted as-is because
* we cannot stat them, and data/blob URIs are rejected outright.
*/
export function publicAssetExists(url: string | null | undefined): boolean {
if (!url) return false;
if (url.startsWith("//")) return true;
if (/^[a-z][a-z0-9+.-]*:/i.test(url)) {
return /^https?:/i.test(url);
}
if (!url.startsWith("/")) return true;
try {
const file = decodeURIComponent(url.split(/[?#]/)[0] ?? "");
return existsSync(join(process.cwd(), "public", file));
} catch {
return false;
}
}
+1
View File
@@ -5353,6 +5353,7 @@
}, },
"home": { "home": {
"welcome": "Welcome to {hotelName}", "welcome": "Welcome to {hotelName}",
"heroEyebrow": "أهلاً بك",
"welcomeSubtitle": "Your retro hotel, now powered by Next.js", "welcomeSubtitle": "Your retro hotel, now powered by Next.js",
"welcomeBody": "Hang out, collect furni, and meet friends.", "welcomeBody": "Hang out, collect furni, and meet friends.",
"enterHotel": "Enter {hotelName}", "enterHotel": "Enter {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Добре дошли в {hotelName}", "welcome": "Добре дошли в {hotelName}",
"heroEyebrow": "Добре дошли",
"welcomeSubtitle": "Вашият ретро хотел, сега захранван от Next.js", "welcomeSubtitle": "Вашият ретро хотел, сега захранван от Next.js",
"welcomeBody": "Излизайте, събирайте мебели и се срещайте с приятели.", "welcomeBody": "Излизайте, събирайте мебели и се срещайте с приятели.",
"enterHotel": "Въведете {hotelName}", "enterHotel": "Въведете {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Vítejte v {hotelName}", "welcome": "Vítejte v {hotelName}",
"heroEyebrow": "Vítejte",
"welcomeSubtitle": "Váš retro hotel, nyní poháněný Next.js", "welcomeSubtitle": "Váš retro hotel, nyní poháněný Next.js",
"welcomeBody": "Setkejte se, sbírejte nábytek a setkejte se s přáteli.", "welcomeBody": "Setkejte se, sbírejte nábytek a setkejte se s přáteli.",
"enterHotel": "Zadejte {hotelName}", "enterHotel": "Zadejte {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Velkommen til {hotelName}", "welcome": "Velkommen til {hotelName}",
"heroEyebrow": "Velkommen",
"welcomeSubtitle": "Dit retrohotel, nu drevet af Next.js", "welcomeSubtitle": "Dit retrohotel, nu drevet af Next.js",
"welcomeBody": "Hæng ud, saml møbler og mød venner.", "welcomeBody": "Hæng ud, saml møbler og mød venner.",
"enterHotel": "Indtast {hotelName}", "enterHotel": "Indtast {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Willkommen bei {hotelName}", "welcome": "Willkommen bei {hotelName}",
"heroEyebrow": "Willkommen",
"welcomeSubtitle": "Dein Retro-Hotel, jetzt mit Next.js", "welcomeSubtitle": "Dein Retro-Hotel, jetzt mit Next.js",
"welcomeBody": "Abhängen, Möbel sammeln und Freunde treffen.", "welcomeBody": "Abhängen, Möbel sammeln und Freunde treffen.",
"enterHotel": "{hotelName} betreten", "enterHotel": "{hotelName} betreten",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Καλώς ήρθατε στο {hotelName}", "welcome": "Καλώς ήρθατε στο {hotelName}",
"heroEyebrow": "Καλώς ήρθατε",
"welcomeSubtitle": "Το ρετρό ξενοδοχείο σας, τώρα με την υποστήριξη του Next.js", "welcomeSubtitle": "Το ρετρό ξενοδοχείο σας, τώρα με την υποστήριξη του Next.js",
"welcomeBody": "Κάντε παρέα, μαζέψτε έπιπλα και συναντήστε φίλους.", "welcomeBody": "Κάντε παρέα, μαζέψτε έπιπλα και συναντήστε φίλους.",
"enterHotel": "Εισαγάγετε {hotelName}", "enterHotel": "Εισαγάγετε {hotelName}",
+1
View File
@@ -278,6 +278,7 @@
}, },
"home": { "home": {
"welcome": "Welcome to {hotelName}", "welcome": "Welcome to {hotelName}",
"heroEyebrow": "Welcome",
"welcomeSubtitle": "Your retro hotel, now powered by Next.js", "welcomeSubtitle": "Your retro hotel, now powered by Next.js",
"welcomeBody": "Hang out, collect furni, and meet friends.", "welcomeBody": "Hang out, collect furni, and meet friends.",
"enterHotel": "Enter {hotelName}", "enterHotel": "Enter {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Bienvenido a {hotelName}", "welcome": "Bienvenido a {hotelName}",
"heroEyebrow": "Bienvenido",
"welcomeSubtitle": "Tu hotel retro, ahora impulsado por Next.js", "welcomeSubtitle": "Tu hotel retro, ahora impulsado por Next.js",
"welcomeBody": "Pasa el rato, colecciona furni y conoce amigos.", "welcomeBody": "Pasa el rato, colecciona furni y conoce amigos.",
"enterHotel": "Entrar a {hotelName}", "enterHotel": "Entrar a {hotelName}",
+1
View File
@@ -5353,6 +5353,7 @@
}, },
"home": { "home": {
"welcome": "Welcome to {hotelName}", "welcome": "Welcome to {hotelName}",
"heroEyebrow": "Tervetuloa",
"welcomeSubtitle": "Your retro hotel, now powered by Next.js", "welcomeSubtitle": "Your retro hotel, now powered by Next.js",
"welcomeBody": "Hang out, collect furni, and meet friends.", "welcomeBody": "Hang out, collect furni, and meet friends.",
"enterHotel": "Enter {hotelName}", "enterHotel": "Enter {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Bienvenue sur {hotelName}", "welcome": "Bienvenue sur {hotelName}",
"heroEyebrow": "Bienvenue",
"welcomeSubtitle": "Votre hôtel rétro, désormais propulsé par Next.js", "welcomeSubtitle": "Votre hôtel rétro, désormais propulsé par Next.js",
"welcomeBody": "Traînez, collectionnez des meubles et rencontrez des amis.", "welcomeBody": "Traînez, collectionnez des meubles et rencontrez des amis.",
"enterHotel": "Entrer dans {hotelName}", "enterHotel": "Entrer dans {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Dobro došli u {hotelName}", "welcome": "Dobro došli u {hotelName}",
"heroEyebrow": "Dobrodošli",
"welcomeSubtitle": "Vaš retro hotel, sada pokreće Next.js", "welcomeSubtitle": "Vaš retro hotel, sada pokreće Next.js",
"welcomeBody": "Družite se, skupljajte namještaj i upoznajte prijatelje.", "welcomeBody": "Družite se, skupljajte namještaj i upoznajte prijatelje.",
"enterHotel": "Unesite {hotelName}", "enterHotel": "Unesite {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Üdvözöljük itt: {hotelName}", "welcome": "Üdvözöljük itt: {hotelName}",
"heroEyebrow": "Üdvözöljük",
"welcomeSubtitle": "Az Ön retro szállodája, amelyet most a Next.js üzemeltet", "welcomeSubtitle": "Az Ön retro szállodája, amelyet most a Next.js üzemeltet",
"welcomeBody": "Tárcsázz, gyűjts bútorokat és találkozz barátokkal.", "welcomeBody": "Tárcsázz, gyűjts bútorokat és találkozz barátokkal.",
"enterHotel": "Írja be a következőt: {hotelName}", "enterHotel": "Írja be a következőt: {hotelName}",
+1
View File
@@ -274,6 +274,7 @@
}, },
"home": { "home": {
"welcome": "Benvenuto su {hotelName}", "welcome": "Benvenuto su {hotelName}",
"heroEyebrow": "Benvenuto",
"welcomeSubtitle": "Il tuo hotel retrò, ora con tecnologia Next.js", "welcomeSubtitle": "Il tuo hotel retrò, ora con tecnologia Next.js",
"welcomeBody": "Rilassati, colleziona furni e fai nuove amicizie.", "welcomeBody": "Rilassati, colleziona furni e fai nuove amicizie.",
"enterHotel": "Entra in {hotelName}", "enterHotel": "Entra in {hotelName}",
+1
View File
@@ -5353,6 +5353,7 @@
}, },
"home": { "home": {
"welcome": "Welcome to {hotelName}", "welcome": "Welcome to {hotelName}",
"heroEyebrow": "ようこそ",
"welcomeSubtitle": "Your retro hotel, now powered by Next.js", "welcomeSubtitle": "Your retro hotel, now powered by Next.js",
"welcomeBody": "Hang out, collect furni, and meet friends.", "welcomeBody": "Hang out, collect furni, and meet friends.",
"enterHotel": "Enter {hotelName}", "enterHotel": "Enter {hotelName}",
+1
View File
@@ -274,6 +274,7 @@
}, },
"home": { "home": {
"welcome": "Welkom bij {hotelName}", "welcome": "Welkom bij {hotelName}",
"heroEyebrow": "Welkom",
"welcomeSubtitle": "Je retro hotel, nu mogelijk gemaakt door Next.js", "welcomeSubtitle": "Je retro hotel, nu mogelijk gemaakt door Next.js",
"welcomeBody": "Rondhangen, meubels verzamelen en vrienden ontmoeten.", "welcomeBody": "Rondhangen, meubels verzamelen en vrienden ontmoeten.",
"enterHotel": "{hotelName} betreden", "enterHotel": "{hotelName} betreden",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Velkommen til {hotelName}", "welcome": "Velkommen til {hotelName}",
"heroEyebrow": "Velkommen",
"welcomeSubtitle": "Ditt retrohotell, nå drevet av Next.js", "welcomeSubtitle": "Ditt retrohotell, nå drevet av Next.js",
"welcomeBody": "Heng med, samle møbler og møt venner.", "welcomeBody": "Heng med, samle møbler og møt venner.",
"enterHotel": "Skriv inn {hotelName}", "enterHotel": "Skriv inn {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Witamy w {hotelName}", "welcome": "Witamy w {hotelName}",
"heroEyebrow": "Witamy",
"welcomeSubtitle": "Twój hotel w stylu retro, teraz obsługiwany przez Next.js", "welcomeSubtitle": "Twój hotel w stylu retro, teraz obsługiwany przez Next.js",
"welcomeBody": "Spędzaj czas, zbieraj meble i spotykaj się z przyjaciółmi.", "welcomeBody": "Spędzaj czas, zbieraj meble i spotykaj się z przyjaciółmi.",
"enterHotel": "Wpisz {hotelName}", "enterHotel": "Wpisz {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Bem-vindo ao {hotelName}", "welcome": "Bem-vindo ao {hotelName}",
"heroEyebrow": "Bem-vindo",
"welcomeSubtitle": "Seu hotel retrô, agora com tecnologia Next.js", "welcomeSubtitle": "Seu hotel retrô, agora com tecnologia Next.js",
"welcomeBody": "Saia, colete mobis e encontre amigos.", "welcomeBody": "Saia, colete mobis e encontre amigos.",
"enterHotel": "Digite {hotelName}", "enterHotel": "Digite {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Bun venit la {hotelName}", "welcome": "Bun venit la {hotelName}",
"heroEyebrow": "Bine ai venit",
"welcomeSubtitle": "Hotelul tău retro, acum dezvoltat de Next.js", "welcomeSubtitle": "Hotelul tău retro, acum dezvoltat de Next.js",
"welcomeBody": "Ieșiți, strângeți mobilier și întâlniți-vă cu prietenii.", "welcomeBody": "Ieșiți, strângeți mobilier și întâlniți-vă cu prietenii.",
"enterHotel": "Introdu {hotelName}", "enterHotel": "Introdu {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Добро пожаловать в {hotelName}", "welcome": "Добро пожаловать в {hotelName}",
"heroEyebrow": "Добро пожаловать",
"welcomeSubtitle": "Ваш ретро-отель на базе Next.js", "welcomeSubtitle": "Ваш ретро-отель на базе Next.js",
"welcomeBody": "Тусуйтесь, собирайте мебель и встречайтесь с друзьями.", "welcomeBody": "Тусуйтесь, собирайте мебель и встречайтесь с друзьями.",
"enterHotel": "Введите {hotelName}", "enterHotel": "Введите {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Vitajte v {hotelName}", "welcome": "Vitajte v {hotelName}",
"heroEyebrow": "Vitajte",
"welcomeSubtitle": "Váš retro hotel, ktorý teraz používa Next.js", "welcomeSubtitle": "Váš retro hotel, ktorý teraz používa Next.js",
"welcomeBody": "Stretnite sa, zbierajte nábytok a stretávajte sa s priateľmi.", "welcomeBody": "Stretnite sa, zbierajte nábytok a stretávajte sa s priateľmi.",
"enterHotel": "Zadajte {hotelName}", "enterHotel": "Zadajte {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Добро дошли у {hotelName}", "welcome": "Добро дошли у {hotelName}",
"heroEyebrow": "Добродошли",
"welcomeSubtitle": "Your retro hotel, now powered by Next.js", "welcomeSubtitle": "Your retro hotel, now powered by Next.js",
"welcomeBody": "Hang out, collect furni, and meet friends.", "welcomeBody": "Hang out, collect furni, and meet friends.",
"enterHotel": "Унесите {hotelName}", "enterHotel": "Унесите {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Välkommen till {hotelName}", "welcome": "Välkommen till {hotelName}",
"heroEyebrow": "Välkommen",
"welcomeSubtitle": "Ditt retrohotell, nu drivs av Next.js", "welcomeSubtitle": "Ditt retrohotell, nu drivs av Next.js",
"welcomeBody": "Umgås, samla möbler och träffa vänner.", "welcomeBody": "Umgås, samla möbler och träffa vänner.",
"enterHotel": "Ange {hotelName}", "enterHotel": "Ange {hotelName}",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "{hotelName}'e hoş geldiniz", "welcome": "{hotelName}'e hoş geldiniz",
"heroEyebrow": "Hoş geldiniz",
"welcomeSubtitle": "Retro oteliniz artık Next.js tarafından destekleniyor", "welcomeSubtitle": "Retro oteliniz artık Next.js tarafından destekleniyor",
"welcomeBody": "Takılın, mobilya toplayın ve arkadaşlarla tanışın.", "welcomeBody": "Takılın, mobilya toplayın ve arkadaşlarla tanışın.",
"enterHotel": "{hotelName} girin", "enterHotel": "{hotelName} girin",
+1
View File
@@ -232,6 +232,7 @@
}, },
"home": { "home": {
"welcome": "Ласкаво просимо до {hotelName}", "welcome": "Ласкаво просимо до {hotelName}",
"heroEyebrow": "Ласкаво просимо",
"welcomeSubtitle": "Ваш ретро-готель, тепер на базі Next.js", "welcomeSubtitle": "Ваш ретро-готель, тепер на базі Next.js",
"welcomeBody": "Гуляйте, збирайте меблі та зустрічайтеся з друзями.", "welcomeBody": "Гуляйте, збирайте меблі та зустрічайтеся з друзями.",
"enterHotel": "Введіть {hotelName}", "enterHotel": "Введіть {hotelName}",