fix(i18n): persist CMS translations and validate message catalogs
This commit is contained in:
1 parent
96234075f8
commit
ef94646d60
37 files changed
+1621
-463
No files matched your search
+25
-38
@@ -3,60 +3,47 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import * as JSONC from "jsonc-parser";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { z } from "zod";
|
||||
import { SUPPORTED_LOCALES } from "@/i18n/locales";
|
||||
import {
|
||||
CLIENT_TRANSLATION_FILES,
|
||||
getClientTranslationFile,
|
||||
} from "@/lib/client-translation-files";
|
||||
import {
|
||||
CmsTranslationError,
|
||||
saveCmsTranslation,
|
||||
} from "@/lib/cms-translations";
|
||||
import { patchJson5 } from "@/lib/json5-patch";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { adminAction } from "@/lib/safe-action";
|
||||
import { ActionError, actionOk } from "@/lib/safe-action-shared";
|
||||
|
||||
const saveTranslationsSchema = z.object({
|
||||
locale: z.enum([
|
||||
"en",
|
||||
"it",
|
||||
"nl",
|
||||
"de",
|
||||
"fr",
|
||||
"es",
|
||||
"pt",
|
||||
"pl",
|
||||
"sv",
|
||||
"tr",
|
||||
"ro",
|
||||
"hu",
|
||||
"cs",
|
||||
"sk",
|
||||
"da",
|
||||
"no",
|
||||
"el",
|
||||
"bg",
|
||||
"hr",
|
||||
"sr",
|
||||
"uk",
|
||||
"ru",
|
||||
]),
|
||||
data: z.record(z.string(), z.unknown()),
|
||||
locale: z.enum(SUPPORTED_LOCALES),
|
||||
revision: z.string().regex(/^[a-f0-9]{64}$/),
|
||||
changes: z
|
||||
.record(z.string().max(300), z.string().max(20000).nullable())
|
||||
.refine((value) => Object.keys(value).length <= 5000),
|
||||
});
|
||||
|
||||
export const saveTranslations = adminAction(
|
||||
{ permission: PERMS.SETTINGS_EDIT, schema: saveTranslationsSchema },
|
||||
async (ctx) => {
|
||||
const filePath = path.join(
|
||||
process.cwd(),
|
||||
"src",
|
||||
"messages",
|
||||
`${ctx.data.locale}.json`,
|
||||
);
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
JSON.stringify(ctx.data.data, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
return actionOk();
|
||||
try {
|
||||
const snapshot = await saveCmsTranslation(
|
||||
ctx.data.locale,
|
||||
ctx.data.revision,
|
||||
ctx.data.changes,
|
||||
);
|
||||
revalidatePath("/", "layout");
|
||||
return actionOk({ snapshot });
|
||||
} catch (error) {
|
||||
if (!(error instanceof CmsTranslationError)) throw error;
|
||||
const t = await getTranslations("pages.admin.translations.cms");
|
||||
return { ok: false, error: t(error.code, { key: error.key }) };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function AdminErrorPage({
|
||||
reset: () => void;
|
||||
}) {
|
||||
const t = useTranslations("pages.error");
|
||||
const tNav = useTranslations("admin.nav");
|
||||
const tNav = useTranslations("pages.admin.nav");
|
||||
|
||||
const [reference, setReference] = useState(error.digest);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,48 +1,24 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { getLocale } from "next-intl/server";
|
||||
import { SUPPORTED_LOCALES } from "@/i18n/locales";
|
||||
import { readCmsTranslation } from "@/lib/cms-translations";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { TranslationsClient } from "../translations-client";
|
||||
|
||||
const LOCALES = [
|
||||
"en",
|
||||
"it",
|
||||
"nl",
|
||||
"de",
|
||||
"fr",
|
||||
"es",
|
||||
"pt",
|
||||
"pl",
|
||||
"sv",
|
||||
"tr",
|
||||
"ro",
|
||||
"hu",
|
||||
"cs",
|
||||
"sk",
|
||||
"da",
|
||||
"no",
|
||||
"el",
|
||||
"bg",
|
||||
"hr",
|
||||
"sr",
|
||||
"uk",
|
||||
"ru",
|
||||
] as const;
|
||||
|
||||
export default async function CmsTranslationsPage() {
|
||||
const messagesDir = path.join(process.cwd(), "src", "messages");
|
||||
|
||||
const contents: Record<string, string> = {};
|
||||
await Promise.all(
|
||||
LOCALES.map(async (locale) => {
|
||||
try {
|
||||
contents[locale] = await fs.readFile(
|
||||
path.join(messagesDir, `${locale}.json`),
|
||||
"utf-8",
|
||||
);
|
||||
} catch {
|
||||
// Skip missing locale files instead of failing the whole page.
|
||||
}
|
||||
}),
|
||||
const [{ session, permissions }, locale, entries] = await Promise.all([
|
||||
getAdminContext(),
|
||||
getLocale(),
|
||||
Promise.all(
|
||||
SUPPORTED_LOCALES.map(
|
||||
async (locale) => [locale, await readCmsTranslation(locale)] as const,
|
||||
),
|
||||
),
|
||||
]);
|
||||
return (
|
||||
<TranslationsClient
|
||||
snapshots={Object.fromEntries(entries)}
|
||||
initialLocale={locale}
|
||||
canEdit={canAccess(permissions, PERMS.SETTINGS_EDIT, session.user.rank)}
|
||||
/>
|
||||
);
|
||||
|
||||
return <TranslationsClient localeContents={contents} />;
|
||||
}
|
||||
@@ -1,132 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import { Download, Save } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useMemo, useState } from "react";
|
||||
import { saveTranslations } from "@/actions/translations";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useServerAction } from "@/hooks/use-server-action";
|
||||
import { flattenObject, unflattenObject } from "@/lib/translations-utils";
|
||||
|
||||
type Locale =
|
||||
| "en"
|
||||
| "it"
|
||||
| "nl"
|
||||
| "de"
|
||||
| "fr"
|
||||
| "es"
|
||||
| "pt"
|
||||
| "pl"
|
||||
| "sv"
|
||||
| "tr"
|
||||
| "ro"
|
||||
| "hu"
|
||||
| "cs"
|
||||
| "sk"
|
||||
| "da"
|
||||
| "no"
|
||||
| "el"
|
||||
| "bg"
|
||||
| "hr"
|
||||
| "sr"
|
||||
| "uk"
|
||||
| "ru";
|
||||
|
||||
const LOCALES: { code: Locale; label: string }[] = [
|
||||
{ code: "en", label: "EN" },
|
||||
{ code: "it", label: "IT" },
|
||||
{ code: "nl", label: "NL" },
|
||||
{ code: "de", label: "DE" },
|
||||
{ code: "fr", label: "FR" },
|
||||
{ code: "es", label: "ES" },
|
||||
{ code: "pt", label: "PT" },
|
||||
{ code: "pl", label: "PL" },
|
||||
{ code: "sv", label: "SV" },
|
||||
{ code: "tr", label: "TR" },
|
||||
{ code: "ro", label: "RO" },
|
||||
{ code: "hu", label: "HU" },
|
||||
{ code: "cs", label: "CS" },
|
||||
{ code: "sk", label: "SK" },
|
||||
{ code: "da", label: "DA" },
|
||||
{ code: "no", label: "NO" },
|
||||
{ code: "el", label: "EL" },
|
||||
{ code: "bg", label: "BG" },
|
||||
{ code: "hr", label: "HR" },
|
||||
{ code: "sr", label: "SR" },
|
||||
{ code: "uk", label: "UK" },
|
||||
{ code: "ru", label: "RU" },
|
||||
];
|
||||
import { useUnsavedChanges } from "@/hooks/use-unsaved-changes";
|
||||
import {
|
||||
type AppLocale,
|
||||
isSupportedLocale,
|
||||
SUPPORTED_LOCALES,
|
||||
} from "@/i18n/locales";
|
||||
import { validateCmsMessage } from "@/lib/cms-message-validation";
|
||||
import type { CmsTranslationSnapshot } from "@/lib/cms-translations";
|
||||
import { unflattenObject } from "@/lib/translations-utils";
|
||||
|
||||
interface Props {
|
||||
localeContents: Record<string, string>;
|
||||
snapshots: Record<string, CmsTranslationSnapshot>;
|
||||
initialLocale: string;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
export function TranslationsClient({ localeContents }: Props) {
|
||||
const [data, setData] = useState<Record<string, Record<string, string>>>(
|
||||
() => {
|
||||
const initial: Record<string, Record<string, string>> = {};
|
||||
for (const [locale, content] of Object.entries(localeContents)) {
|
||||
initial[locale] = flattenObject(JSON.parse(content));
|
||||
}
|
||||
return initial;
|
||||
},
|
||||
const PER_PAGE = 30;
|
||||
export function TranslationsClient({
|
||||
snapshots,
|
||||
initialLocale,
|
||||
canEdit,
|
||||
}: Props) {
|
||||
const t = useTranslations("pages.admin.translations.cms");
|
||||
const router = useRouter();
|
||||
const [saved, setSaved] = useState(snapshots);
|
||||
const [drafts, setDrafts] = useState<Record<string, Record<string, string>>>(
|
||||
{},
|
||||
);
|
||||
const [activeLang, setActiveLang] = useState<AppLocale>(
|
||||
isSupportedLocale(initialLocale) ? initialLocale : "en",
|
||||
);
|
||||
const [search, setSearch] = useState("");
|
||||
const [group, setGroup] = useState("");
|
||||
const [activeLang, setActiveLang] = useState<Locale>("en");
|
||||
const { run, isPending } = useServerAction();
|
||||
const PER_PAGE = 30;
|
||||
const [status, setStatus] = useState("all");
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const activeData = data[activeLang];
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const g = new Set<string>();
|
||||
Object.keys(data.en ?? {}).forEach((k) => {
|
||||
const parts = k.split(".");
|
||||
if (parts.length > 1) g.add(parts[0]);
|
||||
});
|
||||
return Array.from(g).sort();
|
||||
}, [data]);
|
||||
|
||||
const filteredKeys = useMemo(() => {
|
||||
if (!activeData) return [];
|
||||
return Object.keys(activeData).filter((key) => {
|
||||
const matchSearch =
|
||||
!search ||
|
||||
key.toLowerCase().includes(search.toLowerCase()) ||
|
||||
activeData[key].toLowerCase().includes(search.toLowerCase());
|
||||
const matchGroup = !group || key.startsWith(`${group}.`);
|
||||
return matchSearch && matchGroup;
|
||||
const { run, isPending } = useServerAction();
|
||||
const source = saved.en.messages;
|
||||
const activeData = saved[activeLang].messages;
|
||||
const draft = drafts[activeLang] ?? {};
|
||||
const keys = useMemo(() => Object.keys(source).sort(), [source]);
|
||||
const groups = useMemo(
|
||||
() => [...new Set(keys.map((key) => key.split(".")[0]))],
|
||||
[keys],
|
||||
);
|
||||
const dirty = Object.values(drafts).some(
|
||||
(values) => Object.keys(values).length > 0,
|
||||
);
|
||||
useUnsavedChanges(dirty, t("leaveWarning"));
|
||||
const valueFor = (key: string) => draft[key] ?? activeData[key] ?? "";
|
||||
const missing = keys.filter((key) => !valueFor(key).trim()).length;
|
||||
const identical =
|
||||
activeLang === "en"
|
||||
? 0
|
||||
: keys.filter((key) => valueFor(key) === source[key]).length;
|
||||
const errors: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(draft)) {
|
||||
const issue = validateCmsMessage(value, source[key]);
|
||||
if (issue) errors[key] = t(issue, { key });
|
||||
}
|
||||
const filteredKeys = keys.filter((key) => {
|
||||
const value = valueFor(key);
|
||||
const matchesSearch = [key, value, source[key]].some((text) =>
|
||||
text.toLocaleLowerCase().includes(search.toLocaleLowerCase()),
|
||||
);
|
||||
const matchesStatus =
|
||||
status === "all" ||
|
||||
(status === "missing" && !activeData[key]?.trim()) ||
|
||||
(status === "identical" &&
|
||||
activeLang !== "en" &&
|
||||
activeData[key] === source[key]) ||
|
||||
(status === "modified" && Object.hasOwn(draft, key));
|
||||
return (
|
||||
matchesSearch && matchesStatus && (!group || key.startsWith(`${group}.`))
|
||||
);
|
||||
});
|
||||
const totalPages = Math.max(1, Math.ceil(filteredKeys.length / PER_PAGE));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
const pageKeys = filteredKeys.slice(
|
||||
(currentPage - 1) * PER_PAGE,
|
||||
currentPage * PER_PAGE,
|
||||
);
|
||||
function updateValue(key: string, value: string) {
|
||||
setDrafts((current) => {
|
||||
const next = { ...current[activeLang] };
|
||||
if (value === (activeData[key] ?? "")) delete next[key];
|
||||
else next[key] = value;
|
||||
return { ...current, [activeLang]: next };
|
||||
});
|
||||
}, [activeData, search, group]);
|
||||
|
||||
const totalPages = Math.ceil(filteredKeys.length / PER_PAGE);
|
||||
const pageKeys = filteredKeys.slice((page - 1) * PER_PAGE, page * PER_PAGE);
|
||||
|
||||
function updateValue(key: string, value: string) {
|
||||
setData({ ...data, [activeLang]: { ...activeData, [key]: value } });
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
if (!canEdit || !Object.keys(draft).length || Object.keys(errors).length)
|
||||
return;
|
||||
const locale = activeLang;
|
||||
run(
|
||||
() =>
|
||||
saveTranslations({
|
||||
locale: activeLang,
|
||||
data: unflattenObject(activeData),
|
||||
locale,
|
||||
revision: saved[locale].revision,
|
||||
changes: draft,
|
||||
}),
|
||||
{
|
||||
successMessage: "Translations saved!",
|
||||
successMessage: t("saved"),
|
||||
onSuccess: (result) => {
|
||||
const snapshot = result.snapshot as CmsTranslationSnapshot;
|
||||
setSaved((current) => ({ ...current, [locale]: snapshot }));
|
||||
setDrafts((current) => ({ ...current, [locale]: {} }));
|
||||
router.refresh();
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
const blob = new Blob(
|
||||
[JSON.stringify(unflattenObject(activeData), null, 2)],
|
||||
{
|
||||
type: "application/json",
|
||||
},
|
||||
[JSON.stringify(unflattenObject({ ...activeData, ...draft }), null, 2)],
|
||||
{ type: "application/json" },
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
@@ -135,106 +130,213 @@ export function TranslationsClient({ localeContents }: Props) {
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-end">
|
||||
<div className="flex gap-2">
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">{t("title")}</h1>
|
||||
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">
|
||||
{t("description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" onClick={handleExport}>
|
||||
<Download className="w-4 h-4 mr-1" /> Export
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave} disabled={isPending}>
|
||||
<Save className="w-4 h-4 mr-1" /> {isPending ? "Saving..." : "Save"}
|
||||
<Download className="mr-1 h-4 w-4" />
|
||||
{t("export")}
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={
|
||||
isPending ||
|
||||
!Object.keys(draft).length ||
|
||||
!!Object.keys(errors).length
|
||||
}
|
||||
>
|
||||
<Save className="mr-1 h-4 w-4" />
|
||||
{isPending
|
||||
? t("saving")
|
||||
: t("saveLocale", { locale: activeLang.toUpperCase() })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<div className="flex gap-1 overflow-x-auto pb-1 max-w-full">
|
||||
{LOCALES.map(({ code, label }) => (
|
||||
{!canEdit && (
|
||||
<p className="text-sm text-muted-foreground">{t("readOnly")}</p>
|
||||
)}
|
||||
<fieldset disabled={isPending} className="min-w-0 space-y-4">
|
||||
<fieldset
|
||||
className="flex min-w-0 flex-wrap gap-1"
|
||||
aria-label={t("language")}
|
||||
>
|
||||
{SUPPORTED_LOCALES.map((code) => (
|
||||
<Button
|
||||
key={code}
|
||||
variant={activeLang === code ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
variant={activeLang === code ? "default" : "outline"}
|
||||
aria-pressed={activeLang === code}
|
||||
onClick={() => {
|
||||
setActiveLang(code);
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
{code.toUpperCase()}
|
||||
{Object.keys(drafts[code] ?? {}).length ? " *" : ""}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Filter by key or value..."
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
className="max-w-xs"
|
||||
/>
|
||||
<select
|
||||
className="rounded-md border bg-background px-3 py-1 text-sm"
|
||||
value={group}
|
||||
onChange={(e) => {
|
||||
setGroup(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
<option value="">All groups</option>
|
||||
{groups.map((g) => (
|
||||
<option key={g} value={g}>
|
||||
{g}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="text-sm text-muted-foreground self-center">
|
||||
{filteredKeys.length} keys
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border bg-card divide-y">
|
||||
{pageKeys.map((key) => (
|
||||
<div key={key} className="flex items-center gap-3 px-4 py-2">
|
||||
<span className="text-xs font-mono text-muted-foreground w-64 truncate flex-shrink-0">
|
||||
{key}
|
||||
</span>
|
||||
</fieldset>
|
||||
<p className="text-sm" role="status">
|
||||
{t("coverage", { total: keys.length, missing, identical })}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
<label
|
||||
htmlFor="cms-translation-search"
|
||||
className="grid w-full min-w-0 gap-1 text-sm md:w-auto md:flex-1"
|
||||
>
|
||||
{t("search")}
|
||||
<Input
|
||||
value={activeData[key]}
|
||||
onChange={(e) => updateValue(key, e.target.value)}
|
||||
className="flex-1 h-8 text-sm"
|
||||
id="cms-translation-search"
|
||||
type="search"
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
placeholder={t("searchPlaceholder")}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid gap-1 text-sm">
|
||||
{t("group")}
|
||||
<select
|
||||
value={group}
|
||||
onChange={(e) => {
|
||||
setGroup(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
className="rounded-md border bg-background px-3 py-2"
|
||||
>
|
||||
<option value="">{t("allGroups")}</option>
|
||||
{groups.map((g) => (
|
||||
<option key={g} value={g}>
|
||||
{g}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="grid gap-1 text-sm">
|
||||
{t("status")}
|
||||
<select
|
||||
value={status}
|
||||
onChange={(e) => {
|
||||
setStatus(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
className="rounded-md border bg-background px-3 py-2"
|
||||
>
|
||||
<option value="all">{t("all")}</option>
|
||||
<option value="missing">{t("missing")}</option>
|
||||
{activeLang !== "en" && (
|
||||
<option value="identical">{t("identical")}</option>
|
||||
)}
|
||||
<option value="modified">{t("modified")}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t("fallbackHint")}</p>
|
||||
{Object.keys(draft).length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className="text-sm">
|
||||
{t("unsaved", { count: Object.keys(draft).length })}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (window.confirm(t("discardConfirm")))
|
||||
setDrafts((current) => ({ ...current, [activeLang]: {} }));
|
||||
}}
|
||||
>
|
||||
{t("discard")}
|
||||
</Button>
|
||||
{Object.keys(errors).length > 0 && (
|
||||
<span role="alert" className="text-sm text-destructive">
|
||||
{t("fixErrors", { count: Object.keys(errors).length })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between">
|
||||
)}
|
||||
<div className="divide-y rounded-lg border bg-card">
|
||||
{pageKeys.length === 0 && (
|
||||
<p className="p-6 text-sm text-muted-foreground">
|
||||
{t("noResults")}
|
||||
</p>
|
||||
)}
|
||||
{pageKeys.map((key) => (
|
||||
<div key={key} className="grid min-w-0 gap-2 p-4">
|
||||
<label
|
||||
htmlFor={`translation-${key}`}
|
||||
className="break-all font-mono text-xs text-muted-foreground"
|
||||
>
|
||||
{key}
|
||||
</label>
|
||||
{activeLang !== "en" && (
|
||||
<p className="whitespace-pre-wrap break-words text-sm">
|
||||
<span className="font-medium">{t("source")}: </span>
|
||||
{source[key]}
|
||||
</p>
|
||||
)}
|
||||
<textarea
|
||||
id={`translation-${key}`}
|
||||
aria-invalid={!!errors[key]}
|
||||
aria-describedby={errors[key] ? `error-${key}` : undefined}
|
||||
readOnly={!canEdit}
|
||||
value={valueFor(key)}
|
||||
onChange={(e) => updateValue(key, e.target.value)}
|
||||
placeholder={t("missingPlaceholder")}
|
||||
rows={2}
|
||||
className="w-full min-w-0 resize-y rounded-md border bg-background px-3 py-2 text-sm"
|
||||
/>
|
||||
{errors[key] && (
|
||||
<p
|
||||
id={`error-${key}`}
|
||||
role="alert"
|
||||
className="break-words text-sm text-destructive"
|
||||
>
|
||||
{errors[key]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Page {page} of {totalPages}
|
||||
{t("page", {
|
||||
page: currentPage,
|
||||
total: totalPages,
|
||||
count: filteredKeys.length,
|
||||
})}
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page <= 1}
|
||||
onClick={() => setPage(page - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
onClick={() => setPage(currentPage - 1)}
|
||||
>
|
||||
Prev
|
||||
{t("previous")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage(page + 1)}
|
||||
disabled={currentPage >= totalPages}
|
||||
onClick={() => setPage(currentPage + 1)}
|
||||
>
|
||||
Next
|
||||
{t("next")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import { parse } from "@formatjs/icu-messageformat-parser";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { flattenObject } from "@/lib/translations-utils";
|
||||
|
||||
describe("CMS message catalogs", () => {
|
||||
it("contains valid ICU messages in every shipped locale", async () => {
|
||||
const errors: string[] = [];
|
||||
for (const file of await readdir("src/messages")) {
|
||||
if (!file.endsWith(".json")) continue;
|
||||
const messages = flattenObject(
|
||||
JSON.parse(await readFile(`src/messages/${file}`, "utf8")),
|
||||
);
|
||||
for (const [key, value] of Object.entries(messages)) {
|
||||
try {
|
||||
parse(value);
|
||||
} catch {
|
||||
errors.push(`${file}: ${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
it("provides actual text for the search result headings and dashboard counts", async () => {
|
||||
const { createTranslator } = await import("next-intl");
|
||||
const messages = JSON.parse(await readFile("src/messages/en.json", "utf8"));
|
||||
const errors: unknown[] = [];
|
||||
const t = createTranslator({
|
||||
locale: "en",
|
||||
messages,
|
||||
onError: (error) => errors.push(error),
|
||||
});
|
||||
expect(t("pages.admin.dashboard.totalAccounts", { count: 12 })).toContain(
|
||||
"12",
|
||||
);
|
||||
expect(t("pages.admin.search.noResults")).not.toContain("pages.");
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
export const SUPPORTED_LOCALES = [
|
||||
"en",
|
||||
"it",
|
||||
"nl",
|
||||
"de",
|
||||
"fr",
|
||||
"es",
|
||||
"pt",
|
||||
"pl",
|
||||
"sv",
|
||||
"tr",
|
||||
"ro",
|
||||
"hu",
|
||||
"cs",
|
||||
"sk",
|
||||
"da",
|
||||
"no",
|
||||
"el",
|
||||
"bg",
|
||||
"hr",
|
||||
"sr",
|
||||
"uk",
|
||||
"ru",
|
||||
] as const;
|
||||
export type AppLocale = (typeof SUPPORTED_LOCALES)[number];
|
||||
export const DEFAULT_LOCALE: AppLocale = "en";
|
||||
|
||||
export function isSupportedLocale(
|
||||
value: string | undefined,
|
||||
): value is AppLocale {
|
||||
return !!value && (SUPPORTED_LOCALES as readonly string[]).includes(value);
|
||||
}
|
||||
+12
-39
@@ -1,41 +1,17 @@
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { getRequestConfig } from "next-intl/server";
|
||||
import { loadCmsMessages } from "@/lib/cms-translations";
|
||||
import { mergeMessages } from "./merge-messages";
|
||||
|
||||
// i18n WITHOUT routing: the locale is chosen by a `NEXT_LOCALE` cookie, then
|
||||
// the Accept-Language header, and finally English as fallback.
|
||||
export const SUPPORTED_LOCALES = [
|
||||
"en",
|
||||
"it",
|
||||
"nl",
|
||||
"de",
|
||||
"fr",
|
||||
"es",
|
||||
"pt",
|
||||
"pl",
|
||||
"sv",
|
||||
"tr",
|
||||
"ro",
|
||||
"hu",
|
||||
"cs",
|
||||
"sk",
|
||||
"da",
|
||||
"no",
|
||||
"el",
|
||||
"bg",
|
||||
"hr",
|
||||
"sr",
|
||||
"uk",
|
||||
"ru",
|
||||
] as const;
|
||||
export type AppLocale = (typeof SUPPORTED_LOCALES)[number];
|
||||
export const DEFAULT_LOCALE: AppLocale = "en";
|
||||
export {
|
||||
DEFAULT_LOCALE,
|
||||
isSupportedLocale,
|
||||
SUPPORTED_LOCALES,
|
||||
} from "./locales";
|
||||
|
||||
export function isSupportedLocale(
|
||||
value: string | undefined,
|
||||
): value is AppLocale {
|
||||
return !!value && (SUPPORTED_LOCALES as readonly string[]).includes(value);
|
||||
}
|
||||
import { type AppLocale, DEFAULT_LOCALE, isSupportedLocale } from "./locales";
|
||||
|
||||
function detectLocaleFromAcceptLanguage(
|
||||
acceptLanguage: string | null,
|
||||
@@ -73,14 +49,10 @@ export default getRequestConfig(async () => {
|
||||
locale = detected ?? DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
// Safe: `locale` is validated against SUPPORTED_LOCALES above (only en/it/nl/de/fr/es).
|
||||
const messages = (await import(`../messages/${locale}.json`)).default;
|
||||
// English is the source of truth; fall back to it for any key missing from a
|
||||
// translation so the UI never shows a raw key path.
|
||||
const fallback =
|
||||
locale === DEFAULT_LOCALE
|
||||
? messages
|
||||
: (await import(`../messages/${DEFAULT_LOCALE}.json`)).default;
|
||||
const [messages, fallback] = await Promise.all([
|
||||
loadCmsMessages(locale),
|
||||
loadCmsMessages(DEFAULT_LOCALE),
|
||||
]);
|
||||
|
||||
return {
|
||||
locale,
|
||||
@@ -108,6 +80,7 @@ export default getRequestConfig(async () => {
|
||||
if (typeof fromEnglish === "string") return fromEnglish;
|
||||
const seg = key.split(".").pop() ?? key;
|
||||
return seg
|
||||
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
||||
.replace(/[._-]/g, " ")
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
},
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
type MessageFormatElement,
|
||||
parse,
|
||||
TYPE,
|
||||
} from "@formatjs/icu-messageformat-parser";
|
||||
|
||||
/** Argument names and rich-text tags are a contract with the calling component. */
|
||||
export function messageVariables(value: string): string[] {
|
||||
const names = new Set<string>();
|
||||
function visit(nodes: MessageFormatElement[]) {
|
||||
for (const node of nodes) {
|
||||
if (node.type !== TYPE.literal && node.type !== TYPE.pound)
|
||||
names.add(node.value);
|
||||
if (node.type === TYPE.select || node.type === TYPE.plural) {
|
||||
for (const option of Object.values(node.options)) visit(option.value);
|
||||
}
|
||||
if (node.type === TYPE.tag) visit(node.children);
|
||||
}
|
||||
}
|
||||
visit(parse(value));
|
||||
return [...names].sort();
|
||||
}
|
||||
export function validateCmsMessage(
|
||||
value: string,
|
||||
source: string,
|
||||
): "empty" | "invalid" | "variables" | null {
|
||||
if (!value.trim()) return "empty";
|
||||
try {
|
||||
return messageVariables(value).join("\0") ===
|
||||
messageVariables(source).join("\0")
|
||||
? null
|
||||
: "variables";
|
||||
} catch {
|
||||
return "invalid";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { readCmsTranslation, saveCmsTranslation } from "./cms-translations";
|
||||
|
||||
const directories: string[] = [];
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
directories
|
||||
.splice(0)
|
||||
.map((dir) => rm(dir, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
async function root() {
|
||||
const dir = await mkdtemp(path.join(os.tmpdir(), "cms-translations-test-"));
|
||||
directories.push(dir);
|
||||
return dir;
|
||||
}
|
||||
describe("CMS translation persistence", () => {
|
||||
it("reads bundled messages without source files and persists only overrides", async () => {
|
||||
const dir = await root();
|
||||
const before = await readCmsTranslation("it", dir);
|
||||
expect(before.messages["pages.admin.search.users"]).toBe("Utenti");
|
||||
await saveCmsTranslation(
|
||||
"it",
|
||||
before.revision,
|
||||
{ "pages.admin.search.users": "Persone" },
|
||||
dir,
|
||||
);
|
||||
expect(
|
||||
(await readCmsTranslation("it", dir)).messages[
|
||||
"pages.admin.search.users"
|
||||
],
|
||||
).toBe("Persone");
|
||||
expect(
|
||||
JSON.parse(await readFile(path.join(dir, "it.json"), "utf8")),
|
||||
).toEqual({ "pages.admin.search.users": "Persone" });
|
||||
});
|
||||
it("rejects stale saves and invalid ICU without overwriting saved text", async () => {
|
||||
const dir = await root();
|
||||
const before = await readCmsTranslation("it", dir);
|
||||
await saveCmsTranslation(
|
||||
"it",
|
||||
before.revision,
|
||||
{ "pages.admin.search.users": "Persone" },
|
||||
dir,
|
||||
);
|
||||
await expect(
|
||||
saveCmsTranslation(
|
||||
"it",
|
||||
before.revision,
|
||||
{ "pages.admin.search.users": "Stale" },
|
||||
dir,
|
||||
),
|
||||
).rejects.toThrow("conflict");
|
||||
const current = await readCmsTranslation("it", dir);
|
||||
await expect(
|
||||
saveCmsTranslation(
|
||||
"it",
|
||||
current.revision,
|
||||
{ "pages.admin.dashboard.totalAccounts": "{broken" },
|
||||
dir,
|
||||
),
|
||||
).rejects.toThrow("invalid");
|
||||
await expect(
|
||||
saveCmsTranslation(
|
||||
"it",
|
||||
current.revision,
|
||||
{ "pages.admin.dashboard.totalAccounts": "No variable" },
|
||||
dir,
|
||||
),
|
||||
).rejects.toThrow("variables");
|
||||
expect((await readCmsTranslation("it", dir)).revision).toBe(
|
||||
current.revision,
|
||||
);
|
||||
});
|
||||
it("restores the bundled value when an override is reset", async () => {
|
||||
const dir = await root();
|
||||
const before = await readCmsTranslation("it", dir);
|
||||
const saved = await saveCmsTranslation(
|
||||
"it",
|
||||
before.revision,
|
||||
{ "pages.admin.search.users": "Persone" },
|
||||
dir,
|
||||
);
|
||||
const reset = await saveCmsTranslation(
|
||||
"it",
|
||||
saved.revision,
|
||||
{ "pages.admin.search.users": null },
|
||||
dir,
|
||||
);
|
||||
expect(reset.messages["pages.admin.search.users"]).toBe("Utenti");
|
||||
});
|
||||
it("rejects unknown message keys and does not mutate prototypes", async () => {
|
||||
const dir = await root();
|
||||
const before = await readCmsTranslation("it", dir);
|
||||
await expect(
|
||||
saveCmsTranslation(
|
||||
"it",
|
||||
before.revision,
|
||||
{ "__proto__.polluted": "yes" },
|
||||
dir,
|
||||
),
|
||||
).rejects.toThrow("unknownKey");
|
||||
expect(Object.prototype).not.toHaveProperty("polluted");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
import "server-only";
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { cache } from "react";
|
||||
import { type AppLocale, isSupportedLocale } from "@/i18n/locales";
|
||||
import type { MessageTree } from "@/i18n/merge-messages";
|
||||
import { validateCmsMessage } from "./cms-message-validation";
|
||||
import { logger } from "./logger";
|
||||
import {
|
||||
withGamedataLock,
|
||||
writeGamedataJsonAtomic,
|
||||
} from "./services/import/core/gamedata-json";
|
||||
import { flattenObject, unflattenObject } from "./translations-utils";
|
||||
|
||||
const defaultRoot = path.join(
|
||||
/*turbopackIgnore: true*/ process.cwd(),
|
||||
"storage",
|
||||
"cms-translations",
|
||||
);
|
||||
export interface CmsTranslationSnapshot {
|
||||
messages: Record<string, string>;
|
||||
revision: string;
|
||||
}
|
||||
export class CmsTranslationError extends Error {
|
||||
constructor(
|
||||
public readonly code:
|
||||
| "conflict"
|
||||
| "unknownKey"
|
||||
| "empty"
|
||||
| "invalid"
|
||||
| "variables",
|
||||
public readonly key = "",
|
||||
) {
|
||||
super(`${code}: ${key}`);
|
||||
}
|
||||
}
|
||||
async function bundled(locale: AppLocale): Promise<Record<string, string>> {
|
||||
return flattenObject((await import(`../messages/${locale}.json`)).default);
|
||||
}
|
||||
async function readOverrides(file: string): Promise<Record<string, string>> {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(
|
||||
await fs.readFile(/*turbopackIgnore: true*/ file, "utf8"),
|
||||
);
|
||||
if (
|
||||
!parsed ||
|
||||
typeof parsed !== "object" ||
|
||||
Array.isArray(parsed) ||
|
||||
Object.values(parsed).some((v) => typeof v !== "string")
|
||||
)
|
||||
throw new Error("Invalid CMS translation override file");
|
||||
return parsed as Record<string, string>;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function localeFile(locale: AppLocale, root: string) {
|
||||
if (!isSupportedLocale(locale)) throw new Error("Unsupported locale");
|
||||
return path.join(root, `${locale}.json`);
|
||||
}
|
||||
export async function readCmsTranslation(
|
||||
locale: AppLocale,
|
||||
root = defaultRoot,
|
||||
): Promise<CmsTranslationSnapshot> {
|
||||
const [base, source, overrides] = await Promise.all([
|
||||
bundled(locale),
|
||||
bundled("en"),
|
||||
readOverrides(localeFile(locale, root)),
|
||||
]);
|
||||
const messages = { ...base };
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
// Obsolete or incompatible overrides must not break new releases.
|
||||
if (!Object.hasOwn(source, key) || validateCmsMessage(value, source[key]))
|
||||
continue;
|
||||
messages[key] = value;
|
||||
}
|
||||
return {
|
||||
messages,
|
||||
revision: createHash("sha256")
|
||||
.update(JSON.stringify([base, overrides]))
|
||||
.digest("hex"),
|
||||
};
|
||||
}
|
||||
export async function saveCmsTranslation(
|
||||
locale: AppLocale,
|
||||
revision: string,
|
||||
changes: Record<string, string | null>,
|
||||
root = defaultRoot,
|
||||
): Promise<CmsTranslationSnapshot> {
|
||||
const file = localeFile(locale, root);
|
||||
await fs.mkdir(root, { recursive: true });
|
||||
return withGamedataLock(file, async () => {
|
||||
const current = await readCmsTranslation(locale, root);
|
||||
if (current.revision !== revision)
|
||||
throw new CmsTranslationError("conflict");
|
||||
const [base, source, overrides] = await Promise.all([
|
||||
bundled(locale),
|
||||
bundled("en"),
|
||||
readOverrides(file),
|
||||
]);
|
||||
for (const [key, value] of Object.entries(changes)) {
|
||||
if (!Object.hasOwn(source, key))
|
||||
throw new CmsTranslationError("unknownKey", key);
|
||||
if (value === null || value === base[key]) {
|
||||
delete overrides[key];
|
||||
continue;
|
||||
}
|
||||
const issue = validateCmsMessage(value, source[key]);
|
||||
if (issue) throw new CmsTranslationError(issue, key);
|
||||
overrides[key] = value;
|
||||
}
|
||||
await writeGamedataJsonAtomic(file, overrides);
|
||||
return readCmsTranslation(locale, root);
|
||||
});
|
||||
}
|
||||
/** Per-request deduplication; runtime edits are visible on the next request. */
|
||||
export const loadCmsMessages = cache(
|
||||
async (locale: AppLocale): Promise<MessageTree> => {
|
||||
try {
|
||||
return unflattenObject(
|
||||
(await readCmsTranslation(locale)).messages,
|
||||
) as MessageTree;
|
||||
} catch (error) {
|
||||
logger.error("CMS translation overrides could not be loaded", {
|
||||
locale,
|
||||
error,
|
||||
});
|
||||
return (await import(`../messages/${locale}.json`)).default;
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1708,7 +1708,7 @@
|
||||
"noNowPlayingHint": "няма зададен API за игра в момента",
|
||||
"noListenersHint": "няма зададен API за слушатели",
|
||||
"endpointDiagnostics": "Диагностика на крайна точка",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">ОК</span> достъпно · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]\">WARN</span> не е конфигуриран · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">ГРЕШКА</span> извличането е неуспешно / изтече времето за изчакване.",
|
||||
"diagnosticsLegend": "OK: достъпно · WARN: не е конфигурирано · ERROR: неуспешна заявка или изтекло време.",
|
||||
"streamUrl": "URL адрес на потока",
|
||||
"unreachable": "недостижим",
|
||||
"refresh": "Опресняване"
|
||||
@@ -1767,7 +1767,7 @@
|
||||
"noStreamUrlText": "Няма зададен radio_stream_url, така че все още няма нищо за вграждане.",
|
||||
"goToSettings": "Отидете на Радио настройки",
|
||||
"livePreview": "Преглед на живо",
|
||||
"livePreviewText": "Това е голият <audio> плейър, както ще го видят посетителите. Натиснете възпроизвеждане, за да потвърдите, че потокът е достъпен.",
|
||||
"livePreviewText": "Това е голият audio плейър, както ще го видят посетителите. Натиснете възпроизвеждане, за да потвърдите, че потокът е достъпен.",
|
||||
"source": "Източник",
|
||||
"audioSnippet": "Фрагмент от аудио плейър",
|
||||
"audioSnippetDesc": "Вмъкващ се HTML5 аудио плейър, насочен право към потока. Работи навсякъде, където е разрешен обикновен HTML.",
|
||||
@@ -1998,7 +1998,7 @@
|
||||
"title": "Сигнали",
|
||||
"subtitle": "Изпращайте сигнали за целия хотел и вижте последните сигнали",
|
||||
"sendAlert": "Изпратете сигнал за хотел",
|
||||
"recentAlerts": "Последни сигнали",
|
||||
"recentAlerts": "Последни сигнали ({count})",
|
||||
"noRecentAlerts": "Няма скорошни сигнали",
|
||||
"form": {
|
||||
"title": "Заглавие на предупреждение",
|
||||
@@ -2209,7 +2209,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Импортиране Разрешения",
|
||||
"importHint": "Paste a JSON array of разрешение objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of разрешение objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Импортиране",
|
||||
"exportTitle": "Export Разрешения",
|
||||
@@ -2346,7 +2346,7 @@
|
||||
"presetsTitle": "Предварително зададени теми",
|
||||
"presetsSubtitle": "Изберете предварително изградена цветова схема",
|
||||
"applyPreset": "Кандидатствайте",
|
||||
"presetApplied": "Предварителната настройка е приложена",
|
||||
"presetApplied": "Предварителната настройка е приложена: {preset}",
|
||||
"customiseTitle": "Персонализирани цветове",
|
||||
"primary": "Първичен",
|
||||
"secondary": "Вторичен",
|
||||
|
||||
@@ -1708,7 +1708,7 @@
|
||||
"noNowPlayingHint": "žádná nyní přehrávající sada API",
|
||||
"noListenersHint": "není nastaveno žádné rozhraní API pro posluchače",
|
||||
"endpointDiagnostics": "Diagnostika koncových bodů",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> dosažitelné · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-primaryl]-1 bg-8 rounded text-[var(--color-text)]\">WARN</span> není nakonfigurováno · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">CHYBA</span> se nezdařilo / vypršel časový limit.",
|
||||
"diagnosticsLegend": "OK: dostupné · WARN: nenastaveno · ERROR: požadavek selhal nebo vypršel časový limit.",
|
||||
"streamUrl": "Adresa URL streamu",
|
||||
"unreachable": "nedosažitelný",
|
||||
"refresh": "Obnovit"
|
||||
@@ -1767,7 +1767,7 @@
|
||||
"noStreamUrlText": "Neexistuje žádná sada radio_stream_url, takže zatím není co vkládat.",
|
||||
"goToSettings": "Přejděte do Nastavení rádia",
|
||||
"livePreview": "Živý náhled",
|
||||
"livePreviewText": "Toto je holý <audio> přehrávač, jak jej uvidí návštěvníci. Stisknutím tlačítka Přehrát potvrďte, že je stream dosažitelný.",
|
||||
"livePreviewText": "Toto je holý audio přehrávač, jak jej uvidí návštěvníci. Stisknutím tlačítka Přehrát potvrďte, že je stream dosažitelný.",
|
||||
"source": "Zdroj",
|
||||
"audioSnippet": "Úryvek audio přehrávače",
|
||||
"audioSnippetDesc": "Zvukový přehrávač HTML5 namířil přímo na stream. Funguje všude, kde je povoleno prosté HTML.",
|
||||
@@ -1998,7 +1998,7 @@
|
||||
"title": "Upozornění",
|
||||
"subtitle": "Odesílejte upozornění pro celý hotel a prohlížejte si nedávná upozornění",
|
||||
"sendAlert": "Odeslat upozornění na hotel",
|
||||
"recentAlerts": "Nedávná upozornění",
|
||||
"recentAlerts": "Nedávná upozornění ({count})",
|
||||
"noRecentAlerts": "Žádná nedávná upozornění",
|
||||
"form": {
|
||||
"title": "Název upozornění",
|
||||
@@ -2209,7 +2209,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Importovat Oprávnění",
|
||||
"importHint": "Paste a JSON array of Povolení objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of Povolení objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Importovat",
|
||||
"exportTitle": "Export Oprávnění",
|
||||
@@ -2346,7 +2346,7 @@
|
||||
"presetsTitle": "Předvolby motivu",
|
||||
"presetsSubtitle": "Vyberte předem připravené barevné schéma",
|
||||
"applyPreset": "Použít",
|
||||
"presetApplied": "Předvolba použita",
|
||||
"presetApplied": "Předvolba použita: {preset}",
|
||||
"customiseTitle": "Vlastní barvy",
|
||||
"primary": "Primární",
|
||||
"secondary": "Sekundární",
|
||||
|
||||
@@ -1708,7 +1708,7 @@
|
||||
"noNowPlayingHint": "intet API-sæt, der nu spiller",
|
||||
"noListenersHint": "no listeners API set",
|
||||
"endpointDiagnostics": "Slutpunktsdiagnostik",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> tilgængelig · <span class=\"inline-block text-[0.72rem] font-bold px-2 rounded-full. bg-[var(--color-primary)]/18 text-[var(--color-text)]\">ADVARSEL</span> ikke konfigureret · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text) / 22 text-[var(--farve mislykket] OR.",
|
||||
"diagnosticsLegend": "OK: tilgængelig · WARN: ikke konfigureret · ERROR: anmodningen mislykkedes eller fik timeout.",
|
||||
"streamUrl": "Stream URL",
|
||||
"unreachable": "unreachable",
|
||||
"refresh": "Opdater"
|
||||
@@ -1767,7 +1767,7 @@
|
||||
"noStreamUrlText": "Der er ingen radio_stream_url sat, så der er intet at indlejre endnu.",
|
||||
"goToSettings": "Gå til Radioindstillinger",
|
||||
"livePreview": "Live preview",
|
||||
"livePreviewText": "Dette er den blottede <audio>-afspiller, som besøgende vil se den. Tryk på afspil for at bekræfte, at streamen er tilgængelig.",
|
||||
"livePreviewText": "Dette er den blottede audio-afspiller, som besøgende vil se den. Tryk på afspil for at bekræfte, at streamen er tilgængelig.",
|
||||
"source": "Kilde",
|
||||
"audioSnippet": "Uddrag af lydafspiller",
|
||||
"audioSnippetDesc": "Drop-in HTML5-lydafspiller pegede direkte mod strømmen. Virker overalt, hvor almindelig HTML er tilladt.",
|
||||
@@ -1998,7 +1998,7 @@
|
||||
"title": "Advarsler",
|
||||
"subtitle": "Send underretninger for hele hotellet, og se de seneste underretninger",
|
||||
"sendAlert": "Send hotelalarm",
|
||||
"recentAlerts": "Seneste underretninger",
|
||||
"recentAlerts": "Seneste underretninger ({count})",
|
||||
"noRecentAlerts": "No recent alerts",
|
||||
"form": {
|
||||
"title": "Alert title",
|
||||
@@ -2209,7 +2209,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Importer Tilladelser",
|
||||
"importHint": "Paste a JSON array of Tilladelse objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of Tilladelse objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Importer",
|
||||
"exportTitle": "Export Tilladelser",
|
||||
@@ -2346,7 +2346,7 @@
|
||||
"presetsTitle": "Forudindstillinger af tema",
|
||||
"presetsSubtitle": "Vælg et forudbygget farveskema",
|
||||
"applyPreset": "Ansøg",
|
||||
"presetApplied": "Forudindstilling anvendt",
|
||||
"presetApplied": "Forudindstilling anvendt: {preset}",
|
||||
"customiseTitle": "Brugerdefinerede farver",
|
||||
"primary": "Primær",
|
||||
"secondary": "Sekundær",
|
||||
|
||||
@@ -1679,7 +1679,7 @@
|
||||
"noNowPlayingHint": "no now-playing API set",
|
||||
"noListenersHint": "no listeners API set",
|
||||
"endpointDiagnostics": "Endpoint diagnostics",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> reachable · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]\">WARN</span> not configured · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">ERROR</span> fetch failed / timed out.",
|
||||
"diagnosticsLegend": "OK: erreichbar · WARN: nicht konfiguriert · ERROR: Anfrage fehlgeschlagen oder Zeitüberschreitung.",
|
||||
"streamUrl": "Stream URL",
|
||||
"unreachable": "unreachable",
|
||||
"refresh": "Aktualisieren"
|
||||
@@ -1738,7 +1738,7 @@
|
||||
"noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.",
|
||||
"goToSettings": "Go to Radio Settings",
|
||||
"livePreview": "Live preview",
|
||||
"livePreviewText": "This is the bare <audio> player as visitors will see it. Press play to confirm the stream is reachable.",
|
||||
"livePreviewText": "This is the bare audio player as visitors will see it. Press play to confirm the stream is reachable.",
|
||||
"source": "Source",
|
||||
"audioSnippet": "Audio player snippet",
|
||||
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
|
||||
@@ -1969,7 +1969,7 @@
|
||||
"title": "Alarme",
|
||||
"subtitle": "Hotelweite Alarme senden und aktuelle anzeigen",
|
||||
"sendAlert": "Hotel-Alarm senden",
|
||||
"recentAlerts": "Letzte Alarme",
|
||||
"recentAlerts": "Letzte Alarme ({count})",
|
||||
"noRecentAlerts": "Keine aktuellen Alarme",
|
||||
"form": {
|
||||
"title": "Alarm-Titel",
|
||||
@@ -2180,7 +2180,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Importieren Berechtigungen",
|
||||
"importHint": "Paste a JSON array of Berechtigung objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of Berechtigung objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Importieren",
|
||||
"exportTitle": "Export Berechtigungen",
|
||||
@@ -2317,7 +2317,7 @@
|
||||
"presetsTitle": "Design-Voreinstellungen",
|
||||
"presetsSubtitle": "Wähle ein vorgefertigtes Farbschema",
|
||||
"applyPreset": "Anwenden",
|
||||
"presetApplied": "Voreinstellung angewendet",
|
||||
"presetApplied": "Voreinstellung angewendet: {preset}",
|
||||
"customiseTitle": "Benutzerdefinierte Farben",
|
||||
"primary": "Primär",
|
||||
"secondary": "Sekundär",
|
||||
|
||||
@@ -1708,7 +1708,7 @@
|
||||
"noNowPlayingHint": "δεν υπάρχει σύνολο API που παίζει τώρα",
|
||||
"noListenersHint": "δεν έχει οριστεί API ακροατών",
|
||||
"endpointDiagnostics": "Διαγνωστικά τελικού σημείου",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">ΟΚ</span> προσβάσιμο · <span class=\"inline-block text-[0.72-rounded-5-0.72remful. bg-[var(--color-primary)]/18 text-[var(--color-text)]\">ΠΡΟΕΙΔΟΠΟΙΗΣΗ</span> δεν έχει διαμορφωθεί · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/2 text-[var(--color-text)]\">ΣΦΑΛΜΑ</span> η ανάκτηση απέτυχε / έληξε.",
|
||||
"diagnosticsLegend": "OK: διαθέσιμο · WARN: δεν έχει ρυθμιστεί · ERROR: αποτυχία αιτήματος ή λήξη χρονικού ορίου.",
|
||||
"streamUrl": "URL ροής",
|
||||
"unreachable": "απρόσιτος",
|
||||
"refresh": "Ανανέωση"
|
||||
@@ -1767,7 +1767,7 @@
|
||||
"noStreamUrlText": "Δεν υπάρχει set radio_stream_url, επομένως δεν υπάρχει τίποτα για ενσωμάτωση ακόμα.",
|
||||
"goToSettings": "Μεταβείτε στις Ρυθμίσεις ραδιοφώνου",
|
||||
"livePreview": "Ζωντανή προεπισκόπηση",
|
||||
"livePreviewText": "Αυτό είναι το γυμνό πρόγραμμα αναπαραγωγής <audio> όπως θα το δουν οι επισκέπτες. Πατήστε το play για να επιβεβαιώσετε ότι η ροή είναι προσβάσιμη.",
|
||||
"livePreviewText": "Αυτό είναι το γυμνό πρόγραμμα αναπαραγωγής audio όπως θα το δουν οι επισκέπτες. Πατήστε το play για να επιβεβαιώσετε ότι η ροή είναι προσβάσιμη.",
|
||||
"source": "Πηγή",
|
||||
"audioSnippet": "Απόσπασμα αναπαραγωγής ήχου",
|
||||
"audioSnippetDesc": "Το αναπτυσσόμενο πρόγραμμα αναπαραγωγής ήχου HTML5 κατευθείαν στη ροή. Λειτουργεί οπουδήποτε επιτρέπεται απλό HTML.",
|
||||
@@ -1998,7 +1998,7 @@
|
||||
"title": "Ειδοποιήσεις",
|
||||
"subtitle": "Στείλτε ειδοποιήσεις σε όλο το ξενοδοχείο και δείτε πρόσφατες ειδοποιήσεις",
|
||||
"sendAlert": "Αποστολή ειδοποίησης ξενοδοχείου",
|
||||
"recentAlerts": "Πρόσφατες ειδοποιήσεις",
|
||||
"recentAlerts": "Πρόσφατες ειδοποιήσεις ({count})",
|
||||
"noRecentAlerts": "Δεν υπάρχουν πρόσφατες ειδοποιήσεις",
|
||||
"form": {
|
||||
"title": "Τίτλος ειδοποίησης",
|
||||
@@ -2209,7 +2209,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Εισαγωγή Άδειες",
|
||||
"importHint": "Paste a JSON array of Άδεια objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of Άδεια objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Εισαγωγή",
|
||||
"exportTitle": "Export Άδειες",
|
||||
@@ -2346,7 +2346,7 @@
|
||||
"presetsTitle": "Προεπιλογές θεμάτων",
|
||||
"presetsSubtitle": "Επιλέξτε έναν προκατασκευασμένο συνδυασμό χρωμάτων",
|
||||
"applyPreset": "Εφαρμόστε",
|
||||
"presetApplied": "Εφαρμόστηκε η προεπιλογή",
|
||||
"presetApplied": "Εφαρμόστηκε η προεπιλογή: {preset}",
|
||||
"customiseTitle": "Προσαρμοσμένα χρώματα",
|
||||
"primary": "Πρωτοβάθμια",
|
||||
"secondary": "Δευτερεύουσα",
|
||||
|
||||
+268
-32
@@ -878,7 +878,8 @@
|
||||
"codePlaceholder": "6-digit code",
|
||||
"confirm": "Confirm",
|
||||
"enableIntro": "Add a second layer of security with an authenticator app.",
|
||||
"enable": "Enable 2FA"
|
||||
"enable": "Enable 2FA",
|
||||
"rateLimit": "Too many attempts. Please wait before trying again."
|
||||
},
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
@@ -1119,7 +1120,8 @@
|
||||
"rankWithCount": "Rank {rank}: {count} accounts",
|
||||
"recentStaffActivity": "Recent staff activity",
|
||||
"viewAll": "View all",
|
||||
"noStaffActivityLogged": "No staff activity logged yet"
|
||||
"noStaffActivityLogged": "No staff activity logged yet",
|
||||
"totalAccounts": "{count} total accounts"
|
||||
},
|
||||
"articles": {
|
||||
"title": "Articles",
|
||||
@@ -1160,7 +1162,11 @@
|
||||
"statusPublished": "Published",
|
||||
"publishAt": "Publish at",
|
||||
"publishedAt": "Published at"
|
||||
}
|
||||
},
|
||||
"articlesShown": "Articles shown",
|
||||
"latest": "Latest article",
|
||||
"mostRecentPost": "Most recent publication",
|
||||
"recentArticles": "Recent articles"
|
||||
},
|
||||
"events": {
|
||||
"newEvent": "+ New event",
|
||||
@@ -1281,7 +1287,13 @@
|
||||
"colColour": "Colour",
|
||||
"colUsedBy": "Used by",
|
||||
"colCreated": "Created",
|
||||
"colActions": "Actions"
|
||||
"colActions": "Actions",
|
||||
"databaseError": "Tags could not be loaded. Please try again.",
|
||||
"totalTags": "Total tags",
|
||||
"inUse": "In use",
|
||||
"attachedToContent": "Attached to content",
|
||||
"unused": "Unused",
|
||||
"allTags": "All tags"
|
||||
},
|
||||
"navigation": {
|
||||
"title": "Navigator",
|
||||
@@ -1305,7 +1317,12 @@
|
||||
"colName": "Name",
|
||||
"colOrder": "Order",
|
||||
"colRooms": "Rooms",
|
||||
"colActions": "Actions"
|
||||
"colActions": "Actions",
|
||||
"publicFlatcats": "Public room categories",
|
||||
"markedPublic": "Categories marked as public",
|
||||
"visibleRooms": "Visible rooms",
|
||||
"rows": "rows",
|
||||
"unnamed": "Unnamed room"
|
||||
},
|
||||
"helpCenter": {
|
||||
"title": "Help Center",
|
||||
@@ -1332,7 +1349,9 @@
|
||||
"saving": "Saving…",
|
||||
"deleteEntry": "Delete entry",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
},
|
||||
"withButton": "With a button",
|
||||
"smallBoxes": "Small boxes"
|
||||
},
|
||||
"ads": {
|
||||
"title": "Advertisements",
|
||||
@@ -1394,8 +1413,17 @@
|
||||
"save": "Save box",
|
||||
"saving": "Saving…",
|
||||
"deleteBox": "Delete box",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
"cancel": "Cancel",
|
||||
"iconEmoji": "Icon or emoji"
|
||||
},
|
||||
"totalBoxes": "Total boxes",
|
||||
"active": "Active",
|
||||
"shownOnHomePage": "Shown on the home page",
|
||||
"position": "Position",
|
||||
"boxes": "Content boxes",
|
||||
"hidden": "Hidden",
|
||||
"hide": "Hide",
|
||||
"activate": "Activate"
|
||||
},
|
||||
"photos": {
|
||||
"title": "Photo Moderation",
|
||||
@@ -1522,7 +1550,20 @@
|
||||
"save": "Save ban",
|
||||
"saving": "Saving…",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
},
|
||||
"activeBans": "Active bans",
|
||||
"permanent": "Permanent bans",
|
||||
"accountBans": "Account bans",
|
||||
"userIdPlaceholder": "User ID",
|
||||
"typeAccount": "Account",
|
||||
"typeIp": "IP address",
|
||||
"typeMachine": "Device",
|
||||
"typeSuper": "Account, IP and device",
|
||||
"hoursPlaceholder": "Duration in hours",
|
||||
"reasonPlaceholder": "Reason for the ban",
|
||||
"ban": "Ban user",
|
||||
"colUserId": "User ID",
|
||||
"lift": "Lift ban"
|
||||
},
|
||||
"ip": {
|
||||
"title": "IP Management",
|
||||
@@ -1542,7 +1583,11 @@
|
||||
"colIp": "IP",
|
||||
"colReason": "Reason",
|
||||
"colAdded": "Added",
|
||||
"colActions": "Actions"
|
||||
"colActions": "Actions",
|
||||
"whitelisted": "Allowed IP addresses",
|
||||
"blacklisted": "Blocked IP addresses",
|
||||
"asnPlaceholder": "ASN (optional)",
|
||||
"colAsn": "ASN"
|
||||
},
|
||||
"vpn": {
|
||||
"title": "VPN / Proxy Detection",
|
||||
@@ -1555,7 +1600,21 @@
|
||||
"blockOnDetectHint": "Automatically block users detected behind a VPN or proxy",
|
||||
"save": "Save settings",
|
||||
"saving": "Saving…",
|
||||
"settingsSaved": "VPN settings saved successfully"
|
||||
"settingsSaved": "VPN settings saved successfully",
|
||||
"protection": "VPN protection",
|
||||
"on": "Enabled",
|
||||
"off": "Disabled",
|
||||
"providerLabel": "Provider: {provider}",
|
||||
"notScreened": "Connections are not checked for VPN use.",
|
||||
"settings": "VPN settings",
|
||||
"provider": "Provider",
|
||||
"providerHint": "Choose the service used to check incoming connections.",
|
||||
"blockMessage": "Blocked connection message",
|
||||
"blockMessagePlaceholder": "Please disable your VPN and try again.",
|
||||
"blockMessageHint": "Shown when a connection is blocked by VPN protection.",
|
||||
"rawIpTitle": "IP address lists",
|
||||
"rawIpDescription": "Manage individual addresses in <ipLink>IP management</ipLink>.",
|
||||
"openIpManagement": "Open IP management"
|
||||
},
|
||||
"tickets": {
|
||||
"title": "CMS support desk",
|
||||
@@ -1685,7 +1744,10 @@
|
||||
"status": "Status",
|
||||
"submitted": "Submitted",
|
||||
"noMessage": "No message provided"
|
||||
}
|
||||
},
|
||||
"applicationsTitle": "Applications ({count})",
|
||||
"userNumber": "User #{id}",
|
||||
"rankLabel": "Rank {rank}"
|
||||
},
|
||||
"teams": {
|
||||
"title": "Teams",
|
||||
@@ -1713,7 +1775,26 @@
|
||||
"save": "Save team definition",
|
||||
"saving": "Saving…",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
},
|
||||
"teamRanks": "Staff ranks",
|
||||
"visible": "Visible",
|
||||
"hidden": "Hidden",
|
||||
"newTeamRank": "New staff rank",
|
||||
"rankName": "Rank name",
|
||||
"rankNamePlaceholder": "For example: Moderator",
|
||||
"badgeCode": "Badge code",
|
||||
"badgeCodePlaceholder": "For example: ADM",
|
||||
"jobDescription": "Job description",
|
||||
"jobDescriptionPlaceholder": "Describe this staff role",
|
||||
"staffColor": "Staff colour",
|
||||
"staffColorPlaceholder": "For example: #3366cc",
|
||||
"hiddenRank": "Hide from the staff page",
|
||||
"createTeam": "Create staff rank",
|
||||
"ranksTitle": "Staff ranks ({count})",
|
||||
"badge": "Badge",
|
||||
"color": "Colour",
|
||||
"visibility": "Visibility",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "Permissions",
|
||||
@@ -1888,8 +1969,23 @@
|
||||
"enabled": "Enabled",
|
||||
"save": "Save package",
|
||||
"saving": "Saving…",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
"cancel": "Cancel",
|
||||
"infoPlaceholder": "Describe the package contents",
|
||||
"iconPlaceholder": "Icon URL",
|
||||
"colorPlaceholder": "For example: #3366cc",
|
||||
"costsPlaceholder": "Price",
|
||||
"creditsPlaceholder": "Number of credits",
|
||||
"ducketsPlaceholder": "Number of duckets",
|
||||
"diamondsPlaceholder": "Number of diamonds",
|
||||
"giveRankPlaceholder": "Rank ID (0 for none)",
|
||||
"badgesPlaceholder": "Badge codes separated by semicolons",
|
||||
"positionPlaceholder": "Display order",
|
||||
"create": "Create package",
|
||||
"deletePackage": "Delete package"
|
||||
},
|
||||
"newPackageTitle": "New shop package",
|
||||
"backToShop": "Back to shop",
|
||||
"editPackageTitle": "Edit shop package"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2056,7 +2152,15 @@
|
||||
"colActions": "Actions",
|
||||
"statusActive": "Active",
|
||||
"statusCancelled": "Cancelled",
|
||||
"statusExpired": "Expired"
|
||||
"statusExpired": "Expired",
|
||||
"activeSubs": "Active subscriptions",
|
||||
"types": "Subscription types",
|
||||
"expiringSoon": "Expiring soon",
|
||||
"activeSubscriptions": "Active subscriptions",
|
||||
"colSubId": "Subscription ID",
|
||||
"colUserId": "User ID",
|
||||
"colType": "Type",
|
||||
"colExpires": "Expires"
|
||||
},
|
||||
"rareValues": {
|
||||
"title": "Rare Values",
|
||||
@@ -2095,8 +2199,24 @@
|
||||
"pointTypeDiamonds": "Diamonds",
|
||||
"save": "Save item",
|
||||
"saving": "Saving…",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
"cancel": "Cancel",
|
||||
"itemIdPlaceholder": "Item ID",
|
||||
"iconPlaceholder": "Icon URL",
|
||||
"creditsPlaceholder": "Credits",
|
||||
"currencyValuePlaceholder": "Currency value",
|
||||
"addValue": "Add value"
|
||||
},
|
||||
"badgePlaceholder": "Category badge code",
|
||||
"priorityPlaceholder": "Priority",
|
||||
"addCategoryButton": "Add category",
|
||||
"badgeLabel": "Badge: {badge}",
|
||||
"priorityLabel": "Priority: {priority}",
|
||||
"itemCount": "{count} items",
|
||||
"colItemId": "Item ID",
|
||||
"colCurrencyValue": "Currency value",
|
||||
"colCurrency": "Currency",
|
||||
"colIcon": "Icon",
|
||||
"noValuesInCategory": "No values in this category yet."
|
||||
},
|
||||
"badges": {
|
||||
"title": "Badges",
|
||||
@@ -2127,7 +2247,10 @@
|
||||
"colImage": "Image",
|
||||
"colName": "Name",
|
||||
"colDescription": "Description",
|
||||
"colActions": "Actions"
|
||||
"colActions": "Actions",
|
||||
"uploadedMessage": "Badge {code} uploaded.",
|
||||
"errorMessage": "Upload failed: {error}",
|
||||
"userIdPlaceholder": "User ID"
|
||||
},
|
||||
"achievements": {
|
||||
"title": "Achievements",
|
||||
@@ -2141,7 +2264,14 @@
|
||||
"colProgress": "Progress",
|
||||
"levelLabel": "Level {level}",
|
||||
"rewardCredits": "{count} credits",
|
||||
"rewardPoints": "{count} {points}"
|
||||
"rewardPoints": "{count} {points}",
|
||||
"achievementRows": "Achievement levels",
|
||||
"distinctAchievements": "Distinct achievements",
|
||||
"categories": "Categories",
|
||||
"colLevel": "Level",
|
||||
"colRewardType": "Reward type",
|
||||
"colRewardAmount": "Reward amount",
|
||||
"colPoints": "Points"
|
||||
},
|
||||
"calendar": {
|
||||
"title": "Calendar campaigns",
|
||||
@@ -2160,8 +2290,32 @@
|
||||
"days": "Days",
|
||||
"dayLabel": "Day {day}",
|
||||
"reward": "Reward",
|
||||
"noDays": "No days configured for this campaign"
|
||||
}
|
||||
"noDays": "No days configured for this campaign",
|
||||
"campaignMeta": "#{id} · {days} days · starts {date}",
|
||||
"disabled": "Disabled",
|
||||
"enabled": "Enabled",
|
||||
"locksExpired": "Expired days are locked",
|
||||
"rewards": "Rewards",
|
||||
"noRewards": "No rewards configured.",
|
||||
"colId": "ID",
|
||||
"colProduct": "Product",
|
||||
"colItem": "Item",
|
||||
"colCredits": "Credits",
|
||||
"colPixels": "Duckets",
|
||||
"colPoints": "Points",
|
||||
"colBadge": "Badge",
|
||||
"colSubscription": "Subscription",
|
||||
"typeLabel": "Type {type}",
|
||||
"rewardCount": "{count} rewards"
|
||||
},
|
||||
"campaigns": "Campaigns",
|
||||
"enabled": "Enabled campaigns",
|
||||
"totalRewards": "Total rewards",
|
||||
"statusEnabled": "Enabled",
|
||||
"statusDisabled": "Disabled",
|
||||
"campaignMeta": "#{id} · {days} days · {rewards} rewards",
|
||||
"startsAt": "Starts: {date}",
|
||||
"locksExpired": "Expired days are locked"
|
||||
},
|
||||
"radio": {
|
||||
"title": "Radio Moderation",
|
||||
@@ -2231,7 +2385,7 @@
|
||||
"noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.",
|
||||
"goToSettings": "Go to Radio Settings",
|
||||
"livePreview": "Live preview",
|
||||
"livePreviewText": "This is the bare <audio> player as visitors will see it. Press play to confirm the stream is reachable.",
|
||||
"livePreviewText": "This is the bare audio player as visitors will see it. Press play to confirm the stream is reachable.",
|
||||
"source": "Source",
|
||||
"audioSnippet": "Audio player snippet",
|
||||
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
|
||||
@@ -2355,7 +2509,8 @@
|
||||
"pointsPerMinuteHint": "Amount awarded for each full minute of listening.",
|
||||
"maxPerDayHint": "Daily cap per user. Use 0 for unlimited.",
|
||||
"minListenersHint": "Points are only awarded when at least this many people are tuned in.",
|
||||
"save": "Save points settings"
|
||||
"save": "Save points settings",
|
||||
"saved": "Settings saved."
|
||||
},
|
||||
"autoDjForm": {
|
||||
"title": "Radio AutoDJ",
|
||||
@@ -2408,7 +2563,7 @@
|
||||
"noNowPlayingHint": "no now-playing API set",
|
||||
"noListenersHint": "no listeners API set",
|
||||
"endpointDiagnostics": "Endpoint diagnostics",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> reachable · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]\">WARN</span> not configured · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">ERROR</span> fetch failed / timed out.",
|
||||
"diagnosticsLegend": "OK: reachable · WARN: not configured · ERROR: request failed or timed out.",
|
||||
"streamUrl": "Stream URL",
|
||||
"unreachable": "unreachable",
|
||||
"refresh": "Refresh"
|
||||
@@ -2589,7 +2744,16 @@
|
||||
"valuePlaceholder": "Enter value…",
|
||||
"save": "Save changes",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
},
|
||||
"settingsKeys": "Settings",
|
||||
"textKeys": "Text entries",
|
||||
"keysCount": "{count} keys",
|
||||
"addOverwriteSetting": "Add or update a setting",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"showingFirst": "Showing the first {count}",
|
||||
"addOverwriteText": "Add or update a text",
|
||||
"truncatedHint": "Showing {shown} of {total} entries."
|
||||
},
|
||||
"alerts": {
|
||||
"title": "Alerts",
|
||||
@@ -2857,7 +3021,25 @@
|
||||
"save": "Save settings",
|
||||
"saving": "Saving…",
|
||||
"confirmEnable": "Enabling maintenance will prevent all non-staff users from accessing the hotel. Continue?",
|
||||
"confirmDisable": "Disable maintenance mode and restore access for all users?"
|
||||
"confirmDisable": "Disable maintenance mode and restore access for all users?",
|
||||
"maintenance": "Maintenance",
|
||||
"hotelLockedDown": "Access to the hotel is restricted.",
|
||||
"hotelOpen": "The hotel is open.",
|
||||
"minLoginRank": "Minimum login rank",
|
||||
"duringMaintenance": "During maintenance",
|
||||
"splashMessage": "Maintenance message",
|
||||
"set": "Configured",
|
||||
"empty": "Empty",
|
||||
"currentState": "Current status",
|
||||
"maintenanceCurrently": "Maintenance: {status}",
|
||||
"onlyRankCanLogin": "Only users with rank {rank} or higher can log in.",
|
||||
"hotelOpenToEveryone": "All eligible users can log in.",
|
||||
"settings": "Settings",
|
||||
"enableMaintenance": "Enable maintenance mode",
|
||||
"minLoginRankLabel": "Minimum rank allowed during maintenance",
|
||||
"maintenanceMessageLabel": "Message shown to visitors",
|
||||
"maintenanceMessagePlaceholder": "Explain why the hotel is unavailable.",
|
||||
"saveChanges": "Save changes"
|
||||
},
|
||||
"housekeeping": {
|
||||
"title": "Permission archive",
|
||||
@@ -2941,7 +3123,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Import permissions",
|
||||
"importHint": "Paste a JSON array of permission objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of permission objects with the fields permission, minRank, description, groupName and dependsOn.",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Import",
|
||||
"exportTitle": "Export permissions",
|
||||
@@ -3058,8 +3240,13 @@
|
||||
"variablesHint": "{username}, {email}, {hotel_name}",
|
||||
"save": "Save template",
|
||||
"saving": "Saving…",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
"cancel": "Cancel",
|
||||
"variablesPlaceholder": "Variables, separated by commas",
|
||||
"active": "Active"
|
||||
},
|
||||
"create": "Create template",
|
||||
"save": "Save template",
|
||||
"updated": "Updated: {date}"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme",
|
||||
@@ -3085,7 +3272,12 @@
|
||||
"saveCss": "Save CSS",
|
||||
"savingCss": "Saving…",
|
||||
"reset": "Reset to defaults",
|
||||
"confirmReset": "Reset theme to default values?"
|
||||
"confirmReset": "Reset theme to default values?",
|
||||
"saved": "Theme settings saved.",
|
||||
"presetApplied": "Preset applied: {preset}",
|
||||
"typographyLayout": "Typography and layout",
|
||||
"font": "Font",
|
||||
"borderRadius": "Corner radius"
|
||||
},
|
||||
"media": {
|
||||
"title": "Media Library",
|
||||
@@ -3170,6 +3362,43 @@
|
||||
"subtitle": "User-facing strings from emulator_settings (commands, wordfilter, pet/matchmaking messages).",
|
||||
"infoBox": "Configuration settings (ports, timeouts, IDs) do not appear here — use the full settings link. Saved changes are synced to the emulator via RCON.",
|
||||
"fullSettings": "Full settings"
|
||||
},
|
||||
"cms": {
|
||||
"title": "CMS translations",
|
||||
"description": "Edit the text shown on the website and in housekeeping. Saved changes are applied on the next page load and survive deployments.",
|
||||
"export": "Export JSON",
|
||||
"saving": "Saving…",
|
||||
"saveLocale": "Save {locale}",
|
||||
"saved": "Translations saved and applied.",
|
||||
"readOnly": "You can review and export translations. Editing requires permission to change settings.",
|
||||
"language": "Language",
|
||||
"coverage": "{total} keys · {missing} missing · {identical} identical to English",
|
||||
"search": "Search",
|
||||
"searchPlaceholder": "Key, translation or English source…",
|
||||
"group": "Group",
|
||||
"allGroups": "All groups",
|
||||
"status": "Show",
|
||||
"all": "All texts",
|
||||
"missing": "Missing translations",
|
||||
"identical": "Identical to English",
|
||||
"modified": "Unsaved changes",
|
||||
"fallbackHint": "Missing translations use English. Identical text may be intentional. Keep variable names inside braces unchanged.",
|
||||
"unsaved": "{count} unsaved changes in this language",
|
||||
"discard": "Discard changes",
|
||||
"discardConfirm": "Discard unsaved changes in this language?",
|
||||
"fixErrors": "Fix {count} invalid messages before saving.",
|
||||
"noResults": "No texts match these filters.",
|
||||
"source": "English reference",
|
||||
"missingPlaceholder": "Translation missing; English is used.",
|
||||
"page": "Page {page} of {total} · {count} keys",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"leaveWarning": "You have unsaved translations. Leave this page?",
|
||||
"conflict": "These translations changed since you opened the editor. Export your changes, then reload before saving.",
|
||||
"unknownKey": "Unknown translation key: {key}",
|
||||
"empty": "Enter a translation for {key}, or discard this change.",
|
||||
"invalid": "Invalid message syntax in {key}. Check braces and rich-text tags.",
|
||||
"variables": "Keep the same variable names and rich-text tags as the English source in {key}."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -3299,7 +3528,14 @@
|
||||
"pages": "Pages and tools",
|
||||
"description": "Find an admin page or search hotel records.",
|
||||
"loadError": "Records could not be loaded. You can still open the pages above.",
|
||||
"placeholder": "Search pages, users, rooms…"
|
||||
"placeholder": "Search pages, users, rooms…",
|
||||
"users": "Users",
|
||||
"articles": "Articles",
|
||||
"rooms": "Rooms",
|
||||
"guilds": "Groups",
|
||||
"shop": "Shop",
|
||||
"rareValues": "Rare values",
|
||||
"noResults": "No results found."
|
||||
},
|
||||
"table": {
|
||||
"search": "Search…",
|
||||
|
||||
@@ -1679,7 +1679,7 @@
|
||||
"noNowPlayingHint": "no now-playing API set",
|
||||
"noListenersHint": "no listeners API set",
|
||||
"endpointDiagnostics": "Endpoint diagnostics",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> reachable · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]\">WARN</span> not configured · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">ERROR</span> fetch failed / timed out.",
|
||||
"diagnosticsLegend": "OK: accesible · WARN: sin configurar · ERROR: solicitud fallida o tiempo agotado.",
|
||||
"streamUrl": "Stream URL",
|
||||
"unreachable": "unreachable",
|
||||
"refresh": "Actualizar"
|
||||
@@ -1738,7 +1738,7 @@
|
||||
"noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.",
|
||||
"goToSettings": "Go to Radio Settings",
|
||||
"livePreview": "Live preview",
|
||||
"livePreviewText": "This is the bare <audio> player as visitors will see it. Press play to confirm the stream is reachable.",
|
||||
"livePreviewText": "This is the bare audio player as visitors will see it. Press play to confirm the stream is reachable.",
|
||||
"source": "Source",
|
||||
"audioSnippet": "Audio player snippet",
|
||||
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
|
||||
@@ -1969,7 +1969,7 @@
|
||||
"title": "Alertas",
|
||||
"subtitle": "Enviar alertas a todo el hotel y ver alertas recientes",
|
||||
"sendAlert": "Enviar alerta al hotel",
|
||||
"recentAlerts": "Alertas recientes",
|
||||
"recentAlerts": "Alertas recientes ({count})",
|
||||
"noRecentAlerts": "No hay alertas recientes",
|
||||
"form": {
|
||||
"title": "Título de la alerta",
|
||||
@@ -2180,7 +2180,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Importar Permisos",
|
||||
"importHint": "Paste a JSON array of Permiso objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of Permiso objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Importar",
|
||||
"exportTitle": "Export Permisos",
|
||||
@@ -2317,7 +2317,7 @@
|
||||
"presetsTitle": "Preajustes de tema",
|
||||
"presetsSubtitle": "Elige un esquema de color predefinido",
|
||||
"applyPreset": "Aplicar",
|
||||
"presetApplied": "Preajuste aplicado",
|
||||
"presetApplied": "Preajuste aplicado: {preset}",
|
||||
"customiseTitle": "Colores personalizados",
|
||||
"primary": "Primario",
|
||||
"secondary": "Secundario",
|
||||
|
||||
@@ -1679,7 +1679,7 @@
|
||||
"noNowPlayingHint": "no now-playing API set",
|
||||
"noListenersHint": "no listeners API set",
|
||||
"endpointDiagnostics": "Endpoint diagnostics",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> reachable · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]\">WARN</span> not configured · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">ERROR</span> fetch failed / timed out.",
|
||||
"diagnosticsLegend": "OK : accessible · WARN : non configuré · ERROR : échec de la requête ou délai dépassé.",
|
||||
"streamUrl": "Stream URL",
|
||||
"unreachable": "unreachable",
|
||||
"refresh": "Actualiser"
|
||||
@@ -1738,7 +1738,7 @@
|
||||
"noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.",
|
||||
"goToSettings": "Go to Radio Settings",
|
||||
"livePreview": "Live preview",
|
||||
"livePreviewText": "This is the bare <audio> player as visitors will see it. Press play to confirm the stream is reachable.",
|
||||
"livePreviewText": "This is the bare audio player as visitors will see it. Press play to confirm the stream is reachable.",
|
||||
"source": "Source",
|
||||
"audioSnippet": "Audio player snippet",
|
||||
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
|
||||
@@ -1969,7 +1969,7 @@
|
||||
"title": "Alertes",
|
||||
"subtitle": "Envoyer des alertes à tout l'hôtel et voir les alertes récentes",
|
||||
"sendAlert": "Envoyer une alerte à l'hôtel",
|
||||
"recentAlerts": "Alertes récentes",
|
||||
"recentAlerts": "Alertes récentes ({count})",
|
||||
"noRecentAlerts": "Aucune alerte récente",
|
||||
"form": {
|
||||
"title": "Titre de l'alerte",
|
||||
@@ -2180,7 +2180,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Importer permissions",
|
||||
"importHint": "Paste a JSON array of permission objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of permission objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Importer",
|
||||
"exportTitle": "Export permissions",
|
||||
@@ -2317,7 +2317,7 @@
|
||||
"presetsTitle": "Préréglages de thème",
|
||||
"presetsSubtitle": "Choisir un jeu de couleurs prédéfini",
|
||||
"applyPreset": "Appliquer",
|
||||
"presetApplied": "Préréglage appliqué",
|
||||
"presetApplied": "Préréglage appliqué: {preset}",
|
||||
"customiseTitle": "Couleurs personnalisées",
|
||||
"primary": "Primaire",
|
||||
"secondary": "Secondaire",
|
||||
|
||||
@@ -1708,7 +1708,7 @@
|
||||
"noNowPlayingHint": "nema postavljenog API-ja koji se sada igra",
|
||||
"noListenersHint": "nije postavljen API slušatelja",
|
||||
"endpointDiagnostics": "Dijagnostika krajnje točke",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> dostupno · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]\">WARN</span> nije konfigurirano · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">GREŠKA</span> dohvaćanje nije uspjelo / isteklo je vrijeme.",
|
||||
"diagnosticsLegend": "OK: dostupno · WARN: nije konfigurirano · ERROR: zahtjev nije uspio ili je istekao.",
|
||||
"streamUrl": "URL streama",
|
||||
"unreachable": "nedostižan",
|
||||
"refresh": "Osvježi"
|
||||
@@ -1767,7 +1767,7 @@
|
||||
"noStreamUrlText": "Nije postavljen radio_stream_url, tako da još nema ništa za ugraditi.",
|
||||
"goToSettings": "Idite na Radio Settings",
|
||||
"livePreview": "Pregled uživo",
|
||||
"livePreviewText": "Ovo je goli <audio> player kako će ga posjetitelji vidjeti. Pritisnite play kako biste potvrdili da je stream dostupan.",
|
||||
"livePreviewText": "Ovo je goli audio player kako će ga posjetitelji vidjeti. Pritisnite play kako biste potvrdili da je stream dostupan.",
|
||||
"source": "Izvor",
|
||||
"audioSnippet": "Isječak audio playera",
|
||||
"audioSnippetDesc": "Umetnuti HTML5 audio player usmjeren ravno prema streamu. Radi svugdje gdje je dopušten obični HTML.",
|
||||
@@ -1998,7 +1998,7 @@
|
||||
"title": "upozorenja",
|
||||
"subtitle": "Šaljite upozorenja cijelom hotelu i pogledajte nedavna upozorenja",
|
||||
"sendAlert": "Pošalji hotelsko upozorenje",
|
||||
"recentAlerts": "Nedavna upozorenja",
|
||||
"recentAlerts": "Nedavna upozorenja ({count})",
|
||||
"noRecentAlerts": "Nema nedavnih upozorenja",
|
||||
"form": {
|
||||
"title": "Naslov upozorenja",
|
||||
@@ -2209,7 +2209,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Uvoz Dozvole",
|
||||
"importHint": "Paste a JSON array of Dozvola objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of Dozvola objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "uvoz",
|
||||
"exportTitle": "Export Dozvole",
|
||||
@@ -2346,7 +2346,7 @@
|
||||
"presetsTitle": "Unaprijed postavljene teme",
|
||||
"presetsSubtitle": "Odaberite unaprijed pripremljenu shemu boja",
|
||||
"applyPreset": "primijeniti",
|
||||
"presetApplied": "Unaprijed postavljeno",
|
||||
"presetApplied": "Unaprijed postavljeno: {preset}",
|
||||
"customiseTitle": "Prilagođene boje",
|
||||
"primary": "Primarni",
|
||||
"secondary": "Sekundarna",
|
||||
|
||||
@@ -1708,7 +1708,7 @@
|
||||
"noNowPlayingHint": "nincs jelenleg lejátszott API-készlet",
|
||||
"noListenersHint": "nincs hallgató API beállítva",
|
||||
"endpointDiagnostics": "Végpont diagnosztika",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> elérhető · <span class=\"inline-block text-[0.72rem] font-bold.5 round-0 px-2 edpy-0 px-2 ed bg-[var(--color-primary)]/18 text-[var(--color-text)]\">FIGYELMEZTETÉS</span> nincs konfigurálva · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(]--ERtch failed-R)\">fe.",
|
||||
"diagnosticsLegend": "OK: elérhető · WARN: nincs beállítva · ERROR: sikertelen kérés vagy időtúllépés.",
|
||||
"streamUrl": "Stream URL",
|
||||
"unreachable": "elérhetetlen",
|
||||
"refresh": "Frissítés"
|
||||
@@ -1767,7 +1767,7 @@
|
||||
"noStreamUrlText": "Nincs beállítva radio_stream_url, így még nincs mit beágyazni.",
|
||||
"goToSettings": "Menjen a Rádióbeállításokhoz",
|
||||
"livePreview": "Élő előnézet",
|
||||
"livePreviewText": "Ez a csupasz <audio> lejátszó, ahogy a látogatók látni fogják. Nyomja meg a lejátszás gombot, hogy megerősítse, hogy az adatfolyam elérhető.",
|
||||
"livePreviewText": "Ez a csupasz audio lejátszó, ahogy a látogatók látni fogják. Nyomja meg a lejátszás gombot, hogy megerősítse, hogy az adatfolyam elérhető.",
|
||||
"source": "Forrás",
|
||||
"audioSnippet": "Hanglejátszó részlet",
|
||||
"audioSnippetDesc": "A beugró HTML5 audiolejátszó egyenesen a streamre mutatott. Mindenhol működik, ahol a sima HTML megengedett.",
|
||||
@@ -1998,7 +1998,7 @@
|
||||
"title": "Figyelmeztetések",
|
||||
"subtitle": "A szálloda egészére kiterjedő figyelmeztetések küldése és a legutóbbi figyelmeztetések megtekintése",
|
||||
"sendAlert": "Szállodai figyelmeztetés küldése",
|
||||
"recentAlerts": "Legutóbbi figyelmeztetések",
|
||||
"recentAlerts": "Legutóbbi figyelmeztetések ({count})",
|
||||
"noRecentAlerts": "Nincsenek friss figyelmeztetések",
|
||||
"form": {
|
||||
"title": "Figyelmeztetés címe",
|
||||
@@ -2209,7 +2209,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Importálás Engedélyek",
|
||||
"importHint": "Paste a JSON array of Engedély objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of Engedély objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Importálás",
|
||||
"exportTitle": "Export Engedélyek",
|
||||
@@ -2346,7 +2346,7 @@
|
||||
"presetsTitle": "Előre beállított téma",
|
||||
"presetsSubtitle": "Válasszon egy előre elkészített színsémát",
|
||||
"applyPreset": "Alkalmazni",
|
||||
"presetApplied": "Előbeállítás alkalmazva",
|
||||
"presetApplied": "Előbeállítás alkalmazva: {preset}",
|
||||
"customiseTitle": "Egyedi színek",
|
||||
"primary": "Elsődleges",
|
||||
"secondary": "Másodlagos",
|
||||
|
||||
+306
-39
@@ -848,7 +848,8 @@
|
||||
"codePlaceholder": "Codice a 6 cifre",
|
||||
"confirm": "Conferma",
|
||||
"enableIntro": "Aggiungi un livello di sicurezza in più con un'app di autenticazione.",
|
||||
"enable": "Attiva 2FA"
|
||||
"enable": "Attiva 2FA",
|
||||
"rateLimit": "Troppi tentativi. Attendi prima di riprovare."
|
||||
},
|
||||
"title": "Account collegati",
|
||||
"subtitle": "Collega un account social per poter accedere con un clic",
|
||||
@@ -906,7 +907,12 @@
|
||||
"alreadyHaveAccountPre": "Hai già un account?",
|
||||
"alreadyHaveAccountLink": "Accedi",
|
||||
"whoIsOnline": "Chi è online",
|
||||
"usersOnline": "{count} online"
|
||||
"usersOnline": "{count} online",
|
||||
"accepted": "Accettato e confermato",
|
||||
"showPassword": "Mostra",
|
||||
"hidePassword": "Nascondi",
|
||||
"accountDetails": "Dettagli dell’account",
|
||||
"credentials": "Credenziali e sicurezza"
|
||||
},
|
||||
"forgot": {
|
||||
"title": "Reimposta password",
|
||||
@@ -1061,7 +1067,17 @@
|
||||
"themeBuilder": "Editor del tema",
|
||||
"catalogMaintenance": "Manutenzione catalogo",
|
||||
"openSidebar": "Apri menu",
|
||||
"closeSidebar": "Chiudi menu"
|
||||
"closeSidebar": "Chiudi menu",
|
||||
"search": {
|
||||
"placeholder": "Cerca…",
|
||||
"noResults": "Nessun risultato trovato",
|
||||
"users": "Utenti",
|
||||
"articles": "Articoli",
|
||||
"rooms": "Stanze",
|
||||
"guilds": "Gruppi",
|
||||
"shop": "Negozio",
|
||||
"hint": "Inizia a scrivere per cercare tra utenti, articoli, stanze, gruppi e articoli del negozio"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -1078,7 +1094,8 @@
|
||||
"rankWithCount": "Grado {rank}: {count} account",
|
||||
"recentStaffActivity": "Attività recente dello staff",
|
||||
"viewAll": "Vedi tutto",
|
||||
"noStaffActivityLogged": "Nessuna attività dello staff ancora registrata"
|
||||
"noStaffActivityLogged": "Nessuna attività dello staff ancora registrata",
|
||||
"totalAccounts": "{count} account totali"
|
||||
},
|
||||
"articles": {
|
||||
"title": "Articoli",
|
||||
@@ -1119,7 +1136,11 @@
|
||||
"statusPublished": "Pubblicato",
|
||||
"publishAt": "Data di pubblicazione",
|
||||
"publishedAt": "Pubblicato il"
|
||||
}
|
||||
},
|
||||
"articlesShown": "Articoli mostrati",
|
||||
"latest": "Ultimo articolo",
|
||||
"mostRecentPost": "Pubblicazione più recente",
|
||||
"recentArticles": "Articoli recenti"
|
||||
},
|
||||
"events": {
|
||||
"newEvent": "+ Nuovo evento",
|
||||
@@ -1240,7 +1261,13 @@
|
||||
"colColour": "Colore",
|
||||
"colUsedBy": "Usato da",
|
||||
"colCreated": "Creato",
|
||||
"colActions": "Azioni"
|
||||
"colActions": "Azioni",
|
||||
"databaseError": "Impossibile caricare i tag. Riprova.",
|
||||
"totalTags": "Tag totali",
|
||||
"inUse": "In uso",
|
||||
"attachedToContent": "Associati a contenuti",
|
||||
"unused": "Non utilizzati",
|
||||
"allTags": "Tutti i tag"
|
||||
},
|
||||
"navigation": {
|
||||
"title": "Navigatore",
|
||||
@@ -1264,7 +1291,12 @@
|
||||
"colName": "Nome",
|
||||
"colOrder": "Ordine",
|
||||
"colRooms": "Stanze",
|
||||
"colActions": "Azioni"
|
||||
"colActions": "Azioni",
|
||||
"publicFlatcats": "Categorie delle stanze pubbliche",
|
||||
"markedPublic": "Categorie contrassegnate come pubbliche",
|
||||
"visibleRooms": "Stanze visibili",
|
||||
"rows": "righe",
|
||||
"unnamed": "Stanza senza nome"
|
||||
},
|
||||
"helpCenter": {
|
||||
"title": "Centro assistenza",
|
||||
@@ -1291,7 +1323,9 @@
|
||||
"saving": "Salvataggio…",
|
||||
"deleteEntry": "Elimina voce",
|
||||
"cancel": "Annulla"
|
||||
}
|
||||
},
|
||||
"withButton": "Con pulsante",
|
||||
"smallBoxes": "Riquadri piccoli"
|
||||
},
|
||||
"ads": {
|
||||
"title": "Pubblicità",
|
||||
@@ -1354,8 +1388,17 @@
|
||||
"save": "Salva box",
|
||||
"saving": "Salvataggio…",
|
||||
"deleteBox": "Elimina box",
|
||||
"cancel": "Annulla"
|
||||
}
|
||||
"cancel": "Annulla",
|
||||
"iconEmoji": "Icona o emoji"
|
||||
},
|
||||
"totalBoxes": "Riquadri totali",
|
||||
"active": "Attivo",
|
||||
"shownOnHomePage": "Mostrato nella pagina iniziale",
|
||||
"position": "Posizione",
|
||||
"boxes": "Riquadri di contenuto",
|
||||
"hidden": "Nascosto",
|
||||
"hide": "Nascondi",
|
||||
"activate": "Attiva"
|
||||
},
|
||||
"photos": {
|
||||
"title": "Moderazione foto",
|
||||
@@ -1482,7 +1525,20 @@
|
||||
"save": "Salva ban",
|
||||
"saving": "Salvataggio…",
|
||||
"cancel": "Annulla"
|
||||
}
|
||||
},
|
||||
"activeBans": "Ban attivi",
|
||||
"permanent": "Ban permanenti",
|
||||
"accountBans": "Ban degli account",
|
||||
"userIdPlaceholder": "ID utente",
|
||||
"typeAccount": "Account",
|
||||
"typeIp": "Indirizzo IP",
|
||||
"typeMachine": "Dispositivo",
|
||||
"typeSuper": "Account, IP e dispositivo",
|
||||
"hoursPlaceholder": "Durata in ore",
|
||||
"reasonPlaceholder": "Motivo del ban",
|
||||
"ban": "Banna utente",
|
||||
"colUserId": "ID utente",
|
||||
"lift": "Revoca ban"
|
||||
},
|
||||
"ip": {
|
||||
"title": "Gestione IP",
|
||||
@@ -1502,7 +1558,11 @@
|
||||
"colIp": "IP",
|
||||
"colReason": "Motivo",
|
||||
"colAdded": "Aggiunto",
|
||||
"colActions": "Azioni"
|
||||
"colActions": "Azioni",
|
||||
"whitelisted": "Indirizzi IP consentiti",
|
||||
"blacklisted": "Indirizzi IP bloccati",
|
||||
"asnPlaceholder": "ASN (facoltativo)",
|
||||
"colAsn": "ASN"
|
||||
},
|
||||
"vpn": {
|
||||
"title": "Rilevamento VPN / Proxy",
|
||||
@@ -1515,7 +1575,21 @@
|
||||
"blockOnDetectHint": "Blocca automaticamente gli utenti rilevati dietro VPN o proxy",
|
||||
"save": "Salva impostazioni",
|
||||
"saving": "Salvataggio…",
|
||||
"settingsSaved": "Impostazioni VPN salvate con successo"
|
||||
"settingsSaved": "Impostazioni VPN salvate con successo",
|
||||
"protection": "Protezione VPN",
|
||||
"on": "Attiva",
|
||||
"off": "Disattivata",
|
||||
"providerLabel": "Provider: {provider}",
|
||||
"notScreened": "Le connessioni non vengono controllate per l’uso di VPN.",
|
||||
"settings": "Impostazioni VPN",
|
||||
"provider": "Provider",
|
||||
"providerHint": "Scegli il servizio per controllare le connessioni in entrata.",
|
||||
"blockMessage": "Messaggio per le connessioni bloccate",
|
||||
"blockMessagePlaceholder": "Disattiva la VPN e riprova.",
|
||||
"blockMessageHint": "Mostrato quando una connessione viene bloccata dalla protezione VPN.",
|
||||
"rawIpTitle": "Elenchi degli indirizzi IP",
|
||||
"rawIpDescription": "Gestisci i singoli indirizzi in <ipLink>Gestione IP</ipLink>.",
|
||||
"openIpManagement": "Apri gestione IP"
|
||||
},
|
||||
"tickets": {
|
||||
"title": "Supporto CMS",
|
||||
@@ -1621,7 +1695,10 @@
|
||||
"status": "Stato",
|
||||
"submitted": "Inviata",
|
||||
"noMessage": "Nessun messaggio fornito"
|
||||
}
|
||||
},
|
||||
"applicationsTitle": "Candidature ({count})",
|
||||
"userNumber": "Utente #{id}",
|
||||
"rankLabel": "Grado {rank}"
|
||||
},
|
||||
"teams": {
|
||||
"title": "Team",
|
||||
@@ -1649,7 +1726,26 @@
|
||||
"save": "Salva definizione team",
|
||||
"saving": "Salvataggio…",
|
||||
"cancel": "Annulla"
|
||||
}
|
||||
},
|
||||
"teamRanks": "Gradi dello staff",
|
||||
"visible": "Visibili",
|
||||
"hidden": "Nascosti",
|
||||
"newTeamRank": "Nuovo grado dello staff",
|
||||
"rankName": "Nome del grado",
|
||||
"rankNamePlaceholder": "Ad esempio: Moderatore",
|
||||
"badgeCode": "Codice del distintivo",
|
||||
"badgeCodePlaceholder": "Ad esempio: ADM",
|
||||
"jobDescription": "Descrizione dell’incarico",
|
||||
"jobDescriptionPlaceholder": "Descrivi questo incarico dello staff",
|
||||
"staffColor": "Colore dello staff",
|
||||
"staffColorPlaceholder": "Ad esempio: #3366cc",
|
||||
"hiddenRank": "Nascondi dalla pagina staff",
|
||||
"createTeam": "Crea grado dello staff",
|
||||
"ranksTitle": "Gradi dello staff ({count})",
|
||||
"badge": "Distintivo",
|
||||
"color": "Colore",
|
||||
"visibility": "Visibilità",
|
||||
"delete": "Elimina"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "Permessi",
|
||||
@@ -1792,8 +1888,23 @@
|
||||
"enabled": "Attivo",
|
||||
"save": "Salva pacchetto",
|
||||
"saving": "Salvataggio…",
|
||||
"cancel": "Annulla"
|
||||
}
|
||||
"cancel": "Annulla",
|
||||
"infoPlaceholder": "Descrivi il contenuto del pacchetto",
|
||||
"iconPlaceholder": "URL dell’icona",
|
||||
"colorPlaceholder": "Ad esempio: #3366cc",
|
||||
"costsPlaceholder": "Prezzo",
|
||||
"creditsPlaceholder": "Numero di crediti",
|
||||
"ducketsPlaceholder": "Numero di duckets",
|
||||
"diamondsPlaceholder": "Numero di diamanti",
|
||||
"giveRankPlaceholder": "ID grado (0 per nessuno)",
|
||||
"badgesPlaceholder": "Codici dei distintivi separati da punto e virgola",
|
||||
"positionPlaceholder": "Ordine di visualizzazione",
|
||||
"create": "Crea pacchetto",
|
||||
"deletePackage": "Elimina pacchetto"
|
||||
},
|
||||
"newPackageTitle": "Nuovo pacchetto del negozio",
|
||||
"backToShop": "Torna al negozio",
|
||||
"editPackageTitle": "Modifica pacchetto del negozio"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -1837,7 +1948,20 @@
|
||||
"back": "Torna ai gruppi",
|
||||
"viewPublic": "Apri pagina pubblica",
|
||||
"disband": "Sciogli gruppo",
|
||||
"listHeading": "Guilds ({count})"
|
||||
"listHeading": "Guilds ({count})",
|
||||
"edit": "Modifica gruppo",
|
||||
"save": "Salva",
|
||||
"saving": "Salvataggio…",
|
||||
"updated": "Gruppo aggiornato",
|
||||
"updateError": "Impossibile aggiornare il gruppo",
|
||||
"formName": "Nome",
|
||||
"formDescription": "Descrizione",
|
||||
"formState": "Stato",
|
||||
"formForum": "Forum",
|
||||
"formReadForum": "Leggere il forum",
|
||||
"formPostMessages": "Pubblicare messaggi",
|
||||
"formPostThreads": "Creare discussioni",
|
||||
"formModForum": "Moderare il forum"
|
||||
},
|
||||
"transactions": {
|
||||
"title": "Transazioni",
|
||||
@@ -1926,7 +2050,9 @@
|
||||
"save": "Salva buono",
|
||||
"saving": "Salvataggio…",
|
||||
"cancel": "Annulla"
|
||||
}
|
||||
},
|
||||
"updated": "Voucher aggiornato",
|
||||
"updateError": "Impossibile aggiornare il voucher"
|
||||
},
|
||||
"subscriptions": {
|
||||
"title": "Abbonamenti",
|
||||
@@ -1945,7 +2071,15 @@
|
||||
"colActions": "Azioni",
|
||||
"statusActive": "Attivo",
|
||||
"statusCancelled": "Annullato",
|
||||
"statusExpired": "Scaduto"
|
||||
"statusExpired": "Scaduto",
|
||||
"activeSubs": "Abbonamenti attivi",
|
||||
"types": "Tipi di abbonamento",
|
||||
"expiringSoon": "In scadenza",
|
||||
"activeSubscriptions": "Abbonamenti attivi",
|
||||
"colSubId": "ID abbonamento",
|
||||
"colUserId": "ID utente",
|
||||
"colType": "Tipo",
|
||||
"colExpires": "Scadenza"
|
||||
},
|
||||
"rareValues": {
|
||||
"title": "Valori rari",
|
||||
@@ -1983,8 +2117,25 @@
|
||||
"pointTypeDiamonds": "Diamanti",
|
||||
"save": "Salva articolo",
|
||||
"saving": "Salvataggio…",
|
||||
"cancel": "Annulla"
|
||||
}
|
||||
"cancel": "Annulla",
|
||||
"itemIdPlaceholder": "ID furni",
|
||||
"iconPlaceholder": "URL dell’icona",
|
||||
"creditsPlaceholder": "Crediti",
|
||||
"currencyValuePlaceholder": "Valore in valuta",
|
||||
"addValue": "Aggiungi valore"
|
||||
},
|
||||
"badgePlaceholder": "Codice del distintivo della categoria",
|
||||
"priorityPlaceholder": "Priorità",
|
||||
"addCategoryButton": "Aggiungi categoria",
|
||||
"badgeLabel": "Distintivo: {badge}",
|
||||
"priorityLabel": "Priorità: {priority}",
|
||||
"itemCount": "{count} furni",
|
||||
"colItemId": "ID furni",
|
||||
"colCurrencyValue": "Valore in valuta",
|
||||
"colCurrency": "Valuta",
|
||||
"colIcon": "Icona",
|
||||
"noValuesInCategory": "Nessun valore ancora presente in questa categoria.",
|
||||
"editValue": "Modifica valore"
|
||||
},
|
||||
"badges": {
|
||||
"title": "Distintivi",
|
||||
@@ -2015,7 +2166,10 @@
|
||||
"colImage": "Immagine",
|
||||
"colName": "Nome",
|
||||
"colDescription": "Descrizione",
|
||||
"colActions": "Azioni"
|
||||
"colActions": "Azioni",
|
||||
"uploadedMessage": "Distintivo {code} caricato.",
|
||||
"errorMessage": "Caricamento non riuscito: {error}",
|
||||
"userIdPlaceholder": "ID utente"
|
||||
},
|
||||
"achievements": {
|
||||
"title": "Trofei",
|
||||
@@ -2029,7 +2183,14 @@
|
||||
"colProgress": "Progresso",
|
||||
"levelLabel": "Livello {level}",
|
||||
"rewardCredits": "{count} crediti",
|
||||
"rewardPoints": "{count} {points}"
|
||||
"rewardPoints": "{count} {points}",
|
||||
"achievementRows": "Livelli dei traguardi",
|
||||
"distinctAchievements": "Traguardi distinti",
|
||||
"categories": "Categorie",
|
||||
"colLevel": "Livello",
|
||||
"colRewardType": "Tipo di ricompensa",
|
||||
"colRewardAmount": "Quantità della ricompensa",
|
||||
"colPoints": "Punti"
|
||||
},
|
||||
"calendar": {
|
||||
"title": "Campagne calendario",
|
||||
@@ -2048,8 +2209,32 @@
|
||||
"days": "Giorni",
|
||||
"dayLabel": "Giorno {day}",
|
||||
"reward": "Ricompensa",
|
||||
"noDays": "Nessun giorno configurato per questa campagna"
|
||||
}
|
||||
"noDays": "Nessun giorno configurato per questa campagna",
|
||||
"campaignMeta": "#{id} · {days} giorni · inizio {date}",
|
||||
"disabled": "Disattivata",
|
||||
"enabled": "Attiva",
|
||||
"locksExpired": "Giorni scaduti bloccati",
|
||||
"rewards": "Ricompense",
|
||||
"noRewards": "Nessuna ricompensa configurata.",
|
||||
"colId": "ID",
|
||||
"colProduct": "Prodotto",
|
||||
"colItem": "Furni",
|
||||
"colCredits": "Crediti",
|
||||
"colPixels": "Duckets",
|
||||
"colPoints": "Punti",
|
||||
"colBadge": "Distintivo",
|
||||
"colSubscription": "Abbonamento",
|
||||
"typeLabel": "Tipo {type}",
|
||||
"rewardCount": "{count} ricompense"
|
||||
},
|
||||
"campaigns": "Campagne",
|
||||
"enabled": "Campagne attive",
|
||||
"totalRewards": "Ricompense totali",
|
||||
"statusEnabled": "Attiva",
|
||||
"statusDisabled": "Disattivata",
|
||||
"campaignMeta": "#{id} · {days} giorni · {rewards} ricompense",
|
||||
"startsAt": "Inizio: {date}",
|
||||
"locksExpired": "Giorni scaduti bloccati"
|
||||
},
|
||||
"radio": {
|
||||
"title": "Moderazione radio",
|
||||
@@ -2082,7 +2267,7 @@
|
||||
"noNowPlayingHint": "no now-playing API set",
|
||||
"noListenersHint": "no listeners API set",
|
||||
"endpointDiagnostics": "Endpoint diagnostics",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> reachable · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]\">WARN</span> not configured · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">ERROR</span> fetch failed / timed out.",
|
||||
"diagnosticsLegend": "OK: raggiungibile · WARN: non configurato · ERROR: richiesta non riuscita o scaduta.",
|
||||
"streamUrl": "Stream URL",
|
||||
"unreachable": "unreachable",
|
||||
"refresh": "Aggiorna"
|
||||
@@ -2141,7 +2326,7 @@
|
||||
"noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.",
|
||||
"goToSettings": "Go to Radio Settings",
|
||||
"livePreview": "Live preview",
|
||||
"livePreviewText": "This is the bare <audio> player as visitors will see it. Press play to confirm the stream is reachable.",
|
||||
"livePreviewText": "Questo è il lettore audio mostrato ai visitatori. Avvia la riproduzione per verificare che lo streaming sia raggiungibile.",
|
||||
"source": "Source",
|
||||
"audioSnippet": "Audio player snippet",
|
||||
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
|
||||
@@ -2248,7 +2433,7 @@
|
||||
"pointsForm": {
|
||||
"title": "Configurazione punti ascoltatore",
|
||||
"subtitle": "Reward listeners with currency for time spent on the radio. Edits write to website_settings (radio_points_* keys) and bust the runtime settings cache. Booleans use 0 / 1.",
|
||||
"saved": "Saved",
|
||||
"saved": "Impostazioni salvate.",
|
||||
"points": "Points",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
@@ -2478,13 +2663,22 @@
|
||||
"valuePlaceholder": "Inserisci valore…",
|
||||
"save": "Salva modifiche",
|
||||
"cancel": "Annulla"
|
||||
}
|
||||
},
|
||||
"settingsKeys": "Impostazioni",
|
||||
"textKeys": "Voci di testo",
|
||||
"keysCount": "{count} chiavi",
|
||||
"addOverwriteSetting": "Aggiungi o aggiorna un’impostazione",
|
||||
"key": "Chiave",
|
||||
"value": "Valore",
|
||||
"showingFirst": "Prime {count} voci mostrate",
|
||||
"addOverwriteText": "Aggiungi o aggiorna un testo",
|
||||
"truncatedHint": "Mostrate {shown} voci su {total}."
|
||||
},
|
||||
"alerts": {
|
||||
"title": "Avvisi",
|
||||
"subtitle": "Invia avvisi a tutto l'hotel e visualizza avvisi recenti",
|
||||
"sendAlert": "Invia avviso hotel",
|
||||
"recentAlerts": "Avvisi recenti",
|
||||
"recentAlerts": "Avvisi recenti ({count})",
|
||||
"noRecentAlerts": "Nessun avviso recente",
|
||||
"form": {
|
||||
"title": "Titolo avviso",
|
||||
@@ -2619,7 +2813,25 @@
|
||||
"save": "Salva impostazioni",
|
||||
"saving": "Salvataggio…",
|
||||
"confirmEnable": "Attivare la manutenzione impedirà a tutti gli utenti non dello staff di accedere all'hotel. Continuare?",
|
||||
"confirmDisable": "Disattivare la modalità manutenzione e ripristinare l'accesso per tutti gli utenti?"
|
||||
"confirmDisable": "Disattivare la modalità manutenzione e ripristinare l'accesso per tutti gli utenti?",
|
||||
"maintenance": "Manutenzione",
|
||||
"hotelLockedDown": "Accesso all’hotel limitato.",
|
||||
"hotelOpen": "L’hotel è aperto.",
|
||||
"minLoginRank": "Grado minimo per accedere",
|
||||
"duringMaintenance": "Durante la manutenzione",
|
||||
"splashMessage": "Messaggio di manutenzione",
|
||||
"set": "Configurato",
|
||||
"empty": "Vuoto",
|
||||
"currentState": "Stato attuale",
|
||||
"maintenanceCurrently": "Manutenzione: {status}",
|
||||
"onlyRankCanLogin": "Possono accedere solo gli utenti con grado {rank} o superiore.",
|
||||
"hotelOpenToEveryone": "Tutti gli utenti abilitati possono accedere.",
|
||||
"settings": "Impostazioni",
|
||||
"enableMaintenance": "Attiva la modalità manutenzione",
|
||||
"minLoginRankLabel": "Grado minimo consentito durante la manutenzione",
|
||||
"maintenanceMessageLabel": "Messaggio mostrato ai visitatori",
|
||||
"maintenanceMessagePlaceholder": "Spiega perché l’hotel non è disponibile.",
|
||||
"saveChanges": "Salva modifiche"
|
||||
},
|
||||
"housekeeping": {
|
||||
"title": "Archivio permessi",
|
||||
@@ -2695,7 +2907,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Log di audit",
|
||||
"importPermissions": "Importa Permessi",
|
||||
"importHint": "Paste a JSON array of Permesso objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Incolla un array JSON di permessi con i campi permission, minRank, description, groupName e dependsOn.",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Importa",
|
||||
"exportTitle": "Export Permessi",
|
||||
@@ -2820,8 +3032,13 @@
|
||||
"variablesHint": "{username}, {email}, {hotel_name}",
|
||||
"save": "Salva template",
|
||||
"saving": "Salvataggio…",
|
||||
"cancel": "Annulla"
|
||||
}
|
||||
"cancel": "Annulla",
|
||||
"variablesPlaceholder": "Variabili separate da virgole",
|
||||
"active": "Attivo"
|
||||
},
|
||||
"create": "Crea modello",
|
||||
"save": "Salva modello",
|
||||
"updated": "Aggiornato: {date}"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Tema",
|
||||
@@ -2832,7 +3049,7 @@
|
||||
"presetsTitle": "Preimpostazioni tema",
|
||||
"presetsSubtitle": "Scegli una combinazione di colori predefinita",
|
||||
"applyPreset": "Applica",
|
||||
"presetApplied": "Preimpostazione applicata",
|
||||
"presetApplied": "Preset applicato: {preset}",
|
||||
"customiseTitle": "Colori personalizzati",
|
||||
"primary": "Primario",
|
||||
"secondary": "Secondario",
|
||||
@@ -2848,7 +3065,11 @@
|
||||
"saveCss": "Salva CSS",
|
||||
"savingCss": "Salvataggio…",
|
||||
"reset": "Ripristina valori predefiniti",
|
||||
"confirmReset": "Ripristinare il tema ai valori predefiniti?"
|
||||
"confirmReset": "Ripristinare il tema ai valori predefiniti?",
|
||||
"saved": "Impostazioni del tema salvate.",
|
||||
"typographyLayout": "Tipografia e impaginazione",
|
||||
"font": "Carattere",
|
||||
"borderRadius": "Raggio degli angoli"
|
||||
},
|
||||
"media": {
|
||||
"title": "Libreria media",
|
||||
@@ -2982,7 +3203,9 @@
|
||||
"alerts": "Alert",
|
||||
"toolsGroup": "Strumenti",
|
||||
"ticketInbox": "Inbox",
|
||||
"cmsErrors": "Errori CMS"
|
||||
"cmsErrors": "Errori CMS",
|
||||
"themeBuilder": "Editor del tema",
|
||||
"studio": "Studio"
|
||||
},
|
||||
"events": "Eventi",
|
||||
"eventsSubtitle": "Eventi hotel e tipi evento",
|
||||
@@ -3105,6 +3328,43 @@
|
||||
"subtitle": "User-facing strings from emulator_settings (commands, wordfilter, pet/matchmaking messages).",
|
||||
"infoBox": "Configuration settings (ports, timeouts, IDs) do not appear here — use the full settings link. Saved changes are synced to the emulator via RCON.",
|
||||
"fullSettings": "Full settings"
|
||||
},
|
||||
"cms": {
|
||||
"title": "Traduzioni del CMS",
|
||||
"description": "Modifica i testi del sito e dell’housekeeping. Le modifiche salvate vengono applicate al caricamento successivo della pagina e restano dopo i deploy.",
|
||||
"export": "Esporta JSON",
|
||||
"saving": "Salvataggio…",
|
||||
"saveLocale": "Salva {locale}",
|
||||
"saved": "Traduzioni salvate e applicate.",
|
||||
"readOnly": "Puoi consultare ed esportare le traduzioni. Per modificarle serve il permesso di modifica delle impostazioni.",
|
||||
"language": "Lingua",
|
||||
"coverage": "{total} chiavi · {missing} mancanti · {identical} identiche all’inglese",
|
||||
"search": "Cerca",
|
||||
"searchPlaceholder": "Chiave, traduzione o testo inglese…",
|
||||
"group": "Gruppo",
|
||||
"allGroups": "Tutti i gruppi",
|
||||
"status": "Mostra",
|
||||
"all": "Tutti i testi",
|
||||
"missing": "Traduzioni mancanti",
|
||||
"identical": "Identiche all’inglese",
|
||||
"modified": "Modifiche non salvate",
|
||||
"fallbackHint": "Le traduzioni mancanti usano l’inglese. Un testo identico può essere intenzionale. Mantieni invariati i nomi delle variabili tra parentesi graffe.",
|
||||
"unsaved": "{count} modifiche non salvate in questa lingua",
|
||||
"discard": "Annulla modifiche",
|
||||
"discardConfirm": "Annullare le modifiche non salvate in questa lingua?",
|
||||
"fixErrors": "Correggi {count} messaggi non validi prima di salvare.",
|
||||
"noResults": "Nessun testo corrisponde ai filtri.",
|
||||
"source": "Riferimento inglese",
|
||||
"missingPlaceholder": "Traduzione mancante: viene usato l’inglese.",
|
||||
"page": "Pagina {page} di {total} · {count} chiavi",
|
||||
"previous": "Precedente",
|
||||
"next": "Successiva",
|
||||
"leaveWarning": "Ci sono traduzioni non salvate. Vuoi uscire dalla pagina?",
|
||||
"conflict": "Queste traduzioni sono cambiate dopo l’apertura dell’editor. Esporta le modifiche e ricarica la pagina prima di salvare.",
|
||||
"unknownKey": "Chiave di traduzione sconosciuta: {key}",
|
||||
"empty": "Inserisci una traduzione per {key} oppure annulla questa modifica.",
|
||||
"invalid": "Sintassi del messaggio non valida in {key}. Controlla le parentesi graffe e i tag del testo.",
|
||||
"variables": "Mantieni gli stessi nomi delle variabili e tag del testo inglese in {key}."
|
||||
}
|
||||
},
|
||||
"analytics": {
|
||||
@@ -3243,7 +3503,14 @@
|
||||
"pages": "Pagine e strumenti",
|
||||
"description": "Trova una pagina di gestione o cerca nei dati dell’hotel.",
|
||||
"loadError": "Impossibile caricare i risultati. Puoi comunque aprire le pagine elencate.",
|
||||
"placeholder": "Cerca pagine, utenti, stanze…"
|
||||
"placeholder": "Cerca pagine, utenti, stanze…",
|
||||
"users": "Utenti",
|
||||
"articles": "Articoli",
|
||||
"rooms": "Stanze",
|
||||
"guilds": "Gruppi",
|
||||
"shop": "Negozio",
|
||||
"rareValues": "Valori dei rari",
|
||||
"noResults": "Nessun risultato trovato."
|
||||
},
|
||||
"table": {
|
||||
"search": "Cerca…",
|
||||
|
||||
+41
-4
@@ -2045,7 +2045,7 @@
|
||||
"noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.",
|
||||
"goToSettings": "Go to Radio Settings",
|
||||
"livePreview": "Live preview",
|
||||
"livePreviewText": "This is the bare <audio> player as visitors will see it. Press play to confirm the stream is reachable.",
|
||||
"livePreviewText": "This is the bare audio player as visitors will see it. Press play to confirm the stream is reachable.",
|
||||
"source": "Source",
|
||||
"audioSnippet": "Audio player snippet",
|
||||
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
|
||||
@@ -2222,7 +2222,7 @@
|
||||
"noNowPlayingHint": "no now-playing API set",
|
||||
"noListenersHint": "no listeners API set",
|
||||
"endpointDiagnostics": "Endpoint diagnostics",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> reachable · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]\">WARN</span> not configured · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">ERROR</span> fetch failed / timed out.",
|
||||
"diagnosticsLegend": "OK: bereikbaar · WARN: niet ingesteld · ERROR: aanvraag mislukt of time-out.",
|
||||
"streamUrl": "Stream URL",
|
||||
"unreachable": "unreachable",
|
||||
"refresh": "Vernieuwen"
|
||||
@@ -2297,7 +2297,7 @@
|
||||
"title": "Meldingen",
|
||||
"subtitle": "Hotelbrede meldingen verzenden en recente meldingen bekijken",
|
||||
"sendAlert": "Hotelmelding verzenden",
|
||||
"recentAlerts": "Recente meldingen",
|
||||
"recentAlerts": "Recente meldingen ({count})",
|
||||
"noRecentAlerts": "Geen recente meldingen",
|
||||
"form": {
|
||||
"title": "Melding titel",
|
||||
@@ -2516,7 +2516,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Auditlog",
|
||||
"importPermissions": "Permissies importeren",
|
||||
"importHint": "Plak een JSON-array met permissie-objecten. Formaat: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Plak een JSON-array met permissie-objecten. Formaat: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Plak JSON hier…",
|
||||
"import": "Importeren",
|
||||
"exportTitle": "Permissies exporteren",
|
||||
@@ -2774,6 +2774,43 @@
|
||||
"subtitle": "User-facing strings from emulator_settings (commands, wordfilter, pet/matchmaking messages).",
|
||||
"infoBox": "Configuration settings (ports, timeouts, IDs) do not appear here — use the full settings link. Saved changes are synced to the emulator via RCON.",
|
||||
"fullSettings": "Full settings"
|
||||
},
|
||||
"cms": {
|
||||
"title": "CMS-vertalingen",
|
||||
"description": "Bewerk de teksten op de website en in housekeeping. Opgeslagen wijzigingen worden bij de volgende paginalading toegepast en blijven behouden na deployments.",
|
||||
"export": "JSON exporteren",
|
||||
"saving": "Opslaan…",
|
||||
"saveLocale": "{locale} opslaan",
|
||||
"saved": "Vertalingen opgeslagen en toegepast.",
|
||||
"readOnly": "Je kunt vertalingen bekijken en exporteren. Bewerken vereist toestemming om instellingen te wijzigen.",
|
||||
"language": "Taal",
|
||||
"coverage": "{total} sleutels · {missing} ontbreken · {identical} gelijk aan Engels",
|
||||
"search": "Zoeken",
|
||||
"searchPlaceholder": "Sleutel, vertaling of Engelse brontekst…",
|
||||
"group": "Groep",
|
||||
"allGroups": "Alle groepen",
|
||||
"status": "Tonen",
|
||||
"all": "Alle teksten",
|
||||
"missing": "Ontbrekende vertalingen",
|
||||
"identical": "Gelijk aan Engels",
|
||||
"modified": "Niet-opgeslagen wijzigingen",
|
||||
"fallbackHint": "Ontbrekende vertalingen gebruiken Engels. Identieke tekst kan bewust zijn gekozen. Laat de namen van variabelen tussen accolades ongewijzigd.",
|
||||
"unsaved": "{count} niet-opgeslagen wijzigingen in deze taal",
|
||||
"discard": "Wijzigingen verwerpen",
|
||||
"discardConfirm": "Niet-opgeslagen wijzigingen in deze taal verwerpen?",
|
||||
"fixErrors": "Corrigeer {count} ongeldige teksten voordat je opslaat.",
|
||||
"noResults": "Geen teksten gevonden met deze filters.",
|
||||
"source": "Engelse brontekst",
|
||||
"missingPlaceholder": "Vertaling ontbreekt; Engels wordt gebruikt.",
|
||||
"page": "Pagina {page} van {total} · {count} sleutels",
|
||||
"previous": "Vorige",
|
||||
"next": "Volgende",
|
||||
"leaveWarning": "Je hebt niet-opgeslagen vertalingen. Deze pagina verlaten?",
|
||||
"conflict": "Deze vertalingen zijn gewijzigd sinds je de editor opende. Exporteer je wijzigingen en herlaad de pagina voordat je opslaat.",
|
||||
"unknownKey": "Onbekende vertaalsleutel: {key}",
|
||||
"empty": "Vul een vertaling in voor {key} of verwerp deze wijziging.",
|
||||
"invalid": "Ongeldige berichtsyntaxis in {key}. Controleer accolades en tekstopmaaktags.",
|
||||
"variables": "Behoud dezelfde variabelenamen en tekstopmaaktags als de Engelse bron in {key}."
|
||||
}
|
||||
},
|
||||
"hubs": {
|
||||
|
||||
@@ -1708,7 +1708,7 @@
|
||||
"noNowPlayingHint": "ingen API-sett som nå spilles",
|
||||
"noListenersHint": "ingen lyttere API satt",
|
||||
"endpointDiagnostics": "Endepunktdiagnostikk",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> tilgjengelig · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-full. bg-[var(--color-primary)]/18 text-[var(--color-text)]\">ADVARSEL</span> ikke konfigurert · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(>-farge mislyktes] / ER-farge mislyktes ELLER.",
|
||||
"diagnosticsLegend": "OK: tilgjengelig · WARN: ikke konfigurert · ERROR: forespørselen mislyktes eller fikk tidsavbrudd.",
|
||||
"streamUrl": "Strøm URL",
|
||||
"unreachable": "uoppnåelig",
|
||||
"refresh": "Oppdater"
|
||||
@@ -1767,7 +1767,7 @@
|
||||
"noStreamUrlText": "Det er ingen radio_stream_url satt, så det er ingenting å bygge inn ennå.",
|
||||
"goToSettings": "Gå til Radioinnstillinger",
|
||||
"livePreview": "Live forhåndsvisning",
|
||||
"livePreviewText": "Dette er den nakne <audio>-spilleren slik besøkende vil se den. Trykk på play for å bekrefte at strømmen er tilgjengelig.",
|
||||
"livePreviewText": "Dette er den nakne audio-spilleren slik besøkende vil se den. Trykk på play for å bekrefte at strømmen er tilgjengelig.",
|
||||
"source": "Kilde",
|
||||
"audioSnippet": "Utdrag av lydspiller",
|
||||
"audioSnippetDesc": "Drop-in HTML5-lydspiller pekte rett på strømmen. Fungerer overalt hvor vanlig HTML er tillatt.",
|
||||
@@ -1998,7 +1998,7 @@
|
||||
"title": "Varsler",
|
||||
"subtitle": "Send varsler for hele hotellet og se nylige varsler",
|
||||
"sendAlert": "Send hotellvarsel",
|
||||
"recentAlerts": "Nylige varsler",
|
||||
"recentAlerts": "Nylige varsler ({count})",
|
||||
"noRecentAlerts": "Ingen nylige varsler",
|
||||
"form": {
|
||||
"title": "Varseltittel",
|
||||
@@ -2209,7 +2209,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Importer Tillatelser",
|
||||
"importHint": "Paste a JSON array of Tillatelse objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of Tillatelse objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Importer",
|
||||
"exportTitle": "Export Tillatelser",
|
||||
@@ -2346,7 +2346,7 @@
|
||||
"presetsTitle": "Forhåndsinnstillinger for tema",
|
||||
"presetsSubtitle": "Velg et forhåndsbygd fargevalg",
|
||||
"applyPreset": "Søk",
|
||||
"presetApplied": "Forhåndsinnstilling brukt",
|
||||
"presetApplied": "Forhåndsinnstilling brukt: {preset}",
|
||||
"customiseTitle": "Egendefinerte farger",
|
||||
"primary": "Primær",
|
||||
"secondary": "Sekundær",
|
||||
|
||||
@@ -1709,7 +1709,7 @@
|
||||
"noNowPlayingHint": "brak aktualnie odtwarzanego zestawu API",
|
||||
"noListenersHint": "brak zestawu API odbiorników",
|
||||
"endpointDiagnostics": "Diagnostyka punktów końcowych",
|
||||
"diagnosticsLegend": "<span class=\"inline-block tekst-[0.72rem] czcionka-bold px-2 py-0.5 zaokrąglona-pełna bg-green-600/20 tekst-[var(--color-text)]\">OK</span> osiągalna · <span class=\"inline-block tekst-[0.72rem] czcionka-bold px-2 py-0.5 zaokrąglona-pełna bg-[var(--color-primary)]/18 tekst-[var(--color-text)]\">WARN</span> nie skonfigurowano · <span class=\"inline-block text-[0.72rem] Font-bold px-2 py-0.5 zaokrąglony-pełny bg-red-500/22 tekst-[var(--color-text)]\">BŁĄD</span> pobieranie nie powiodło się / przekroczyło czas na zewnątrz.",
|
||||
"diagnosticsLegend": "OK: dostępne · WARN: nieskonfigurowane · ERROR: błąd żądania lub przekroczony limit czasu.",
|
||||
"streamUrl": "Adres URL strumienia",
|
||||
"unreachable": "nieosiągalny",
|
||||
"refresh": "Odśwież"
|
||||
@@ -1768,7 +1768,7 @@
|
||||
"noStreamUrlText": "Nie ma ustawionego radio_stream_url, więc nie ma jeszcze czego osadzać.",
|
||||
"goToSettings": "Przejdź do Ustawień radia",
|
||||
"livePreview": "Podgląd na żywo",
|
||||
"livePreviewText": "To jest goły odtwarzacz <audio>, jaki zobaczą odwiedzający. Naciśnij przycisk odtwarzania, aby potwierdzić, że strumień jest osiągalny.",
|
||||
"livePreviewText": "To jest goły odtwarzacz audio, jaki zobaczą odwiedzający. Naciśnij przycisk odtwarzania, aby potwierdzić, że strumień jest osiągalny.",
|
||||
"source": "Źródło",
|
||||
"audioSnippet": "Fragment odtwarzacza audio",
|
||||
"audioSnippetDesc": "Wpuszczany odtwarzacz audio HTML5 skierowany bezpośrednio na strumień. Działa wszędzie tam, gdzie dozwolony jest zwykły HTML.",
|
||||
@@ -1999,7 +1999,7 @@
|
||||
"title": "Alerty",
|
||||
"subtitle": "Wysyłaj alerty dotyczące całego hotelu i przeglądaj najnowsze alerty",
|
||||
"sendAlert": "Wyślij alert hotelowy",
|
||||
"recentAlerts": "Ostatnie alerty",
|
||||
"recentAlerts": "Ostatnie alerty ({count})",
|
||||
"noRecentAlerts": "Brak ostatnich alertów",
|
||||
"form": {
|
||||
"title": "Tytuł alertu",
|
||||
@@ -2210,7 +2210,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Importuj Uprawnienia",
|
||||
"importHint": "Paste a JSON array of Pozwolenie objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of Pozwolenie objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Importuj",
|
||||
"exportTitle": "Export Uprawnienia",
|
||||
@@ -2347,7 +2347,7 @@
|
||||
"presetsTitle": "Wstępne ustawienia motywu",
|
||||
"presetsSubtitle": "Wybierz gotowy schemat kolorów",
|
||||
"applyPreset": "Zastosuj",
|
||||
"presetApplied": "Zastosowano ustawienie wstępne",
|
||||
"presetApplied": "Zastosowano ustawienie wstępne: {preset}",
|
||||
"customiseTitle": "Kolory niestandardowe",
|
||||
"primary": "Podstawowy",
|
||||
"secondary": "Drugorzędne",
|
||||
|
||||
@@ -1709,7 +1709,7 @@
|
||||
"noNowPlayingHint": "nenhum conjunto de API em execução",
|
||||
"noListenersHint": "nenhuma API de ouvinte definida",
|
||||
"endpointDiagnostics": "Diagnóstico de endpoint",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 round-full bg-green-600/20 text-[var(--color-text)]\">OK</span> alcançável · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 arredondado-completo bg-[var(--color-primary)]/18 text-[var(--color-text)]\">WARN</span> não configurado · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 round-full bg-red-500/22 text-[var(--color-text)]\">ERROR</span> busca falhou/expirou o tempo limite.",
|
||||
"diagnosticsLegend": "OK: acessível · WARN: não configurado · ERROR: falha no pedido ou tempo esgotado.",
|
||||
"streamUrl": "URL de transmissão",
|
||||
"unreachable": "inacessível",
|
||||
"refresh": "Atualizar"
|
||||
@@ -1768,7 +1768,7 @@
|
||||
"noStreamUrlText": "Não há radio_stream_url definido, então não há nada para incorporar ainda.",
|
||||
"goToSettings": "Vá para configurações de rádio",
|
||||
"livePreview": "Visualização ao vivo",
|
||||
"livePreviewText": "Este é o player <audio> simples, como os visitantes o verão. Pressione play para confirmar que o stream está acessível.",
|
||||
"livePreviewText": "Este é o player audio simples, como os visitantes o verão. Pressione play para confirmar que o stream está acessível.",
|
||||
"source": "Fonte",
|
||||
"audioSnippet": "Snippet do player de áudio",
|
||||
"audioSnippetDesc": "Reprodutor de áudio HTML5 drop-in apontado diretamente para o stream. Funciona em qualquer lugar onde HTML simples seja permitido.",
|
||||
@@ -1999,7 +1999,7 @@
|
||||
"title": "Alertas",
|
||||
"subtitle": "Envie alertas para todo o hotel e visualize alertas recentes",
|
||||
"sendAlert": "Enviar alerta de hotel",
|
||||
"recentAlerts": "Alertas recentes",
|
||||
"recentAlerts": "Alertas recentes ({count})",
|
||||
"noRecentAlerts": "Nenhum alerta recente",
|
||||
"form": {
|
||||
"title": "Título do alerta",
|
||||
@@ -2210,7 +2210,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Importar Permissões",
|
||||
"importHint": "Paste a JSON array of Permissão objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of Permissão objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Importar",
|
||||
"exportTitle": "Export Permissões",
|
||||
@@ -2347,7 +2347,7 @@
|
||||
"presetsTitle": "Predefinições de tema",
|
||||
"presetsSubtitle": "Escolha um esquema de cores pré-construído",
|
||||
"applyPreset": "Aplicar",
|
||||
"presetApplied": "Predefinição aplicada",
|
||||
"presetApplied": "Predefinição aplicada: {preset}",
|
||||
"customiseTitle": "Cores personalizadas",
|
||||
"primary": "Primário",
|
||||
"secondary": "Secundário",
|
||||
|
||||
@@ -1708,7 +1708,7 @@
|
||||
"noNowPlayingHint": "nici un set API care se joacă acum",
|
||||
"noListenersHint": "nu se setează API pentru ascultători",
|
||||
"endpointDiagnostics": "Diagnosticarea punctului final",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> accesibil · <span class=\"inline-block text-[0.72rem] font-bold-full-pxl.5 bg-[var(--color-primary)]/18 text-[var(--color-text)]\">AVERTISMENT</span> neconfigurat · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(]> text-[var](---> text-[var](--) eșuat-[var (---> text-[var](--)",
|
||||
"diagnosticsLegend": "OK: accesibil · WARN: neconfigurat · ERROR: cerere eșuată sau timp expirat.",
|
||||
"streamUrl": "Adresa URL a fluxului",
|
||||
"unreachable": "de neatins",
|
||||
"refresh": "Reîmprospătați"
|
||||
@@ -1767,7 +1767,7 @@
|
||||
"noStreamUrlText": "Nu există nicio setare radio_stream_url, așa că încă nu există nimic de încorporat.",
|
||||
"goToSettings": "Accesați Setările radio",
|
||||
"livePreview": "Previzualizare live",
|
||||
"livePreviewText": "Acesta este playerul <audio> gol, așa cum îl vor vedea vizitatorii. Apăsați pe redare pentru a confirma că fluxul este accesibil.",
|
||||
"livePreviewText": "Acesta este playerul audio gol, așa cum îl vor vedea vizitatorii. Apăsați pe redare pentru a confirma că fluxul este accesibil.",
|
||||
"source": "Sursa",
|
||||
"audioSnippet": "Fragment de player audio",
|
||||
"audioSnippetDesc": "Playerul audio HTML5 integrat a îndreptat direct spre flux. Funcționează oriunde este permis HTML simplu.",
|
||||
@@ -1998,7 +1998,7 @@
|
||||
"title": "Alerte",
|
||||
"subtitle": "Trimiteți alerte la nivel de hotel și vizualizați alertele recente",
|
||||
"sendAlert": "Trimiteți o alertă la hotel",
|
||||
"recentAlerts": "Alerte recente",
|
||||
"recentAlerts": "Alerte recente ({count})",
|
||||
"noRecentAlerts": "Nu există alerte recente",
|
||||
"form": {
|
||||
"title": "Titlul alertei",
|
||||
@@ -2209,7 +2209,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Import Permisiuni",
|
||||
"importHint": "Paste a JSON array of Permisiune objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of Permisiune objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Import",
|
||||
"exportTitle": "Export Permisiuni",
|
||||
@@ -2346,7 +2346,7 @@
|
||||
"presetsTitle": "presetări teme",
|
||||
"presetsSubtitle": "Alegeți o schemă de culori prefabricată",
|
||||
"applyPreset": "Aplicați",
|
||||
"presetApplied": "Presetarea aplicată",
|
||||
"presetApplied": "Presetarea aplicată: {preset}",
|
||||
"customiseTitle": "Culori personalizate",
|
||||
"primary": "Primar",
|
||||
"secondary": "Secundar",
|
||||
|
||||
@@ -1707,7 +1707,7 @@
|
||||
"noNowPlayingHint": "no now-playing API set",
|
||||
"noListenersHint": "no listeners API set",
|
||||
"endpointDiagnostics": "Endpoint diagnostics",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> reachable · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]\">WARN</span> not configured · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">ERROR</span> fetch failed / timed out.",
|
||||
"diagnosticsLegend": "OK: доступно · WARN: не настроено · ERROR: ошибка запроса или истекло время ожидания.",
|
||||
"streamUrl": "Stream URL",
|
||||
"unreachable": "unreachable",
|
||||
"refresh": "Refresh"
|
||||
@@ -1766,7 +1766,7 @@
|
||||
"noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.",
|
||||
"goToSettings": "Go to Radio Settings",
|
||||
"livePreview": "Live preview",
|
||||
"livePreviewText": "This is the bare <audio> player as visitors will see it. Press play to confirm the stream is reachable.",
|
||||
"livePreviewText": "This is the bare audio player as visitors will see it. Press play to confirm the stream is reachable.",
|
||||
"source": "Source",
|
||||
"audioSnippet": "Audio player snippet",
|
||||
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
|
||||
@@ -1997,7 +1997,7 @@
|
||||
"title": "Alerts",
|
||||
"subtitle": "Send hotel-wide alerts and view recent alerts",
|
||||
"sendAlert": "Send hotel alert",
|
||||
"recentAlerts": "Recent alerts",
|
||||
"recentAlerts": "Recent alerts ({count})",
|
||||
"noRecentAlerts": "No recent alerts",
|
||||
"form": {
|
||||
"title": "Alert title",
|
||||
@@ -2208,7 +2208,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Импорт Разрешения",
|
||||
"importHint": "Paste a JSON array of permission objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of permission objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Импорт",
|
||||
"exportTitle": "Export Разрешения",
|
||||
@@ -2345,7 +2345,7 @@
|
||||
"presetsTitle": "Theme presets",
|
||||
"presetsSubtitle": "Choose a pre-built colour scheme",
|
||||
"applyPreset": "Apply",
|
||||
"presetApplied": "Preset applied",
|
||||
"presetApplied": "Preset applied: {preset}",
|
||||
"customiseTitle": "Custom colours",
|
||||
"primary": "Primary",
|
||||
"secondary": "Secondary",
|
||||
|
||||
@@ -1708,7 +1708,7 @@
|
||||
"noNowPlayingHint": "nie je teraz nastavené rozhranie API",
|
||||
"noListenersHint": "nie je nastavené žiadne rozhranie API poslucháčov",
|
||||
"endpointDiagnostics": "Diagnostika koncového bodu",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> dosiahnuteľné · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-primaryl]-1 bg-8 rounded text-[var(--color-text)]\">UPOZORNENIE</span> nie je nakonfigurované · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">CHYBA</span> načítanie zlyhalo / vypršal časový limit.",
|
||||
"diagnosticsLegend": "OK: dostupné · WARN: nenastavené · ERROR: požiadavka zlyhala alebo vypršal časový limit.",
|
||||
"streamUrl": "Webová adresa streamu",
|
||||
"unreachable": "nedosiahnuteľný",
|
||||
"refresh": "Obnoviť"
|
||||
@@ -1767,7 +1767,7 @@
|
||||
"noStreamUrlText": "Nie je nastavená žiadna adresa_url rádia, takže zatiaľ nie je čo vkladať.",
|
||||
"goToSettings": "Prejdite do časti Nastavenia rádia",
|
||||
"livePreview": "Živá ukážka",
|
||||
"livePreviewText": "Toto je holý <audio> prehrávač, ako ho uvidia návštevníci. Stlačením tlačidla prehrávania potvrďte, že stream je dostupný.",
|
||||
"livePreviewText": "Toto je holý audio prehrávač, ako ho uvidia návštevníci. Stlačením tlačidla prehrávania potvrďte, že stream je dostupný.",
|
||||
"source": "Zdroj",
|
||||
"audioSnippet": "Útržok zvukového prehrávača",
|
||||
"audioSnippetDesc": "Zvukový prehrávač HTML5 nasmerovaný priamo na stream. Funguje všade, kde je povolený obyčajný HTML.",
|
||||
@@ -1998,7 +1998,7 @@
|
||||
"title": "Upozornenia",
|
||||
"subtitle": "Odosielajte upozornenia pre celý hotel a zobrazujte posledné upozornenia",
|
||||
"sendAlert": "Odoslať upozornenie na hotel",
|
||||
"recentAlerts": "Nedávne upozornenia",
|
||||
"recentAlerts": "Nedávne upozornenia ({count})",
|
||||
"noRecentAlerts": "Žiadne nedávne upozornenia",
|
||||
"form": {
|
||||
"title": "Názov upozornenia",
|
||||
@@ -2209,7 +2209,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Importovať Povolenia",
|
||||
"importHint": "Paste a JSON array of Povolenie objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of Povolenie objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Importovať",
|
||||
"exportTitle": "Export Povolenia",
|
||||
@@ -2346,7 +2346,7 @@
|
||||
"presetsTitle": "Prednastavenia tém",
|
||||
"presetsSubtitle": "Vyberte si vopred zostavenú farebnú schému",
|
||||
"applyPreset": "Použiť",
|
||||
"presetApplied": "Predvoľba bola použitá",
|
||||
"presetApplied": "Predvoľba bola použitá: {preset}",
|
||||
"customiseTitle": "Vlastné farby",
|
||||
"primary": "Primárne",
|
||||
"secondary": "Sekundárne",
|
||||
|
||||
@@ -1708,7 +1708,7 @@
|
||||
"noNowPlayingHint": "нема скупа АПИ-ја који се тренутно репродукује",
|
||||
"noListenersHint": "није постављен АПИ слушалаца",
|
||||
"endpointDiagnostics": "Ендпоинт диагностицс",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">ОК</span> доступан · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]\">УПОЗОРЕЊЕ</span> није конфигурисано · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">ERROR</span> преузимање није успело / време је истекло.",
|
||||
"diagnosticsLegend": "OK: доступно · WARN: није подешено · ERROR: захтев није успео или је истекао.",
|
||||
"streamUrl": "УРЛ стрима",
|
||||
"unreachable": "недостижан",
|
||||
"refresh": "Освежи"
|
||||
@@ -1767,7 +1767,7 @@
|
||||
"noStreamUrlText": "Радио_стреам_урл није постављен, тако да још нема шта да се угради.",
|
||||
"goToSettings": "Идите на Подешавања радија",
|
||||
"livePreview": "Преглед уживо",
|
||||
"livePreviewText": "Ово је голи <audio> плејер како ће га посетиоци видети. Притисните плаи да бисте потврдили да је стрим доступан.",
|
||||
"livePreviewText": "Ово је голи audio плејер како ће га посетиоци видети. Притисните плаи да бисте потврдили да је стрим доступан.",
|
||||
"source": "Извор",
|
||||
"audioSnippet": "Исечак аудио плејера",
|
||||
"audioSnippetDesc": "Улазни ХТМЛ5 аудио плејер усмерен право на ток. Ради свуда где је обичан ХТМЛ дозвољен.",
|
||||
@@ -1998,7 +1998,7 @@
|
||||
"title": "Алертс",
|
||||
"subtitle": "Шаљите упозорења широм хотела и погледајте недавна упозорења",
|
||||
"sendAlert": "Пошаљите хотелско обавештење",
|
||||
"recentAlerts": "Недавна упозорења",
|
||||
"recentAlerts": "Недавна упозорења ({count})",
|
||||
"noRecentAlerts": "Нема недавних упозорења",
|
||||
"form": {
|
||||
"title": "Наслов упозорења",
|
||||
@@ -2209,7 +2209,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Увоз Дозволе",
|
||||
"importHint": "Paste a JSON array of Дозвола objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of Дозвола objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Увоз",
|
||||
"exportTitle": "Export Дозволе",
|
||||
@@ -2346,7 +2346,7 @@
|
||||
"presetsTitle": "Унапред подешене теме",
|
||||
"presetsSubtitle": "Choose a pre-built colour scheme",
|
||||
"applyPreset": "Пријавите се",
|
||||
"presetApplied": "Пресет је примењен",
|
||||
"presetApplied": "Пресет је примењен: {preset}",
|
||||
"customiseTitle": "Прилагођене боје",
|
||||
"primary": "Примарни",
|
||||
"secondary": "Секундарни",
|
||||
|
||||
@@ -1708,7 +1708,7 @@
|
||||
"noNowPlayingHint": "ingen nu-spelande API-uppsättning",
|
||||
"noListenersHint": "ingen lyssnars API inställd",
|
||||
"endpointDiagnostics": "Slutpunktsdiagnostik",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> nås · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-full. bg-[var(--color-primary)]/18 text-[var(--color-text)]\">VARNING</span> inte konfigurerad · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text) /hämtad tid utebliven text-[var(--färg-R-text])",
|
||||
"diagnosticsLegend": "OK: tillgänglig · WARN: inte konfigurerad · ERROR: begäran misslyckades eller tog för lång tid.",
|
||||
"streamUrl": "Streama URL",
|
||||
"unreachable": "oåtkomlig",
|
||||
"refresh": "Uppdatera"
|
||||
@@ -1767,7 +1767,7 @@
|
||||
"noStreamUrlText": "Det finns ingen radio_stream_url inställd, så det finns inget att bädda in ännu.",
|
||||
"goToSettings": "Gå till Radioinställningar",
|
||||
"livePreview": "Live förhandsvisning",
|
||||
"livePreviewText": "Detta är den blotta <audio>-spelaren som besökare kommer att se den. Tryck på play för att bekräfta att streamen är tillgänglig.",
|
||||
"livePreviewText": "Detta är den blotta audio-spelaren som besökare kommer att se den. Tryck på play för att bekräfta att streamen är tillgänglig.",
|
||||
"source": "Källa",
|
||||
"audioSnippet": "Utdrag från ljudspelare",
|
||||
"audioSnippetDesc": "Drop-in HTML5-ljudspelare pekade rakt mot strömmen. Fungerar överallt där vanlig HTML är tillåtet.",
|
||||
@@ -1998,7 +1998,7 @@
|
||||
"title": "Varningar",
|
||||
"subtitle": "Skicka varningar för hela hotellet och visa de senaste varningarna",
|
||||
"sendAlert": "Skicka hotellvarning",
|
||||
"recentAlerts": "Senaste varningar",
|
||||
"recentAlerts": "Senaste varningar ({count})",
|
||||
"noRecentAlerts": "Inga senaste varningar",
|
||||
"form": {
|
||||
"title": "Varningsrubrik",
|
||||
@@ -2209,7 +2209,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Importera Behörigheter",
|
||||
"importHint": "Paste a JSON array of Tillstånd objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of Tillstånd objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Importera",
|
||||
"exportTitle": "Export Behörigheter",
|
||||
@@ -2346,7 +2346,7 @@
|
||||
"presetsTitle": "Förinställda tema",
|
||||
"presetsSubtitle": "Välj ett förbyggt färgschema",
|
||||
"applyPreset": "Ansök",
|
||||
"presetApplied": "Förinställning tillämpas",
|
||||
"presetApplied": "Förinställning tillämpas: {preset}",
|
||||
"customiseTitle": "Anpassade färger",
|
||||
"primary": "Primär",
|
||||
"secondary": "Sekundär",
|
||||
|
||||
@@ -1709,7 +1709,7 @@
|
||||
"noNowPlayingHint": "şu anda oynatılan API seti yok",
|
||||
"noListenersHint": "dinleyici API seti yok",
|
||||
"endpointDiagnostics": "Uç nokta teşhisi",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">Tamam</span> ulaşılabilir · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]\">WARN</span> yapılandırılmadı · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">ERROR</span> getirme başarısız oldu / zaman aşımına uğradı.",
|
||||
"diagnosticsLegend": "OK: erişilebilir · WARN: yapılandırılmamış · ERROR: istek başarısız veya zaman aşımına uğradı.",
|
||||
"streamUrl": "Akış URL'si",
|
||||
"unreachable": "ulaşılamaz",
|
||||
"refresh": "Yenile"
|
||||
@@ -1768,7 +1768,7 @@
|
||||
"noStreamUrlText": "Radio_stream_url kümesi yok, bu nedenle henüz eklenecek bir şey yok.",
|
||||
"goToSettings": "Radyo Ayarlarına Git",
|
||||
"livePreview": "Canlı önizleme",
|
||||
"livePreviewText": "Bu, ziyaretçilerin göreceği şekliyle çıplak <audio> oynatıcısıdır. Akışın erişilebilir olduğunu onaylamak için oynat tuşuna basın.",
|
||||
"livePreviewText": "Bu, ziyaretçilerin göreceği şekliyle çıplak audio oynatıcısıdır. Akışın erişilebilir olduğunu onaylamak için oynat tuşuna basın.",
|
||||
"source": "Kaynak",
|
||||
"audioSnippet": "Ses oynatıcı snippet'i",
|
||||
"audioSnippetDesc": "Açılan HTML5 ses oynatıcısı doğrudan akışa işaret ediyordu. Düz HTML'ye izin verilen her yerde çalışır.",
|
||||
@@ -1999,7 +1999,7 @@
|
||||
"title": "Uyarılar",
|
||||
"subtitle": "Otel genelinde uyarılar gönderin ve son uyarıları görüntüleyin",
|
||||
"sendAlert": "Otel uyarısı gönder",
|
||||
"recentAlerts": "Son uyarılar",
|
||||
"recentAlerts": "Son uyarılar ({count})",
|
||||
"noRecentAlerts": "Yeni uyarı yok",
|
||||
"form": {
|
||||
"title": "Uyarı başlığı",
|
||||
@@ -2210,7 +2210,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "İthalat İzinler",
|
||||
"importHint": "Paste a JSON array of İzin objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of İzin objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "İthalat",
|
||||
"exportTitle": "Export İzinler",
|
||||
@@ -2347,7 +2347,7 @@
|
||||
"presetsTitle": "Tema ön ayarları",
|
||||
"presetsSubtitle": "Önceden oluşturulmuş bir renk şeması seçin",
|
||||
"applyPreset": "Uygula",
|
||||
"presetApplied": "Ön ayar uygulandı",
|
||||
"presetApplied": "Ön ayar uygulandı: {preset}",
|
||||
"customiseTitle": "Özel renkler",
|
||||
"primary": "Birincil",
|
||||
"secondary": "İkincil",
|
||||
|
||||
@@ -1708,7 +1708,7 @@
|
||||
"noNowPlayingHint": "не налаштовано API поточного відтворення",
|
||||
"noListenersHint": "API слухачів не встановлено",
|
||||
"endpointDiagnostics": "Діагностика кінцевої точки",
|
||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> доступно · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]\">WARN</span> не налаштовано · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">ПОМИЛКА</span> вибірка не вдалася / час очікування минув.",
|
||||
"diagnosticsLegend": "OK: доступно · WARN: не налаштовано · ERROR: помилка запиту або вичерпано час очікування.",
|
||||
"streamUrl": "URL-адреса потоку",
|
||||
"unreachable": "недоступний",
|
||||
"refresh": "Оновити"
|
||||
@@ -1767,7 +1767,7 @@
|
||||
"noStreamUrlText": "Немає параметра radio_stream_url, тому поки що немає чого вставляти.",
|
||||
"goToSettings": "Перейдіть до налаштувань радіо",
|
||||
"livePreview": "Попередній перегляд",
|
||||
"livePreviewText": "Це простий програвач <audio>, як його побачать відвідувачі. Натисніть кнопку відтворення, щоб підтвердити, що потік доступний.",
|
||||
"livePreviewText": "Це простий програвач audio, як його побачать відвідувачі. Натисніть кнопку відтворення, щоб підтвердити, що потік доступний.",
|
||||
"source": "Джерело",
|
||||
"audioSnippet": "Фрагмент аудіоплеєра",
|
||||
"audioSnippetDesc": "Вставний аудіопрогравач HTML5 спрямований прямо на потік. Працює скрізь, де дозволено простий HTML.",
|
||||
@@ -1998,7 +1998,7 @@
|
||||
"title": "Оповіщення",
|
||||
"subtitle": "Надсилайте сповіщення для всього готелю та переглядайте останні сповіщення",
|
||||
"sendAlert": "Надіслати сповіщення готелю",
|
||||
"recentAlerts": "Останні сповіщення",
|
||||
"recentAlerts": "Останні сповіщення ({count})",
|
||||
"noRecentAlerts": "Немає останніх сповіщень",
|
||||
"form": {
|
||||
"title": "Назва сповіщення",
|
||||
@@ -2209,7 +2209,7 @@
|
||||
"tabTest": "Test",
|
||||
"tabAudit": "Audit log",
|
||||
"importPermissions": "Імпорт Дозволи",
|
||||
"importHint": "Paste a JSON array of Дозвіл objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
|
||||
"importHint": "Paste a JSON array of Дозвіл objects. Format: ['{' \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" '}']",
|
||||
"importPlaceholder": "Paste JSON here…",
|
||||
"import": "Імпорт",
|
||||
"exportTitle": "Export Дозволи",
|
||||
@@ -2346,7 +2346,7 @@
|
||||
"presetsTitle": "Попередні налаштування теми",
|
||||
"presetsSubtitle": "Виберіть попередньо створену колірну схему",
|
||||
"applyPreset": "Застосувати",
|
||||
"presetApplied": "Попереднє налаштування застосовано",
|
||||
"presetApplied": "Попереднє налаштування застосовано: {preset}",
|
||||
"customiseTitle": "Нестандартні кольори",
|
||||
"primary": "Первинний",
|
||||
"secondary": "Вторинний",
|
||||
|
||||
Reference in new issue
Block a user