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
This commit is contained in:
1 parent
e1ecb190bc
commit
67b67798b9
32 files changed
+758
-61
No files matched your search
+11
-1
@@ -9,6 +9,8 @@ import { HomeLoginForm } from "@/components/auth/home-login-form";
|
|||||||
import { Clock } from "@/components/clock";
|
import { Clock } from "@/components/clock";
|
||||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||||
import Link from "@/components/link";
|
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 { Reveal } from "@/components/motion-reveal";
|
||||||
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
|
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
|
||||||
import { SurfaceCard } from "@/components/surface-card";
|
import { SurfaceCard } from "@/components/surface-card";
|
||||||
@@ -235,7 +237,7 @@ export default async function Home() {
|
|||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
||||||
</span>
|
</span>
|
||||||
{th("online", { count: online, hotel: hotelName })}
|
<LiveOnlineCount initial={online} hotelName={hotelName} />
|
||||||
</div>
|
</div>
|
||||||
<h1
|
<h1
|
||||||
className="text-2xl font-black leading-[1.05] tracking-tight mb-2 sm:text-3xl md:text-5xl lg:text-6xl"
|
className="text-2xl font-black leading-[1.05] tracking-tight mb-2 sm:text-3xl md:text-5xl lg:text-6xl"
|
||||||
@@ -337,21 +339,25 @@ export default async function Home() {
|
|||||||
value: users,
|
value: users,
|
||||||
label: tp("statsCitizens"),
|
label: tp("statsCitizens"),
|
||||||
icon: "/assets/images/icons/friends.png",
|
icon: "/assets/images/icons/friends.png",
|
||||||
|
live: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: rooms,
|
value: rooms,
|
||||||
label: tp("statsRooms"),
|
label: tp("statsRooms"),
|
||||||
icon: "/assets/images/icons/catalog.png",
|
icon: "/assets/images/icons/catalog.png",
|
||||||
|
live: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: online,
|
value: online,
|
||||||
label: tp("statsOnline"),
|
label: tp("statsOnline"),
|
||||||
icon: "/assets/images/icons/navigation/me.png",
|
icon: "/assets/images/icons/navigation/me.png",
|
||||||
|
live: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: articles.length,
|
value: articles.length,
|
||||||
label: tp("statsArticles"),
|
label: tp("statsArticles"),
|
||||||
icon: "/assets/images/icons/article.gif",
|
icon: "/assets/images/icons/article.gif",
|
||||||
|
live: false,
|
||||||
},
|
},
|
||||||
].map((s) => (
|
].map((s) => (
|
||||||
<SurfaceCard
|
<SurfaceCard
|
||||||
@@ -359,7 +365,11 @@ export default async function Home() {
|
|||||||
className="relative p-3 sm:p-5 text-center transition-all duration-300 hover:-translate-y-1 hover:shadow-lg"
|
className="relative p-3 sm:p-5 text-center transition-all duration-300 hover:-translate-y-1 hover:shadow-lg"
|
||||||
>
|
>
|
||||||
<div className="mb-0.5 text-2xl sm:text-3xl font-black tracking-tight md:text-4xl">
|
<div className="mb-0.5 text-2xl sm:text-3xl font-black tracking-tight md:text-4xl">
|
||||||
|
{s.live ? (
|
||||||
|
<LiveOnlineCounter initial={s.value} />
|
||||||
|
) : (
|
||||||
<AnimatedCounter value={s.value} />
|
<AnimatedCounter value={s.value} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="text-[10px] sm:text-xs font-bold uppercase tracking-widest flex items-center justify-center gap-1 sm:gap-1.5"
|
className="text-[10px] sm:text-xs font-bold uppercase tracking-widest flex items-center justify-center gap-1 sm:gap-1.5"
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// Server-Sent Events stream of the live online user count for the in-game
|
||||||
|
// client toolbar. Pushes { count } every ~5s so the counter updates in
|
||||||
|
// real-time without the client polling. Closes cleanly when the client
|
||||||
|
// disconnects.
|
||||||
|
//
|
||||||
|
// The first event is sent immediately; subsequent events push the cached
|
||||||
|
// count (10s TTL in the REST endpoint) so repeated reads share one query.
|
||||||
|
|
||||||
|
import { count, eq } from "drizzle-orm";
|
||||||
|
import { cached } from "@/lib/cache";
|
||||||
|
import { db, User } from "@/lib/db";
|
||||||
|
import { rcon } from "@/lib/services/rcon";
|
||||||
|
|
||||||
|
async function fetchOnlineCount(): Promise<number> {
|
||||||
|
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<boolean> {
|
||||||
|
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<Uint8Array>({
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
+255
-54
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,33 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useEventSource } from "@/lib/use-event-source";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live online user counter. Subscribes to the SSE online-count stream so the
|
||||||
|
* displayed count updates in real-time (multicast) instead of only reflecting
|
||||||
|
* the server-rendered value at page load.
|
||||||
|
*/
|
||||||
|
export function LiveOnlineCount({
|
||||||
|
initial,
|
||||||
|
hotelName,
|
||||||
|
}: {
|
||||||
|
initial: number;
|
||||||
|
hotelName: string;
|
||||||
|
}) {
|
||||||
|
const t = useTranslations("header");
|
||||||
|
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 <>{t("online", { count, hotel: hotelName })}</>;
|
||||||
|
}
|
||||||
@@ -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 <AnimatedCounter value={count} suffix={suffix} />;
|
||||||
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import { count, eq } from "drizzle-orm";
|
import { count, eq } from "drizzle-orm";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { getTranslations } from "next-intl/server";
|
|
||||||
import Link from "@/components/link";
|
import Link from "@/components/link";
|
||||||
|
import { LiveOnlineCount } from "@/components/live-online-count";
|
||||||
import { cached } from "@/lib/cache";
|
import { cached } from "@/lib/cache";
|
||||||
import { db, User } from "@/lib/db";
|
import { db, User } from "@/lib/db";
|
||||||
import { resolveHotelName } from "@/lib/hotel-name";
|
import { resolveHotelName } from "@/lib/hotel-name";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
|
|
||||||
export async function SiteHeader() {
|
export async function SiteHeader() {
|
||||||
const t = await getTranslations("header");
|
|
||||||
const [hotelName, header, logo] = await Promise.all([
|
const [hotelName, header, logo] = await Promise.all([
|
||||||
resolveHotelName(),
|
resolveHotelName(),
|
||||||
siteSettings.get("cms_header", "/assets/images/background.png"),
|
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"
|
className="text-xs sm:text-sm font-bold tracking-wide"
|
||||||
style={{ color: "rgba(255,255,255,0.9)" }}
|
style={{ color: "rgba(255,255,255,0.9)" }}
|
||||||
>
|
>
|
||||||
{t("online", { count: online, hotel: hotelName })}
|
<LiveOnlineCount initial={online} hotelName={hotelName} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { count, eq, inArray } from "drizzle-orm";
|
import { count, eq, inArray } from "drizzle-orm";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
import { getTranslations } from "next-intl/server";
|
|
||||||
import { signOutAndRevokeTicket } from "@/actions/sessions";
|
import { signOutAndRevokeTicket } from "@/actions/sessions";
|
||||||
import { HeaderUserIdentity } from "@/components/header-user-identity";
|
import { HeaderUserIdentity } from "@/components/header-user-identity";
|
||||||
import Link from "@/components/link";
|
import Link from "@/components/link";
|
||||||
|
import { LiveOnlineCount } from "@/components/live-online-count";
|
||||||
import { RoomQuickEntry } from "@/components/room-quick-entry";
|
import { RoomQuickEntry } from "@/components/room-quick-entry";
|
||||||
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
|
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
|
||||||
import { cached } from "@/lib/cache";
|
import { cached } from "@/lib/cache";
|
||||||
@@ -100,7 +100,6 @@ export async function TopHeader({ session }: { session: Session | null }) {
|
|||||||
const textColor =
|
const textColor =
|
||||||
"var(--color-navbar-text-readable, var(--color-navbar-text, var(--color-text)))";
|
"var(--color-navbar-text-readable, var(--color-navbar-text, var(--color-text)))";
|
||||||
|
|
||||||
const t = await getTranslations("header");
|
|
||||||
const hotelName = await resolveHotelName();
|
const hotelName = await resolveHotelName();
|
||||||
|
|
||||||
let online: number;
|
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"
|
className="text-[10px] sm:text-sm font-bold tracking-wide"
|
||||||
style={{ color: "rgba(255,255,255,0.9)" }}
|
style={{ color: "rgba(255,255,255,0.9)" }}
|
||||||
>
|
>
|
||||||
{t("online", { count: online, hotel: hotelName })}
|
<LiveOnlineCount initial={online} hotelName={hotelName} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,18 @@
|
|||||||
"studio": "Studio"
|
"studio": "Studio"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"toolbar": {
|
||||||
|
"home": "الرئيسية",
|
||||||
|
"reload": "إعادة تحميل",
|
||||||
|
"fullscreen": "ملء الشاشة",
|
||||||
|
"logout": "تسجيل الخروج",
|
||||||
|
"onlineUsers": "المستخدمون المتواجدون",
|
||||||
|
"emulatorOnline": "المحاكي متصل",
|
||||||
|
"emulatorOffline": "المحاكي غير متصل",
|
||||||
|
"dragToMove": "اسحب للتحريك",
|
||||||
|
"hide": "إخفاء الشريط",
|
||||||
|
"fullscreenExit": "الخروج من ملء الشاشة",
|
||||||
|
"show": "إظهار الشريط"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3262,5 +3262,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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": "Покажи лентата"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3262,5 +3262,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3262,5 +3262,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3263,5 +3263,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3262,5 +3262,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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": "Εμφάνιση γραμμής"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3263,5 +3263,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,5 +9,18 @@
|
|||||||
"studio": "Studio"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3263,5 +3263,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3262,5 +3262,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3262,5 +3262,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,5 +9,18 @@
|
|||||||
"studio": "Studio"
|
"studio": "Studio"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"toolbar": {
|
||||||
|
"home": "ホーム",
|
||||||
|
"reload": "再読み込み",
|
||||||
|
"fullscreen": "全画面",
|
||||||
|
"logout": "ログアウト",
|
||||||
|
"onlineUsers": "オンラインのユーザー",
|
||||||
|
"emulatorOnline": "エミュレータオンライン",
|
||||||
|
"emulatorOffline": "エミュレータオフライン",
|
||||||
|
"dragToMove": "ドラッグして移動",
|
||||||
|
"hide": "ツールバーを隠す",
|
||||||
|
"fullscreenExit": "全画面を終了",
|
||||||
|
"show": "ツールバーを表示"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3262,5 +3262,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3263,5 +3263,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3263,5 +3263,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3262,5 +3262,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3261,5 +3261,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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": "Показать панель"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3262,5 +3262,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3262,5 +3262,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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": "Прикажи траку"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3262,5 +3262,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3263,5 +3263,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3262,5 +3262,18 @@
|
|||||||
"button": "Verify email",
|
"button": "Verify email",
|
||||||
"fallback": "If the button doesn't work, paste this link into your browser:"
|
"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": "Показати панель"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user