From 67b67798b9dfaddfea6bb9b0e0d57cc882bc156c Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 2 Sep 2026 21:14:42 +0200 Subject: [PATCH] feat(client): polished toolbar, live online count via SSE - Redesign in-game client toolbar with refined glass styling and buttons - Live online count + emulator status streamed over SSE multicast - Who's-online tooltip throttled to reduce repeated requests - Fix show button so it always returns the hidden toolbar - Use theme CSS variables instead of hardcoded colors - Add toolbar translations across all 25 locales --- src/app/(site)/page.tsx | 14 +- src/app/api/online/count/stream/route.ts | 98 ++++++ src/app/client/client-view.tsx | 389 +++++++++++++++++------ src/components/live-online-count.tsx | 33 ++ src/components/live-online-counter.tsx | 32 ++ src/components/site-header.tsx | 5 +- src/components/top-header.tsx | 5 +- src/messages/ar.json | 13 + src/messages/bg.json | 13 + src/messages/cs.json | 13 + src/messages/da.json | 13 + src/messages/de.json | 13 + src/messages/el.json | 13 + src/messages/en.json | 13 + src/messages/es.json | 13 + src/messages/fi.json | 13 + src/messages/fr.json | 13 + src/messages/hr.json | 13 + src/messages/hu.json | 13 + src/messages/it.json | 13 + src/messages/ja.json | 13 + src/messages/nl.json | 13 + src/messages/no.json | 13 + src/messages/pl.json | 13 + src/messages/pt.json | 13 + src/messages/ro.json | 13 + src/messages/ru.json | 13 + src/messages/sk.json | 13 + src/messages/sr.json | 13 + src/messages/sv.json | 13 + src/messages/tr.json | 13 + src/messages/uk.json | 13 + 32 files changed, 799 insertions(+), 102 deletions(-) create mode 100644 src/app/api/online/count/stream/route.ts create mode 100644 src/components/live-online-count.tsx create mode 100644 src/components/live-online-counter.tsx diff --git a/src/app/(site)/page.tsx b/src/app/(site)/page.tsx index 98e968eb..4a2835d4 100644 --- a/src/app/(site)/page.tsx +++ b/src/app/(site)/page.tsx @@ -9,6 +9,8 @@ import { HomeLoginForm } from "@/components/auth/home-login-form"; import { Clock } from "@/components/clock"; import { LanguageSwitcher } from "@/components/language-switcher"; import Link from "@/components/link"; +import { LiveOnlineCount } from "@/components/live-online-count"; +import { LiveOnlineCounter } from "@/components/live-online-counter"; import { Reveal } from "@/components/motion-reveal"; import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail"; import { SurfaceCard } from "@/components/surface-card"; @@ -235,7 +237,7 @@ export default async function Home() { - {th("online", { count: online, hotel: hotelName })} +

(
- + {s.live ? ( + + ) : ( + + )}
{ + try { + return await cached("online_count", 10_000, async () => { + const [row] = await db + .select({ total: count() }) + .from(User) + .where(eq(User.online, "1")); + return row?.total ?? 0; + }); + } catch { + return 0; + } +} + +// Emulator (RCON) reachability, cached and shared across all subscribers so an +// N-client toolbar doesn't fire N pings every interval. Falls back to graceful +// degradation on failure so the stream never errors out. +async function fetchEmulatorStatus(): Promise { + try { + return await cached("emulator_rcon_status", 15_000, () => + rcon.send("ping", null).catch(() => false), + ); + } catch { + return false; + } +} + +export async function GET(req: Request) { + const encoder = new TextEncoder(); + + const stream = new ReadableStream({ + async start(controller) { + let closed = false; + + const send = async () => { + if (closed) return; + const [count, emulator] = await Promise.all([ + fetchOnlineCount(), + fetchEmulatorStatus(), + ]); + try { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ count, emulator })}\n\n`), + ); + } catch { + closed = true; + } + }; + + // Initial event immediately, then on an interval. + await send(); + const interval = setInterval(() => void send(), 5_000); + // SSE comment as a keep-alive ping between data events. + const ping = setInterval(() => { + if (!closed) { + try { + controller.enqueue(encoder.encode(": ping\n\n")); + } catch { + closed = true; + } + } + }, 25_000); + + const stop = () => { + closed = true; + clearInterval(interval); + clearInterval(ping); + try { + controller.close(); + } catch { + /* already closed */ + } + }; + req.signal.addEventListener("abort", stop); + }, + }); + + return new Response(stream, { + headers: { + "content-type": "text/event-stream; charset=utf-8", + "cache-control": "no-store, no-transform", + connection: "keep-alive", + }, + }); +} diff --git a/src/app/client/client-view.tsx b/src/app/client/client-view.tsx index aa14ba05..177283d0 100644 --- a/src/app/client/client-view.tsx +++ b/src/app/client/client-view.tsx @@ -1,6 +1,7 @@ "use client"; import { LogOut } from "lucide-react"; +import { useTranslations } from "next-intl"; import { type ReactNode, useCallback, @@ -12,45 +13,78 @@ import { import { signOutAndRevokeTicket } from "@/actions/sessions"; import Link from "@/components/link"; import { buildClientLoginUrl } from "@/lib/client-url"; +import { useEventSource } from "@/lib/use-event-source"; function ToolbarBtn({ onClick, - title, + label, children, href, }: { onClick?: () => void; - title: string; + label: string; children: ReactNode; href?: string; }) { + // Custom floating tooltip: appears on hover/focus regardless of where the + // element is on screen. Kept in one component for a consistent look across + // the bar (replaces the browser's slow, untitled `title` bubbles). + const [tip, setTip] = useState(false); const cls = - "w-7 h-7 sm:w-8 sm:h-8 rounded-lg border-2 cursor-pointer flex items-center justify-center shadow-lg transition-all duration-200 hover:-translate-y-0.5 hover:shadow-xl active:translate-y-0"; + "group relative w-8 h-8 sm:w-9 sm:h-9 rounded-xl cursor-pointer flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5 hover:brightness-110 active:translate-y-0 active:scale-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-primary"; const style = { - borderColor: "color-mix(in srgb, var(--color-primary, #eeb425) 75%, black)", + border: + "1px solid color-mix(in srgb, var(--color-primary, #eeb425) 85%, white)", background: - "linear-gradient(135deg, var(--color-primary, #eeb425) 0%, color-mix(in srgb, var(--color-primary, #eeb425) 80%, black) 100%)", + "linear-gradient(180deg, color-mix(in srgb, var(--color-primary, #eeb425) 92%, white), var(--color-primary, #eeb425) 55%, color-mix(in srgb, var(--color-primary, #eeb425) 60%, black))", color: "var(--button-text-color-readable, var(--button-text-color, #1a1a2e))", + boxShadow: + "0 2px 8px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.35), inset 0 -2px 4px rgba(0,0,0,0.12)", } as const; - if (href) { - return ( - - {children} - - ); - } - return ( + + const trigger = href ? ( + setTip(true)} + onMouseLeave={() => setTip(false)} + onFocus={() => setTip(true)} + onBlur={() => setTip(false)} + > + {children} + + ) : ( ); + + return ( + + {trigger} + {tip && ( + + {label} + + )} + + ); } export function ClientView({ @@ -64,17 +98,47 @@ export function ClientView({ hotelName: string; initialOnline: number; }) { - const [_isFullscreen, setIsFullscreen] = useState(false); + const tToolbar = useTranslations("toolbar"); + const [isFullscreen, setIsFullscreen] = useState(false); const [onlineCount, setOnlineCount] = useState(initialOnline); + const [emulatorOk, setEmulatorOk] = useState(null); + const [onlineUsers, setOnlineUsers] = useState([]); + const [showOnlineUsers, setShowOnlineUsers] = useState(false); + const [barHidden, setBarHidden] = useState(false); + // Real-time online count + emulator status via SSE multicast (replaces the + // legacy 15s polling). + const { data: onlineStream } = useEventSource<{ + count: number; + emulator: boolean; + }>("/api/online/count/stream", { enabled: true }); useEffect(() => { - const interval = setInterval(async () => { - try { - const res = await fetch("/api/online/count"); - if (res.ok) setOnlineCount((await res.json()).count ?? 0); - } catch {} - }, 15000); - return () => clearInterval(interval); + if (!onlineStream) return; + if (typeof onlineStream.count === "number") + setOnlineCount(onlineStream.count); + if (typeof onlineStream.emulator === "boolean") + setEmulatorOk(onlineStream.emulator); + }, [onlineStream]); + + // "Who's online" list, fetched sparingly (server-side cached 10s). Only a + // fresh fetch is issued when hovered and the last fetch is stale — hovering + // repeatedly must not spam the endpoint. + const onlineUsersRef = useRef(0); + const refreshOnlineUsers = useCallback(() => { + const now = Date.now(); + if (now - onlineUsersRef.current < 30_000) return; + onlineUsersRef.current = now; + fetch("/api/online", { cache: "no-store" }) + .then((r) => (r.ok ? r.json() : null)) + .then((data) => { + const usernames = Array.isArray(data?.users) + ? data.users + .map((u: { username?: string }) => u.username) + .filter((n: unknown): n is string => typeof n === "string") + : []; + setOnlineUsers(usernames); + }) + .catch(() => setOnlineUsers([])); }, []); const toggleFullscreen = useCallback(() => { @@ -163,16 +227,18 @@ export function ClientView({ const handleMouseDown = useCallback( (e: React.MouseEvent) => { - if ((e.target as HTMLElement).closest("button, a")) return; - startDrag(e.clientX, e.clientY); + if ((e.target as HTMLElement).closest(".toolbar-grip")) { + startDrag(e.clientX, e.clientY); + } }, [startDrag], ); const handleTouchStart = useCallback( (e: React.TouchEvent) => { - if ((e.target as HTMLElement).closest("button, a")) return; - startDrag(e.touches[0].clientX, e.touches[0].clientY); + if ((e.target as HTMLElement).closest(".toolbar-grip")) { + startDrag(e.touches[0].clientX, e.touches[0].clientY); + } }, [startDrag], ); @@ -225,22 +291,24 @@ export function ClientView({ return ( <> {pos ? ( - // biome-ignore lint/a11y/noStaticElementInteractions: draggable toolbar with explicit mouse/touch handlers
-
- +
+ - Home + {tToolbar("home")} - window.location.reload()} title="Reload"> - - Reload - - - - - - - - Fullscreen - - - -
window.location.reload()} + label={tToolbar("reload")} > - Online users + {tToolbar("reload")} + + + + + + + {isFullscreen ? ( + + {tToolbar("fullscreenExit")} + + + ) : ( + + {tToolbar("fullscreen")} + + + )} + +
+ + {emulatorOk === false ? ( + + ) : ( + + )} + +
+
{ + setShowOnlineUsers(true); + refreshOnlineUsers(); + }} + onMouseLeave={() => setShowOnlineUsers(false)} + > + + + + + + {tToolbar("onlineUsers")} {onlineCount} + {showOnlineUsers && onlineUsers.length > 0 && ( + + {onlineUsers.slice(0, 8).join(", ")} + {onlineUsers.length > 8 + ? ` +${onlineUsers.length - 8} more` + : ""} + + )}
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: grip handle explicitly initiates a drag (mouse+touch) and is not a button */}
- Drag + {tToolbar("dragToMove")} @@ -361,14 +501,75 @@ export function ClientView({
void signOutAndRevokeTicket()} - title="Logout" + label={tToolbar("logout")} > + setBarHidden(true)} + label={tToolbar("hide")} + > + + {tToolbar("hide")} + + +
) : null} + {barHidden && pos ? ( + + ) : null} +
( + "/api/online/count/stream", + { enabled: true }, + ); + useEffect(() => { + if (onlineStream && typeof onlineStream.count === "number") { + setCount(onlineStream.count); + } + }, [onlineStream]); + + return <>{t("online", { count, hotel: hotelName })}; +} diff --git a/src/components/live-online-counter.tsx b/src/components/live-online-counter.tsx new file mode 100644 index 00000000..aecb4fdc --- /dev/null +++ b/src/components/live-online-counter.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { AnimatedCounter } from "@/components/animated-counter"; +import { useEventSource } from "@/lib/use-event-source"; + +/** + * Live animated online-count number. Subscribes to the SSE online-count stream + * so the stat updates in real-time (multicast) instead of only the initial SSR + * value, while keeping the entrance counter animation. + */ +export function LiveOnlineCounter({ + initial, + suffix = "", +}: { + initial: number; + suffix?: string; +}) { + const [count, setCount] = useState(initial); + + const { data: onlineStream } = useEventSource<{ count: number }>( + "/api/online/count/stream", + { enabled: true }, + ); + useEffect(() => { + if (onlineStream && typeof onlineStream.count === "number") { + setCount(onlineStream.count); + } + }, [onlineStream]); + + return ; +} diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx index 81c51095..66fa0a36 100644 --- a/src/components/site-header.tsx +++ b/src/components/site-header.tsx @@ -1,14 +1,13 @@ import { count, eq } from "drizzle-orm"; import Image from "next/image"; -import { getTranslations } from "next-intl/server"; import Link from "@/components/link"; +import { LiveOnlineCount } from "@/components/live-online-count"; import { cached } from "@/lib/cache"; import { db, User } from "@/lib/db"; import { resolveHotelName } from "@/lib/hotel-name"; import { siteSettings } from "@/lib/services/site-settings"; export async function SiteHeader() { - const t = await getTranslations("header"); const [hotelName, header, logo] = await Promise.all([ resolveHotelName(), siteSettings.get("cms_header", "/assets/images/background.png"), @@ -140,7 +139,7 @@ export async function SiteHeader() { className="text-xs sm:text-sm font-bold tracking-wide" style={{ color: "rgba(255,255,255,0.9)" }} > - {t("online", { count: online, hotel: hotelName })} +
diff --git a/src/components/top-header.tsx b/src/components/top-header.tsx index ab65692c..f39d4a7c 100644 --- a/src/components/top-header.tsx +++ b/src/components/top-header.tsx @@ -1,10 +1,10 @@ import { count, eq, inArray } from "drizzle-orm"; import Image from "next/image"; import type { Session } from "next-auth"; -import { getTranslations } from "next-intl/server"; import { signOutAndRevokeTicket } from "@/actions/sessions"; import { HeaderUserIdentity } from "@/components/header-user-identity"; import Link from "@/components/link"; +import { LiveOnlineCount } from "@/components/live-online-count"; import { RoomQuickEntry } from "@/components/room-quick-entry"; import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail"; import { cached } from "@/lib/cache"; @@ -100,7 +100,6 @@ export async function TopHeader({ session }: { session: Session | null }) { const textColor = "var(--color-navbar-text-readable, var(--color-navbar-text, var(--color-text)))"; - const t = await getTranslations("header"); const hotelName = await resolveHotelName(); let online: number; @@ -173,7 +172,7 @@ export async function TopHeader({ session }: { session: Session | null }) { className="text-[10px] sm:text-sm font-bold tracking-wide" style={{ color: "rgba(255,255,255,0.9)" }} > - {t("online", { count: online, hotel: hotelName })} +
diff --git a/src/messages/ar.json b/src/messages/ar.json index 678cb97a..ae67f357 100644 --- a/src/messages/ar.json +++ b/src/messages/ar.json @@ -9,5 +9,18 @@ "studio": "Studio" } } + }, + "toolbar": { + "home": "الرئيسية", + "reload": "إعادة تحميل", + "fullscreen": "ملء الشاشة", + "logout": "تسجيل الخروج", + "onlineUsers": "المستخدمون المتواجدون", + "emulatorOnline": "المحاكي متصل", + "emulatorOffline": "المحاكي غير متصل", + "dragToMove": "اسحب للتحريك", + "hide": "إخفاء الشريط", + "fullscreenExit": "الخروج من ملء الشاشة", + "show": "إظهار الشريط" } } diff --git a/src/messages/bg.json b/src/messages/bg.json index c11a3727..26358438 100644 --- a/src/messages/bg.json +++ b/src/messages/bg.json @@ -3262,5 +3262,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Начало", + "reload": "Презареждане", + "fullscreen": "Цял екран", + "logout": "Изход", + "onlineUsers": "Потребители онлайн", + "emulatorOnline": "Емулаторът е онлайн", + "emulatorOffline": "Емулаторът е офлайн", + "dragToMove": "Влачете за преместване", + "hide": "Скрий лентата", + "fullscreenExit": "Изход от цял екран", + "show": "Покажи лентата" } } diff --git a/src/messages/cs.json b/src/messages/cs.json index 005595e8..d018adf5 100644 --- a/src/messages/cs.json +++ b/src/messages/cs.json @@ -3262,5 +3262,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Domů", + "reload": "Obnovit", + "fullscreen": "Celá obrazovka", + "logout": "Odhlásit", + "onlineUsers": "Uživatelé online", + "emulatorOnline": "Emulátor online", + "emulatorOffline": "Emulátor offline", + "dragToMove": "Přetažením přesunete", + "hide": "Skrýt lištu", + "fullscreenExit": "Ukončit celou obrazovku", + "show": "Zobrazit lištu" } } diff --git a/src/messages/da.json b/src/messages/da.json index adf0ab6d..a2ef0df1 100644 --- a/src/messages/da.json +++ b/src/messages/da.json @@ -3262,5 +3262,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Hjem", + "reload": "Genindlæs", + "fullscreen": "Fuldskærm", + "logout": "Log ud", + "onlineUsers": "Brugere online", + "emulatorOnline": "Emulator online", + "emulatorOffline": "Emulator offline", + "dragToMove": "Træk for at flytte", + "hide": "Skjul værktøjslinje", + "fullscreenExit": "Afslut fuldskærm", + "show": "Vis værktøjslinje" } } diff --git a/src/messages/de.json b/src/messages/de.json index 38dfeb29..7ebaabb5 100644 --- a/src/messages/de.json +++ b/src/messages/de.json @@ -3263,5 +3263,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Startseite", + "reload": "Neu laden", + "fullscreen": "Vollbild", + "logout": "Abmelden", + "onlineUsers": "Nutzer online", + "emulatorOnline": "Emulator online", + "emulatorOffline": "Emulator offline", + "dragToMove": "Ziehen zum Verschieben", + "hide": "Leiste ausblenden", + "fullscreenExit": "Vollbild beenden", + "show": "Leiste anzeigen" } } diff --git a/src/messages/el.json b/src/messages/el.json index 3c74e882..9be7dbc0 100644 --- a/src/messages/el.json +++ b/src/messages/el.json @@ -3262,5 +3262,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Αρχική", + "reload": "Επαναφόρτωση", + "fullscreen": "Πλήρης οθόνη", + "logout": "Αποσύνδεση", + "onlineUsers": "Χρήστες συνδεδεμένοι", + "emulatorOnline": "Ο εξομοιωτής είναι online", + "emulatorOffline": "Ο εξομοιωτής είναι offline", + "dragToMove": "Σύρετε για μετακίνηση", + "hide": "Απόκρυψη γραμμής", + "fullscreenExit": "Έξοδος από πλήρη οθόνη", + "show": "Εμφάνιση γραμμής" } } diff --git a/src/messages/en.json b/src/messages/en.json index 3840db69..c2fcda00 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -3321,5 +3321,18 @@ } } } + }, + "toolbar": { + "home": "Home", + "reload": "Reload", + "fullscreen": "Fullscreen", + "logout": "Logout", + "onlineUsers": "Online users", + "emulatorOnline": "Emulator online", + "emulatorOffline": "Emulator offline", + "dragToMove": "Drag to move", + "hide": "Hide toolbar", + "fullscreenExit": "Exit fullscreen", + "show": "Show toolbar" } } diff --git a/src/messages/es.json b/src/messages/es.json index 0be19582..d716ed98 100644 --- a/src/messages/es.json +++ b/src/messages/es.json @@ -3263,5 +3263,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Inicio", + "reload": "Recargar", + "fullscreen": "Pantalla completa", + "logout": "Cerrar sesión", + "onlineUsers": "Usuarios en línea", + "emulatorOnline": "Emulador en línea", + "emulatorOffline": "Emulador fuera de línea", + "dragToMove": "Arrastra para mover", + "hide": "Ocultar barra", + "fullscreenExit": "Salir de pantalla completa", + "show": "Mostrar barra" } } diff --git a/src/messages/fi.json b/src/messages/fi.json index 678cb97a..ee7d5eeb 100644 --- a/src/messages/fi.json +++ b/src/messages/fi.json @@ -9,5 +9,18 @@ "studio": "Studio" } } + }, + "toolbar": { + "home": "Koti", + "reload": "Lataa uudelleen", + "fullscreen": "Koko näyttö", + "logout": "Kirjaudu ulos", + "onlineUsers": "Käyttäjät verkossa", + "emulatorOnline": "Emulaattori verkossa", + "emulatorOffline": "Emulaattori ei verkossa", + "dragToMove": "Vedä liikuttaaksesi", + "hide": "Piilota työkalupalkki", + "fullscreenExit": "Lopeta koko näyttö", + "show": "Näytä työkalupalkki" } } diff --git a/src/messages/fr.json b/src/messages/fr.json index 8d41a177..5b23e4b0 100644 --- a/src/messages/fr.json +++ b/src/messages/fr.json @@ -3263,5 +3263,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Accueil", + "reload": "Recharger", + "fullscreen": "Plein écran", + "logout": "Déconnexion", + "onlineUsers": "Utilisateurs en ligne", + "emulatorOnline": "Émulateur en ligne", + "emulatorOffline": "Émulateur hors ligne", + "dragToMove": "Glissez pour déplacer", + "hide": "Masquer la barre", + "fullscreenExit": "Quitter le plein écran", + "show": "Afficher la barre" } } diff --git a/src/messages/hr.json b/src/messages/hr.json index eb5128cf..6916e818 100644 --- a/src/messages/hr.json +++ b/src/messages/hr.json @@ -3262,5 +3262,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Početna", + "reload": "Ponovno učitaj", + "fullscreen": "Cijeli zaslon", + "logout": "Odjava", + "onlineUsers": "Korisnici online", + "emulatorOnline": "Emulator je online", + "emulatorOffline": "Emulator je offline", + "dragToMove": "Povucite za pomicanje", + "hide": "Sakrij traku", + "fullscreenExit": "Izađi iz cijelog zaslona", + "show": "Prikaži traku" } } diff --git a/src/messages/hu.json b/src/messages/hu.json index bce6f5c5..c81b6294 100644 --- a/src/messages/hu.json +++ b/src/messages/hu.json @@ -3262,5 +3262,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Kezdőlap", + "reload": "Újratöltés", + "fullscreen": "Teljes képernyő", + "logout": "Kijelentkezés", + "onlineUsers": "Online felhasználók", + "emulatorOnline": "Emulátor online", + "emulatorOffline": "Emulátor offline", + "dragToMove": "Húzás a mozgatáshoz", + "hide": "Sáv elrejtése", + "fullscreenExit": "Kilépés a teljes képernyőből", + "show": "Sáv megjelenítése" } } diff --git a/src/messages/it.json b/src/messages/it.json index 307cb530..24ab9630 100644 --- a/src/messages/it.json +++ b/src/messages/it.json @@ -3320,5 +3320,18 @@ } } } + }, + "toolbar": { + "home": "Home", + "reload": "Ricarica", + "fullscreen": "Schermo intero", + "logout": "Esci", + "onlineUsers": "Utenti online", + "emulatorOnline": "Emulatore online", + "emulatorOffline": "Emulatore offline", + "dragToMove": "Trascina per spostare", + "hide": "Nascondi barra", + "fullscreenExit": "Esci dalla schermo intero", + "show": "Mostra barra" } } diff --git a/src/messages/ja.json b/src/messages/ja.json index 678cb97a..b9a87a3b 100644 --- a/src/messages/ja.json +++ b/src/messages/ja.json @@ -9,5 +9,18 @@ "studio": "Studio" } } + }, + "toolbar": { + "home": "ホーム", + "reload": "再読み込み", + "fullscreen": "全画面", + "logout": "ログアウト", + "onlineUsers": "オンラインのユーザー", + "emulatorOnline": "エミュレータオンライン", + "emulatorOffline": "エミュレータオフライン", + "dragToMove": "ドラッグして移動", + "hide": "ツールバーを隠す", + "fullscreenExit": "全画面を終了", + "show": "ツールバーを表示" } } diff --git a/src/messages/nl.json b/src/messages/nl.json index 7e00beb5..26d71527 100644 --- a/src/messages/nl.json +++ b/src/messages/nl.json @@ -3265,5 +3265,18 @@ } } } + }, + "toolbar": { + "home": "Home", + "reload": "Vernieuwen", + "fullscreen": "Volledig scherm", + "logout": "Uitloggen", + "onlineUsers": "Gebruikers online", + "emulatorOnline": "Emulator online", + "emulatorOffline": "Emulator offline", + "dragToMove": "Sleep om te verplaatsen", + "hide": "Werkbalk verbergen", + "fullscreenExit": "Volledig scherm afsluiten", + "show": "Werkbalk tonen" } } diff --git a/src/messages/no.json b/src/messages/no.json index 86b4a407..8f01644a 100644 --- a/src/messages/no.json +++ b/src/messages/no.json @@ -3262,5 +3262,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Hjem", + "reload": "Last inn på nytt", + "fullscreen": "Fullskjerm", + "logout": "Logg ut", + "onlineUsers": "Brukere på nett", + "emulatorOnline": "Emulator på nett", + "emulatorOffline": "Emulator frakoblet", + "dragToMove": "Dra for å flytte", + "hide": "Skjul verktøylinje", + "fullscreenExit": "Avslutt fullskjerm", + "show": "Vis verktøylinje" } } diff --git a/src/messages/pl.json b/src/messages/pl.json index a0992235..60566f9d 100644 --- a/src/messages/pl.json +++ b/src/messages/pl.json @@ -3263,5 +3263,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Start", + "reload": "Przeładuj", + "fullscreen": "Pełny ekran", + "logout": "Wyloguj", + "onlineUsers": "Użytkownicy online", + "emulatorOnline": "Emulator online", + "emulatorOffline": "Emulator offline", + "dragToMove": "Przeciągnij, aby przenieść", + "hide": "Ukryj pasek", + "fullscreenExit": "Zamknij pełny ekran", + "show": "Pokaż pasek" } } diff --git a/src/messages/pt.json b/src/messages/pt.json index e74a48fb..b4ef75ad 100644 --- a/src/messages/pt.json +++ b/src/messages/pt.json @@ -3263,5 +3263,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Início", + "reload": "Recarregar", + "fullscreen": "Tela cheia", + "logout": "Sair", + "onlineUsers": "Usuários online", + "emulatorOnline": "Emulador online", + "emulatorOffline": "Emulador offline", + "dragToMove": "Arraste para mover", + "hide": "Ocultar barra", + "fullscreenExit": "Sair da tela cheia", + "show": "Mostrar barra" } } diff --git a/src/messages/ro.json b/src/messages/ro.json index c78744fd..e41bfd2b 100644 --- a/src/messages/ro.json +++ b/src/messages/ro.json @@ -3262,5 +3262,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Acasă", + "reload": "Reîncarcă", + "fullscreen": "Ecran complet", + "logout": "Deconectare", + "onlineUsers": "Utilizatori online", + "emulatorOnline": "Emulatorul este online", + "emulatorOffline": "Emulatorul este offline", + "dragToMove": "Trageți pentru a muta", + "hide": "Ascunde bara", + "fullscreenExit": "Ieșiți din ecran complet", + "show": "Afișați bara" } } diff --git a/src/messages/ru.json b/src/messages/ru.json index bb5ade06..12a31846 100644 --- a/src/messages/ru.json +++ b/src/messages/ru.json @@ -3261,5 +3261,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Главная", + "reload": "Перезагрузить", + "fullscreen": "Полный экран", + "logout": "Выйти", + "onlineUsers": "Пользователи онлайн", + "emulatorOnline": "Эмулятор онлайн", + "emulatorOffline": "Эмулятор офлайн", + "dragToMove": "Перетащите, чтобы переместить", + "hide": "Скрыть панель", + "fullscreenExit": "Выйти из полноэкранного режима", + "show": "Показать панель" } } diff --git a/src/messages/sk.json b/src/messages/sk.json index 7e1bd649..95248fd9 100644 --- a/src/messages/sk.json +++ b/src/messages/sk.json @@ -3262,5 +3262,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Domov", + "reload": "Obnoviť", + "fullscreen": "Celá obrazovka", + "logout": "Odhlásiť", + "onlineUsers": "Používatelia online", + "emulatorOnline": "Emulátor online", + "emulatorOffline": "Emulátor offline", + "dragToMove": "Potiahnite na presun", + "hide": "Skryť panel", + "fullscreenExit": "Ukončiť celú obrazovku", + "show": "Zobraziť panel" } } diff --git a/src/messages/sr.json b/src/messages/sr.json index 3faaa802..b7b88350 100644 --- a/src/messages/sr.json +++ b/src/messages/sr.json @@ -3262,5 +3262,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Почетна", + "reload": "Освежи", + "fullscreen": "Цео екран", + "logout": "Одјава", + "onlineUsers": "Корисници на мрежи", + "emulatorOnline": "Емулатор онлајн", + "emulatorOffline": "Емулатор офлајн", + "dragToMove": "Превуците да померите", + "hide": "Сакриј траку", + "fullscreenExit": "Изађи из целог екрана", + "show": "Прикажи траку" } } diff --git a/src/messages/sv.json b/src/messages/sv.json index f2f6de14..89aa9a5c 100644 --- a/src/messages/sv.json +++ b/src/messages/sv.json @@ -3262,5 +3262,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Hem", + "reload": "Ladda om", + "fullscreen": "Helskärm", + "logout": "Logga ut", + "onlineUsers": "Användare online", + "emulatorOnline": "Emulator online", + "emulatorOffline": "Emulator offline", + "dragToMove": "Dra för att flytta", + "hide": "Dölj verktygsfält", + "fullscreenExit": "Avsluta helskärm", + "show": "Visa verktygsfält" } } diff --git a/src/messages/tr.json b/src/messages/tr.json index 15caa96a..67e73204 100644 --- a/src/messages/tr.json +++ b/src/messages/tr.json @@ -3263,5 +3263,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Ana sayfa", + "reload": "Yenile", + "fullscreen": "Tam ekran", + "logout": "Çıkış", + "onlineUsers": "Çevrimiçi kullanıcılar", + "emulatorOnline": "Emülatör çevrimiçi", + "emulatorOffline": "Emülatör çevrimdışı", + "dragToMove": "Taşımak için sürükleyin", + "hide": "Araç çubuğunu gizle", + "fullscreenExit": "Tam ekrandan çık", + "show": "Araç çubuğunu göster" } } diff --git a/src/messages/uk.json b/src/messages/uk.json index 75cf7354..2131797f 100644 --- a/src/messages/uk.json +++ b/src/messages/uk.json @@ -3262,5 +3262,18 @@ "button": "Verify email", "fallback": "If the button doesn't work, paste this link into your browser:" } + }, + "toolbar": { + "home": "Головна", + "reload": "Перезавантажити", + "fullscreen": "Повний екран", + "logout": "Вийти", + "onlineUsers": "Користувачі онлайн", + "emulatorOnline": "Емулятор онлайн", + "emulatorOffline": "Емулятор офлайн", + "dragToMove": "Перетягніть, щоб перемістити", + "hide": "Сховати панель", + "fullscreenExit": "Вийти з повноекранного режиму", + "show": "Показати панель" } }