fix(i18n): persist CMS translations and validate message catalogs
CI / check (push) Successful in 1m14s
CI / deploy (push) Successful in 1m27s

This commit is contained in:
Simo committed 2026-09-06 17:55:11 +02:00
1 parent 96234075f8
commit ef94646d60
37 files changed
+1621 -463

No files matched your search

+3
View File
@@ -37,6 +37,9 @@ jobs:
- name: Lint - name: Lint
run: pnpm biome:lint run: pnpm biome:lint
- name: CMS translation contracts
run: pnpm i18n:check
- name: Typecheck - name: Typecheck
run: pnpm typecheck run: pnpm typecheck
+30
View File
@@ -0,0 +1,30 @@
# CMS translation audit and editor
The CMS editor at `/admin/translations/cms` now uses the same bundled catalogs as the request-time translator. Runtime changes are stored separately in `storage/cms-translations/<locale>.json`, inside the existing persistent `/app/storage` mount. No source-file write, environment-variable change or rebuild is required to apply an edit.
Only overrides are saved. Unchanged messages continue to receive updates from Git. Saves use a file lock, an atomic replacement and a revision check; a stale editor cannot overwrite another operator's changes. ICU syntax, argument names, rich-text tags and allowed keys are checked server-side. Invalid or obsolete overrides are excluded when reading a new release. Storage read errors are reported to the CMS error monitor and public pages fall back to bundled text.
The page starts in the operator's language. It shows all English reference keys, including missing translations, and supports search by key, translated text or English source. Filters separate missing, identical and modified text. Drafts survive language switches and failed saves. Users without `SETTINGS_EDIT` can review and export but cannot save.
## Audit outcome, 6 September 2026
- Scanned 25 JSON catalogs; 22 languages are selectable. The small Arabic, Finnish and Japanese catalogs are legacy files and remain outside the supported locale list.
- Repaired 66 malformed ICU messages across the 22 active catalogs, including HTML fragments and unescaped JSON examples.
- Added 199 missing English reference keys used by page components, with Italian translations, and fixed the incorrect navigation namespace in the admin error page.
- Completed the 31 previously missing Italian reference keys.
- Repaired missing `count` and `preset` variables in other locales.
- Final checks: zero malformed messages, zero argument/tag mismatches and zero missing references among the statically resolved translation calls.
- English contains 3,423 reference keys. Italian covers all of them; 629 values match English. Dutch is missing 524 reference keys and has 618 identical values. Matching English can be intentional for names and technical labels; this is not proof of translation quality.
- Found 1,577 literal JSX text candidates outside translation calls. These include labels, technical strings and names; they are an editorial inventory, not 1,577 confirmed bugs. The largest concentrations are the catalog item table (118), Studio main component (79), import audit (53), sound management (50) and permission editor (42).
## Repeatable checks
- `pnpm i18n:check`: fails on malformed messages, incompatible variables/tags, empty messages, source parsing errors or missing statically referenced keys. Runs in Gitea CI.
- `pnpm i18n:audit`: prints coverage and findings.
- `node scripts/audit-cms-translations.mjs --json`: full machine-readable inventory, including file and line references for literal JSX candidates.
Static analysis resolves literal translator namespaces and literal message keys. Dynamic key construction, prose embedded in arbitrary JavaScript strings, and the linguistic accuracy of all 22 translations still require targeted review. English fallback remains explicit; copying English into other catalogs would hide untranslated entries and is intentionally avoided.
## Validation
Automated tests exercise message syntax, actual translator output, persistent overrides, invalid-message rejection, revision conflicts and reset behavior. A browser fixture mounts the real editor and verifies missing-key editing, validation, failed-save preservation, language switching, successful saves, read-only access and mobile layout. Server actions are simulated in that fixture; authenticated production editing requires a staff session.
+7 -3
View File
@@ -30,7 +30,9 @@
"prepare": "node scripts/prepare-hooks.mjs", "prepare": "node scripts/prepare-hooks.mjs",
"assets:editor": "node scripts/copy-editor-assets.mjs", "assets:editor": "node scripts/copy-editor-assets.mjs",
"deps:audit": "pnpm audit --audit-level=high", "deps:audit": "pnpm audit --audit-level=high",
"analyze": "next experimental-analyze" "analyze": "next experimental-analyze",
"i18n:check": "node scripts/audit-cms-translations.mjs --check",
"i18n:audit": "node scripts/audit-cms-translations.mjs"
}, },
"lint-staged": { "lint-staged": {
"*.{js,ts,jsx,tsx,json}": "biome check --write --no-errors-on-unmatched", "*.{js,ts,jsx,tsx,json}": "biome check --write --no-errors-on-unmatched",
@@ -41,6 +43,7 @@
"@dnd-kit/core": "6.3.1", "@dnd-kit/core": "6.3.1",
"@dnd-kit/sortable": "10.0.0", "@dnd-kit/sortable": "10.0.0",
"@dnd-kit/utilities": "3.2.2", "@dnd-kit/utilities": "3.2.2",
"@formatjs/icu-messageformat-parser": "3.5.17",
"@hookform/resolvers": "5.9.1", "@hookform/resolvers": "5.9.1",
"@tanstack/react-virtual": "3.14.10", "@tanstack/react-virtual": "3.14.10",
"class-variance-authority": "0.7.1", "class-variance-authority": "0.7.1",
@@ -72,10 +75,11 @@
"sharp": "^0.35.4", "sharp": "^0.35.4",
"sonner": "2.0.8", "sonner": "2.0.8",
"tailwind-merge": "3.6.0", "tailwind-merge": "3.6.0",
"zod": "4.5.4", "tinymce": "8.9.0",
"tinymce": "8.9.0" "zod": "4.5.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/parser": "7.29.8",
"@biomejs/biome": "2.5.12", "@biomejs/biome": "2.5.12",
"@playwright/test": "^1.62.1", "@playwright/test": "^1.62.1",
"@tailwindcss/forms": "0.5.11", "@tailwindcss/forms": "0.5.11",
+6
View File
@@ -25,6 +25,9 @@ importers:
'@dnd-kit/utilities': '@dnd-kit/utilities':
specifier: 3.2.2 specifier: 3.2.2
version: 3.2.2([email protected]) version: 3.2.2([email protected])
'@formatjs/icu-messageformat-parser':
specifier: 3.5.17
version: 3.5.17
'@hookform/resolvers': '@hookform/resolvers':
specifier: 5.9.1 specifier: 5.9.1
version: 5.9.1(@standard-schema/[email protected])([email protected]([email protected]))([email protected]) version: 5.9.1(@standard-schema/[email protected])([email protected]([email protected]))([email protected])
@@ -125,6 +128,9 @@ importers:
specifier: 4.5.4 specifier: 4.5.4
version: 4.5.4 version: 4.5.4
devDependencies: devDependencies:
'@babel/parser':
specifier: 7.29.8
version: 7.29.8
'@biomejs/biome': '@biomejs/biome':
specifier: 2.5.12 specifier: 2.5.12
version: 2.5.12 version: 2.5.12
+189
View File
@@ -0,0 +1,189 @@
import fs from "node:fs";
import path from "node:path";
import * as babel from "@babel/parser";
import { parse } from "@formatjs/icu-messageformat-parser";
const flat = (obj, p = "", r = {}) => {
for (const [k, v] of Object.entries(obj)) {
const key = p ? `${p}.${k}` : k;
if (v && typeof v === "object") flat(v, key, r);
else r[key] = v;
}
return r;
};
const messages = Object.fromEntries(
fs
.readdirSync("src/messages")
.filter((n) => n.endsWith(".json"))
.map((n) => [
n.slice(0, -5),
flat(JSON.parse(fs.readFileSync(`src/messages/${n}`, "utf8"))),
]),
);
const base = messages.en,
invalid = [],
mismatches = [];
function vars(ast, r = new Set()) {
for (const n of ast) {
if ([1, 2, 3, 4, 5, 6, 8].includes(n.type)) r.add(n.value);
if (n.options) for (const o of Object.values(n.options)) vars(o.value, r);
if (n.children) vars(n.children, r);
}
return [...r].sort().join(",");
}
const signatures = {};
for (const [k, v] of Object.entries(base)) {
try {
signatures[k] = vars(parse(v));
} catch {}
}
const locales = Object.entries(messages).map(([locale, m]) => {
for (const [k, v] of Object.entries(m)) {
try {
const sig = vars(parse(v));
if (k in signatures && sig !== signatures[k])
mismatches.push({
locale,
key: k,
expected: signatures[k],
actual: sig,
value: v,
});
} catch (e) {
invalid.push({ locale, key: k, value: v, error: e.message });
}
}
return {
locale,
total: Object.keys(m).length,
missing: Object.keys(base).filter((k) => !(k in m)).length,
empty: Object.values(m).filter((v) => !v?.trim()).length,
sameAsEnglish: Object.keys(base).filter((k) => m[k] === base[k]).length,
};
});
const missingCalls = [],
hardcoded = [],
unparsedFiles = [];
function walk(n, fn) {
if (!n || typeof n !== "object") return;
fn(n);
for (const [k, v] of Object.entries(n)) {
if (k === "loc" || k === "extra") continue;
if (Array.isArray(v))
v.forEach((x) => {
walk(x, fn);
});
else if (v && typeof v === "object") walk(v, fn);
}
}
for (const f of fs
.readdirSync("src", { recursive: true })
.filter((f) => /\.(tsx|ts)$/.test(f) && !f.includes(".test."))) {
let ast;
try {
ast = babel.parse(fs.readFileSync(path.join("src", f), "utf8"), {
sourceType: "module",
plugins: ["typescript", "jsx"],
});
} catch (error) {
unparsedFiles.push({ file: f, error: error.message });
continue;
}
const bindings = {};
walk(ast, (n) => {
if (n.type === "VariableDeclarator" && n.id.type === "Identifier") {
let c = n.init;
if (c?.type === "AwaitExpression") c = c.argument;
if (
c?.type === "CallExpression" &&
["useTranslations", "getTranslations"].includes(c.callee.name)
) {
const arg = c.arguments[0];
const ns =
arg?.type === "StringLiteral"
? arg.value
: arg?.type === "ObjectExpression"
? arg.properties.find((p) => p.key?.name === "namespace")?.value
?.value
: arg
? null
: "";
if (ns !== null) {
bindings[n.id.name] ??= new Set();
bindings[n.id.name].add(ns);
}
}
}
});
walk(ast, (n) => {
if (n.type === "CallExpression") {
const name = n.callee.name ?? n.callee.object?.name;
const ns = bindings[name];
const k = n.arguments[0];
if (ns?.size === 1 && k?.type === "StringLiteral") {
const full = [...ns][0] ? `${[...ns][0]}.${k.value}` : k.value;
if (
!(full in base) &&
!Object.keys(base).some((b) => b.startsWith(`${full}.`))
)
missingCalls.push({ file: f, line: n.loc.start.line, key: full });
}
}
if (n.type === "JSXText" && /[A-Za-z]{3}/.test(n.value.trim()))
hardcoded.push({
file: f,
line: n.loc.start.line,
text: n.value.trim().replace(/\s+/g, " "),
});
});
}
const result = {
locales,
invalid,
mismatches,
missingCalls,
hardcoded,
unparsedFiles,
};
if (process.argv.includes("--json"))
console.log(JSON.stringify(result, null, 2));
else {
console.table(locales);
console.log(
"Invalid ICU:",
invalid.length,
"Variable mismatches:",
mismatches.length,
"Missing static message references:",
missingCalls.length,
);
console.log(
"Literal JSX text candidates requiring editorial review:",
hardcoded.length,
);
console.log(
"Coverage checks static translator namespaces and literal keys; dynamic keys and semantic translation quality require review.",
);
if (
invalid.length ||
mismatches.length ||
missingCalls.length ||
unparsedFiles.length
)
console.log(
JSON.stringify(
{ invalid, mismatches, missingCalls, unparsedFiles },
null,
2,
),
);
}
if (
process.argv.includes("--check") &&
(invalid.length ||
mismatches.length ||
missingCalls.length ||
unparsedFiles.length ||
locales.some((locale) => locale.empty > 0))
)
process.exitCode = 1;
+25 -38
View File
@@ -3,60 +3,47 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import * as JSONC from "jsonc-parser"; import * as JSONC from "jsonc-parser";
import { revalidatePath } from "next/cache";
import { getTranslations } from "next-intl/server";
import { z } from "zod"; import { z } from "zod";
import { SUPPORTED_LOCALES } from "@/i18n/locales";
import { import {
CLIENT_TRANSLATION_FILES, CLIENT_TRANSLATION_FILES,
getClientTranslationFile, getClientTranslationFile,
} from "@/lib/client-translation-files"; } from "@/lib/client-translation-files";
import {
CmsTranslationError,
saveCmsTranslation,
} from "@/lib/cms-translations";
import { patchJson5 } from "@/lib/json5-patch"; import { patchJson5 } from "@/lib/json5-patch";
import { PERMS } from "@/lib/permissions"; import { PERMS } from "@/lib/permissions";
import { adminAction } from "@/lib/safe-action"; import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared"; import { ActionError, actionOk } from "@/lib/safe-action-shared";
const saveTranslationsSchema = z.object({ const saveTranslationsSchema = z.object({
locale: z.enum([ locale: z.enum(SUPPORTED_LOCALES),
"en", revision: z.string().regex(/^[a-f0-9]{64}$/),
"it", changes: z
"nl", .record(z.string().max(300), z.string().max(20000).nullable())
"de", .refine((value) => Object.keys(value).length <= 5000),
"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()),
}); });
export const saveTranslations = adminAction( export const saveTranslations = adminAction(
{ permission: PERMS.SETTINGS_EDIT, schema: saveTranslationsSchema }, { permission: PERMS.SETTINGS_EDIT, schema: saveTranslationsSchema },
async (ctx) => { async (ctx) => {
const filePath = path.join( try {
process.cwd(), const snapshot = await saveCmsTranslation(
"src", ctx.data.locale,
"messages", ctx.data.revision,
`${ctx.data.locale}.json`, ctx.data.changes,
); );
await fs.writeFile( revalidatePath("/", "layout");
filePath, return actionOk({ snapshot });
JSON.stringify(ctx.data.data, null, 2), } catch (error) {
"utf-8", if (!(error instanceof CmsTranslationError)) throw error;
); const t = await getTranslations("pages.admin.translations.cms");
return { ok: false, error: t(error.code, { key: error.key }) };
return actionOk(); }
}, },
); );
+1 -1
View File
@@ -18,7 +18,7 @@ export default function AdminErrorPage({
reset: () => void; reset: () => void;
}) { }) {
const t = useTranslations("pages.error"); const t = useTranslations("pages.error");
const tNav = useTranslations("admin.nav"); const tNav = useTranslations("pages.admin.nav");
const [reference, setReference] = useState(error.digest); const [reference, setReference] = useState(error.digest);
useEffect(() => { useEffect(() => {
+19 -43
View File
@@ -1,48 +1,24 @@
import fs from "node:fs/promises"; import { getLocale } from "next-intl/server";
import path from "node:path"; import { SUPPORTED_LOCALES } from "@/i18n/locales";
import { readCmsTranslation } from "@/lib/cms-translations";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { TranslationsClient } from "../translations-client"; 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() { export default async function CmsTranslationsPage() {
const messagesDir = path.join(process.cwd(), "src", "messages"); const [{ session, permissions }, locale, entries] = await Promise.all([
getAdminContext(),
const contents: Record<string, string> = {}; getLocale(),
await Promise.all( Promise.all(
LOCALES.map(async (locale) => { SUPPORTED_LOCALES.map(
try { async (locale) => [locale, await readCmsTranslation(locale)] as const,
contents[locale] = await fs.readFile( ),
path.join(messagesDir, `${locale}.json`), ),
"utf-8", ]);
); return (
} catch { <TranslationsClient
// Skip missing locale files instead of failing the whole page. snapshots={Object.fromEntries(entries)}
} initialLocale={locale}
}), canEdit={canAccess(permissions, PERMS.SETTINGS_EDIT, session.user.rank)}
/>
); );
return <TranslationsClient localeContents={contents} />;
} }
+271 -169
View File
@@ -1,132 +1,127 @@
"use client"; "use client";
import { Download, Save } from "lucide-react"; import { Download, Save } from "lucide-react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { saveTranslations } from "@/actions/translations"; import { saveTranslations } from "@/actions/translations";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { useServerAction } from "@/hooks/use-server-action"; import { useServerAction } from "@/hooks/use-server-action";
import { flattenObject, unflattenObject } from "@/lib/translations-utils"; import { useUnsavedChanges } from "@/hooks/use-unsaved-changes";
import {
type Locale = type AppLocale,
| "en" isSupportedLocale,
| "it" SUPPORTED_LOCALES,
| "nl" } from "@/i18n/locales";
| "de" import { validateCmsMessage } from "@/lib/cms-message-validation";
| "fr" import type { CmsTranslationSnapshot } from "@/lib/cms-translations";
| "es" import { unflattenObject } from "@/lib/translations-utils";
| "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" },
];
interface Props { interface Props {
localeContents: Record<string, string>; snapshots: Record<string, CmsTranslationSnapshot>;
initialLocale: string;
canEdit: boolean;
} }
const PER_PAGE = 30;
export function TranslationsClient({ localeContents }: Props) { export function TranslationsClient({
const [data, setData] = useState<Record<string, Record<string, string>>>( snapshots,
() => { initialLocale,
const initial: Record<string, Record<string, string>> = {}; canEdit,
for (const [locale, content] of Object.entries(localeContents)) { }: Props) {
initial[locale] = flattenObject(JSON.parse(content)); const t = useTranslations("pages.admin.translations.cms");
} const router = useRouter();
return initial; 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 [search, setSearch] = useState("");
const [group, setGroup] = useState(""); const [group, setGroup] = useState("");
const [activeLang, setActiveLang] = useState<Locale>("en"); const [status, setStatus] = useState("all");
const { run, isPending } = useServerAction();
const PER_PAGE = 30;
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const { run, isPending } = useServerAction();
const activeData = data[activeLang]; const source = saved.en.messages;
const activeData = saved[activeLang].messages;
const groups = useMemo(() => { const draft = drafts[activeLang] ?? {};
const g = new Set<string>(); const keys = useMemo(() => Object.keys(source).sort(), [source]);
Object.keys(data.en ?? {}).forEach((k) => { const groups = useMemo(
const parts = k.split("."); () => [...new Set(keys.map((key) => key.split(".")[0]))],
if (parts.length > 1) g.add(parts[0]); [keys],
}); );
return Array.from(g).sort(); const dirty = Object.values(drafts).some(
}, [data]); (values) => Object.keys(values).length > 0,
);
const filteredKeys = useMemo(() => { useUnsavedChanges(dirty, t("leaveWarning"));
if (!activeData) return []; const valueFor = (key: string) => draft[key] ?? activeData[key] ?? "";
return Object.keys(activeData).filter((key) => { const missing = keys.filter((key) => !valueFor(key).trim()).length;
const matchSearch = const identical =
!search || activeLang === "en"
key.toLowerCase().includes(search.toLowerCase()) || ? 0
activeData[key].toLowerCase().includes(search.toLowerCase()); : keys.filter((key) => valueFor(key) === source[key]).length;
const matchGroup = !group || key.startsWith(`${group}.`); const errors: Record<string, string> = {};
return matchSearch && matchGroup; 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() { function handleSave() {
if (!canEdit || !Object.keys(draft).length || Object.keys(errors).length)
return;
const locale = activeLang;
run( run(
() => () =>
saveTranslations({ saveTranslations({
locale: activeLang, locale,
data: unflattenObject(activeData), 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() { function handleExport() {
const blob = new Blob( const blob = new Blob(
[JSON.stringify(unflattenObject(activeData), null, 2)], [JSON.stringify(unflattenObject({ ...activeData, ...draft }), null, 2)],
{ { type: "application/json" },
type: "application/json",
},
); );
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
@@ -135,106 +130,213 @@ export function TranslationsClient({ localeContents }: Props) {
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
return ( return (
<div className="space-y-6"> <div className="space-y-5">
<div className="flex justify-end"> <div className="flex flex-wrap items-start justify-between gap-4">
<div className="flex gap-2"> <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}> <Button variant="outline" size="sm" onClick={handleExport}>
<Download className="w-4 h-4 mr-1" /> Export <Download className="mr-1 h-4 w-4" />
</Button> {t("export")}
<Button size="sm" onClick={handleSave} disabled={isPending}>
<Save className="w-4 h-4 mr-1" /> {isPending ? "Saving..." : "Save"}
</Button> </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> </div>
{!canEdit && (
<div className="flex gap-2 flex-wrap"> <p className="text-sm text-muted-foreground">{t("readOnly")}</p>
<div className="flex gap-1 overflow-x-auto pb-1 max-w-full"> )}
{LOCALES.map(({ code, label }) => ( <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 <Button
key={code} key={code}
variant={activeLang === code ? "default" : "outline"}
size="sm" size="sm"
className="shrink-0" variant={activeLang === code ? "default" : "outline"}
aria-pressed={activeLang === code}
onClick={() => { onClick={() => {
setActiveLang(code); setActiveLang(code);
setPage(1); setPage(1);
}} }}
> >
{label} {code.toUpperCase()}
{Object.keys(drafts[code] ?? {}).length ? " *" : ""}
</Button> </Button>
))} ))}
</div> </fieldset>
<Input <p className="text-sm" role="status">
placeholder="Filter by key or value..." {t("coverage", { total: keys.length, missing, identical })}
value={search} </p>
onChange={(e) => { <div className="flex flex-wrap items-end gap-3">
setSearch(e.target.value); <label
setPage(1); htmlFor="cms-translation-search"
}} className="grid w-full min-w-0 gap-1 text-sm md:w-auto md:flex-1"
className="max-w-xs" >
/> {t("search")}
<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>
<Input <Input
value={activeData[key]} id="cms-translation-search"
onChange={(e) => updateValue(key, e.target.value)} type="search"
className="flex-1 h-8 text-sm" 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>
))} )}
</div> <div className="divide-y rounded-lg border bg-card">
{pageKeys.length === 0 && (
{totalPages > 1 && ( <p className="p-6 text-sm text-muted-foreground">
<div className="flex items-center justify-between"> {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"> <span className="text-sm text-muted-foreground">
Page {page} of {totalPages} {t("page", {
page: currentPage,
total: totalPages,
count: filteredKeys.length,
})}
</span> </span>
<div className="flex gap-1"> <div className="flex gap-2">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
disabled={page <= 1} disabled={currentPage <= 1}
onClick={() => setPage(page - 1)} onClick={() => setPage(currentPage - 1)}
> >
Prev {t("previous")}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
disabled={page >= totalPages} disabled={currentPage >= totalPages}
onClick={() => setPage(page + 1)} onClick={() => setPage(currentPage + 1)}
> >
Next {t("next")}
</Button> </Button>
</div> </div>
</div> </div>
)} </fieldset>
</div> </div>
); );
} }
+39
View File
@@ -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([]);
});
});
+32
View File
@@ -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
View File
@@ -1,41 +1,17 @@
import { cookies, headers } from "next/headers"; import { cookies, headers } from "next/headers";
import { getRequestConfig } from "next-intl/server"; import { getRequestConfig } from "next-intl/server";
import { loadCmsMessages } from "@/lib/cms-translations";
import { mergeMessages } from "./merge-messages"; import { mergeMessages } from "./merge-messages";
// i18n WITHOUT routing: the locale is chosen by a `NEXT_LOCALE` cookie, then // i18n WITHOUT routing: the locale is chosen by a `NEXT_LOCALE` cookie, then
// the Accept-Language header, and finally English as fallback. // the Accept-Language header, and finally English as fallback.
export const SUPPORTED_LOCALES = [ export {
"en", DEFAULT_LOCALE,
"it", isSupportedLocale,
"nl", SUPPORTED_LOCALES,
"de", } from "./locales";
"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( import { type AppLocale, DEFAULT_LOCALE, isSupportedLocale } from "./locales";
value: string | undefined,
): value is AppLocale {
return !!value && (SUPPORTED_LOCALES as readonly string[]).includes(value);
}
function detectLocaleFromAcceptLanguage( function detectLocaleFromAcceptLanguage(
acceptLanguage: string | null, acceptLanguage: string | null,
@@ -73,14 +49,10 @@ export default getRequestConfig(async () => {
locale = detected ?? DEFAULT_LOCALE; locale = detected ?? DEFAULT_LOCALE;
} }
// Safe: `locale` is validated against SUPPORTED_LOCALES above (only en/it/nl/de/fr/es). const [messages, fallback] = await Promise.all([
const messages = (await import(`../messages/${locale}.json`)).default; loadCmsMessages(locale),
// English is the source of truth; fall back to it for any key missing from a loadCmsMessages(DEFAULT_LOCALE),
// translation so the UI never shows a raw key path. ]);
const fallback =
locale === DEFAULT_LOCALE
? messages
: (await import(`../messages/${DEFAULT_LOCALE}.json`)).default;
return { return {
locale, locale,
@@ -108,6 +80,7 @@ export default getRequestConfig(async () => {
if (typeof fromEnglish === "string") return fromEnglish; if (typeof fromEnglish === "string") return fromEnglish;
const seg = key.split(".").pop() ?? key; const seg = key.split(".").pop() ?? key;
return seg return seg
.replace(/([a-z])([A-Z])/g, "$1 $2")
.replace(/[._-]/g, " ") .replace(/[._-]/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase()); .replace(/\b\w/g, (c) => c.toUpperCase());
}, },
+36
View File
@@ -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";
}
}
+108
View File
@@ -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");
});
});
+133
View File
@@ -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;
}
},
);
+5 -5
View File
@@ -1708,7 +1708,7 @@
"noNowPlayingHint": "няма зададен API за игра в момента", "noNowPlayingHint": "няма зададен API за игра в момента",
"noListenersHint": "няма зададен API за слушатели", "noListenersHint": "няма зададен API за слушатели",
"endpointDiagnostics": "Диагностика на крайна точка", "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 адрес на потока", "streamUrl": "URL адрес на потока",
"unreachable": "недостижим", "unreachable": "недостижим",
"refresh": "Опресняване" "refresh": "Опресняване"
@@ -1767,7 +1767,7 @@
"noStreamUrlText": "Няма зададен radio_stream_url, така че все още няма нищо за вграждане.", "noStreamUrlText": "Няма зададен radio_stream_url, така че все още няма нищо за вграждане.",
"goToSettings": "Отидете на Радио настройки", "goToSettings": "Отидете на Радио настройки",
"livePreview": "Преглед на живо", "livePreview": "Преглед на живо",
"livePreviewText": "Това е голият <audio> плейър, както ще го видят посетителите. Натиснете възпроизвеждане, за да потвърдите, че потокът е достъпен.", "livePreviewText": "Това е голият audio плейър, както ще го видят посетителите. Натиснете възпроизвеждане, за да потвърдите, че потокът е достъпен.",
"source": "Източник", "source": "Източник",
"audioSnippet": "Фрагмент от аудио плейър", "audioSnippet": "Фрагмент от аудио плейър",
"audioSnippetDesc": "Вмъкващ се HTML5 аудио плейър, насочен право към потока. Работи навсякъде, където е разрешен обикновен HTML.", "audioSnippetDesc": "Вмъкващ се HTML5 аудио плейър, насочен право към потока. Работи навсякъде, където е разрешен обикновен HTML.",
@@ -1998,7 +1998,7 @@
"title": "Сигнали", "title": "Сигнали",
"subtitle": "Изпращайте сигнали за целия хотел и вижте последните сигнали", "subtitle": "Изпращайте сигнали за целия хотел и вижте последните сигнали",
"sendAlert": "Изпратете сигнал за хотел", "sendAlert": "Изпратете сигнал за хотел",
"recentAlerts": "Последни сигнали", "recentAlerts": "Последни сигнали ({count})",
"noRecentAlerts": "Няма скорошни сигнали", "noRecentAlerts": "Няма скорошни сигнали",
"form": { "form": {
"title": "Заглавие на предупреждение", "title": "Заглавие на предупреждение",
@@ -2209,7 +2209,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Импортиране Разрешения", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Импортиране", "import": "Импортиране",
"exportTitle": "Export Разрешения", "exportTitle": "Export Разрешения",
@@ -2346,7 +2346,7 @@
"presetsTitle": "Предварително зададени теми", "presetsTitle": "Предварително зададени теми",
"presetsSubtitle": "Изберете предварително изградена цветова схема", "presetsSubtitle": "Изберете предварително изградена цветова схема",
"applyPreset": "Кандидатствайте", "applyPreset": "Кандидатствайте",
"presetApplied": "Предварителната настройка е приложена", "presetApplied": "Предварителната настройка е приложена: {preset}",
"customiseTitle": "Персонализирани цветове", "customiseTitle": "Персонализирани цветове",
"primary": "Първичен", "primary": "Първичен",
"secondary": "Вторичен", "secondary": "Вторичен",
+5 -5
View File
@@ -1708,7 +1708,7 @@
"noNowPlayingHint": "žádná nyní přehrávající sada API", "noNowPlayingHint": "žádná nyní přehrávající sada API",
"noListenersHint": "není nastaveno žádné rozhraní API pro posluchače", "noListenersHint": "není nastaveno žádné rozhraní API pro posluchače",
"endpointDiagnostics": "Diagnostika koncových bodů", "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", "streamUrl": "Adresa URL streamu",
"unreachable": "nedosažitelný", "unreachable": "nedosažitelný",
"refresh": "Obnovit" "refresh": "Obnovit"
@@ -1767,7 +1767,7 @@
"noStreamUrlText": "Neexistuje žádná sada radio_stream_url, takže zatím není co vkládat.", "noStreamUrlText": "Neexistuje žádná sada radio_stream_url, takže zatím není co vkládat.",
"goToSettings": "Přejděte do Nastavení rádia", "goToSettings": "Přejděte do Nastavení rádia",
"livePreview": "Živý náhled", "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", "source": "Zdroj",
"audioSnippet": "Úryvek audio přehrávače", "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.", "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í", "title": "Upozornění",
"subtitle": "Odesílejte upozornění pro celý hotel a prohlížejte si nedávná upozornění", "subtitle": "Odesílejte upozornění pro celý hotel a prohlížejte si nedávná upozornění",
"sendAlert": "Odeslat upozornění na hotel", "sendAlert": "Odeslat upozornění na hotel",
"recentAlerts": "Nedávná upozornění", "recentAlerts": "Nedávná upozornění ({count})",
"noRecentAlerts": "Žádná nedávná upozornění", "noRecentAlerts": "Žádná nedávná upozornění",
"form": { "form": {
"title": "Název upozornění", "title": "Název upozornění",
@@ -2209,7 +2209,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Importovat Oprávnění", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Importovat", "import": "Importovat",
"exportTitle": "Export Oprávnění", "exportTitle": "Export Oprávnění",
@@ -2346,7 +2346,7 @@
"presetsTitle": "Předvolby motivu", "presetsTitle": "Předvolby motivu",
"presetsSubtitle": "Vyberte předem připravené barevné schéma", "presetsSubtitle": "Vyberte předem připravené barevné schéma",
"applyPreset": "Použít", "applyPreset": "Použít",
"presetApplied": "Předvolba použita", "presetApplied": "Předvolba použita: {preset}",
"customiseTitle": "Vlastní barvy", "customiseTitle": "Vlastní barvy",
"primary": "Primární", "primary": "Primární",
"secondary": "Sekundární", "secondary": "Sekundární",
+5 -5
View File
@@ -1708,7 +1708,7 @@
"noNowPlayingHint": "intet API-sæt, der nu spiller", "noNowPlayingHint": "intet API-sæt, der nu spiller",
"noListenersHint": "no listeners API set", "noListenersHint": "no listeners API set",
"endpointDiagnostics": "Slutpunktsdiagnostik", "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", "streamUrl": "Stream URL",
"unreachable": "unreachable", "unreachable": "unreachable",
"refresh": "Opdater" "refresh": "Opdater"
@@ -1767,7 +1767,7 @@
"noStreamUrlText": "Der er ingen radio_stream_url sat, så der er intet at indlejre endnu.", "noStreamUrlText": "Der er ingen radio_stream_url sat, så der er intet at indlejre endnu.",
"goToSettings": "Gå til Radioindstillinger", "goToSettings": "Gå til Radioindstillinger",
"livePreview": "Live preview", "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", "source": "Kilde",
"audioSnippet": "Uddrag af lydafspiller", "audioSnippet": "Uddrag af lydafspiller",
"audioSnippetDesc": "Drop-in HTML5-lydafspiller pegede direkte mod strømmen. Virker overalt, hvor almindelig HTML er tilladt.", "audioSnippetDesc": "Drop-in HTML5-lydafspiller pegede direkte mod strømmen. Virker overalt, hvor almindelig HTML er tilladt.",
@@ -1998,7 +1998,7 @@
"title": "Advarsler", "title": "Advarsler",
"subtitle": "Send underretninger for hele hotellet, og se de seneste underretninger", "subtitle": "Send underretninger for hele hotellet, og se de seneste underretninger",
"sendAlert": "Send hotelalarm", "sendAlert": "Send hotelalarm",
"recentAlerts": "Seneste underretninger", "recentAlerts": "Seneste underretninger ({count})",
"noRecentAlerts": "No recent alerts", "noRecentAlerts": "No recent alerts",
"form": { "form": {
"title": "Alert title", "title": "Alert title",
@@ -2209,7 +2209,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Importer Tilladelser", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Importer", "import": "Importer",
"exportTitle": "Export Tilladelser", "exportTitle": "Export Tilladelser",
@@ -2346,7 +2346,7 @@
"presetsTitle": "Forudindstillinger af tema", "presetsTitle": "Forudindstillinger af tema",
"presetsSubtitle": "Vælg et forudbygget farveskema", "presetsSubtitle": "Vælg et forudbygget farveskema",
"applyPreset": "Ansøg", "applyPreset": "Ansøg",
"presetApplied": "Forudindstilling anvendt", "presetApplied": "Forudindstilling anvendt: {preset}",
"customiseTitle": "Brugerdefinerede farver", "customiseTitle": "Brugerdefinerede farver",
"primary": "Primær", "primary": "Primær",
"secondary": "Sekundær", "secondary": "Sekundær",
+5 -5
View File
@@ -1679,7 +1679,7 @@
"noNowPlayingHint": "no now-playing API set", "noNowPlayingHint": "no now-playing API set",
"noListenersHint": "no listeners API set", "noListenersHint": "no listeners API set",
"endpointDiagnostics": "Endpoint diagnostics", "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", "streamUrl": "Stream URL",
"unreachable": "unreachable", "unreachable": "unreachable",
"refresh": "Aktualisieren" "refresh": "Aktualisieren"
@@ -1738,7 +1738,7 @@
"noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.", "noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.",
"goToSettings": "Go to Radio Settings", "goToSettings": "Go to Radio Settings",
"livePreview": "Live preview", "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", "source": "Source",
"audioSnippet": "Audio player snippet", "audioSnippet": "Audio player snippet",
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.", "audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
@@ -1969,7 +1969,7 @@
"title": "Alarme", "title": "Alarme",
"subtitle": "Hotelweite Alarme senden und aktuelle anzeigen", "subtitle": "Hotelweite Alarme senden und aktuelle anzeigen",
"sendAlert": "Hotel-Alarm senden", "sendAlert": "Hotel-Alarm senden",
"recentAlerts": "Letzte Alarme", "recentAlerts": "Letzte Alarme ({count})",
"noRecentAlerts": "Keine aktuellen Alarme", "noRecentAlerts": "Keine aktuellen Alarme",
"form": { "form": {
"title": "Alarm-Titel", "title": "Alarm-Titel",
@@ -2180,7 +2180,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Importieren Berechtigungen", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Importieren", "import": "Importieren",
"exportTitle": "Export Berechtigungen", "exportTitle": "Export Berechtigungen",
@@ -2317,7 +2317,7 @@
"presetsTitle": "Design-Voreinstellungen", "presetsTitle": "Design-Voreinstellungen",
"presetsSubtitle": "Wähle ein vorgefertigtes Farbschema", "presetsSubtitle": "Wähle ein vorgefertigtes Farbschema",
"applyPreset": "Anwenden", "applyPreset": "Anwenden",
"presetApplied": "Voreinstellung angewendet", "presetApplied": "Voreinstellung angewendet: {preset}",
"customiseTitle": "Benutzerdefinierte Farben", "customiseTitle": "Benutzerdefinierte Farben",
"primary": "Primär", "primary": "Primär",
"secondary": "Sekundär", "secondary": "Sekundär",
+5 -5
View File
@@ -1708,7 +1708,7 @@
"noNowPlayingHint": "δεν υπάρχει σύνολο API που παίζει τώρα", "noNowPlayingHint": "δεν υπάρχει σύνολο API που παίζει τώρα",
"noListenersHint": "δεν έχει οριστεί API ακροατών", "noListenersHint": "δεν έχει οριστεί API ακροατών",
"endpointDiagnostics": "Διαγνωστικά τελικού σημείου", "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 ροής", "streamUrl": "URL ροής",
"unreachable": "απρόσιτος", "unreachable": "απρόσιτος",
"refresh": "Ανανέωση" "refresh": "Ανανέωση"
@@ -1767,7 +1767,7 @@
"noStreamUrlText": "Δεν υπάρχει set radio_stream_url, επομένως δεν υπάρχει τίποτα για ενσωμάτωση ακόμα.", "noStreamUrlText": "Δεν υπάρχει set radio_stream_url, επομένως δεν υπάρχει τίποτα για ενσωμάτωση ακόμα.",
"goToSettings": "Μεταβείτε στις Ρυθμίσεις ραδιοφώνου", "goToSettings": "Μεταβείτε στις Ρυθμίσεις ραδιοφώνου",
"livePreview": "Ζωντανή προεπισκόπηση", "livePreview": "Ζωντανή προεπισκόπηση",
"livePreviewText": "Αυτό είναι το γυμνό πρόγραμμα αναπαραγωγής <audio> όπως θα το δουν οι επισκέπτες. Πατήστε το play για να επιβεβαιώσετε ότι η ροή είναι προσβάσιμη.", "livePreviewText": "Αυτό είναι το γυμνό πρόγραμμα αναπαραγωγής audio όπως θα το δουν οι επισκέπτες. Πατήστε το play για να επιβεβαιώσετε ότι η ροή είναι προσβάσιμη.",
"source": "Πηγή", "source": "Πηγή",
"audioSnippet": "Απόσπασμα αναπαραγωγής ήχου", "audioSnippet": "Απόσπασμα αναπαραγωγής ήχου",
"audioSnippetDesc": "Το αναπτυσσόμενο πρόγραμμα αναπαραγωγής ήχου HTML5 κατευθείαν στη ροή. Λειτουργεί οπουδήποτε επιτρέπεται απλό HTML.", "audioSnippetDesc": "Το αναπτυσσόμενο πρόγραμμα αναπαραγωγής ήχου HTML5 κατευθείαν στη ροή. Λειτουργεί οπουδήποτε επιτρέπεται απλό HTML.",
@@ -1998,7 +1998,7 @@
"title": "Ειδοποιήσεις", "title": "Ειδοποιήσεις",
"subtitle": "Στείλτε ειδοποιήσεις σε όλο το ξενοδοχείο και δείτε πρόσφατες ειδοποιήσεις", "subtitle": "Στείλτε ειδοποιήσεις σε όλο το ξενοδοχείο και δείτε πρόσφατες ειδοποιήσεις",
"sendAlert": "Αποστολή ειδοποίησης ξενοδοχείου", "sendAlert": "Αποστολή ειδοποίησης ξενοδοχείου",
"recentAlerts": "Πρόσφατες ειδοποιήσεις", "recentAlerts": "Πρόσφατες ειδοποιήσεις ({count})",
"noRecentAlerts": "Δεν υπάρχουν πρόσφατες ειδοποιήσεις", "noRecentAlerts": "Δεν υπάρχουν πρόσφατες ειδοποιήσεις",
"form": { "form": {
"title": "Τίτλος ειδοποίησης", "title": "Τίτλος ειδοποίησης",
@@ -2209,7 +2209,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Εισαγωγή Άδειες", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Εισαγωγή", "import": "Εισαγωγή",
"exportTitle": "Export Άδειες", "exportTitle": "Export Άδειες",
@@ -2346,7 +2346,7 @@
"presetsTitle": "Προεπιλογές θεμάτων", "presetsTitle": "Προεπιλογές θεμάτων",
"presetsSubtitle": "Επιλέξτε έναν προκατασκευασμένο συνδυασμό χρωμάτων", "presetsSubtitle": "Επιλέξτε έναν προκατασκευασμένο συνδυασμό χρωμάτων",
"applyPreset": "Εφαρμόστε", "applyPreset": "Εφαρμόστε",
"presetApplied": "Εφαρμόστηκε η προεπιλογή", "presetApplied": "Εφαρμόστηκε η προεπιλογή: {preset}",
"customiseTitle": "Προσαρμοσμένα χρώματα", "customiseTitle": "Προσαρμοσμένα χρώματα",
"primary": "Πρωτοβάθμια", "primary": "Πρωτοβάθμια",
"secondary": "Δευτερεύουσα", "secondary": "Δευτερεύουσα",
+268 -32
View File
@@ -878,7 +878,8 @@
"codePlaceholder": "6-digit code", "codePlaceholder": "6-digit code",
"confirm": "Confirm", "confirm": "Confirm",
"enableIntro": "Add a second layer of security with an authenticator app.", "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": { "login": {
"title": "Sign in", "title": "Sign in",
@@ -1119,7 +1120,8 @@
"rankWithCount": "Rank {rank}: {count} accounts", "rankWithCount": "Rank {rank}: {count} accounts",
"recentStaffActivity": "Recent staff activity", "recentStaffActivity": "Recent staff activity",
"viewAll": "View all", "viewAll": "View all",
"noStaffActivityLogged": "No staff activity logged yet" "noStaffActivityLogged": "No staff activity logged yet",
"totalAccounts": "{count} total accounts"
}, },
"articles": { "articles": {
"title": "Articles", "title": "Articles",
@@ -1160,7 +1162,11 @@
"statusPublished": "Published", "statusPublished": "Published",
"publishAt": "Publish at", "publishAt": "Publish at",
"publishedAt": "Published at" "publishedAt": "Published at"
} },
"articlesShown": "Articles shown",
"latest": "Latest article",
"mostRecentPost": "Most recent publication",
"recentArticles": "Recent articles"
}, },
"events": { "events": {
"newEvent": "+ New event", "newEvent": "+ New event",
@@ -1281,7 +1287,13 @@
"colColour": "Colour", "colColour": "Colour",
"colUsedBy": "Used by", "colUsedBy": "Used by",
"colCreated": "Created", "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": { "navigation": {
"title": "Navigator", "title": "Navigator",
@@ -1305,7 +1317,12 @@
"colName": "Name", "colName": "Name",
"colOrder": "Order", "colOrder": "Order",
"colRooms": "Rooms", "colRooms": "Rooms",
"colActions": "Actions" "colActions": "Actions",
"publicFlatcats": "Public room categories",
"markedPublic": "Categories marked as public",
"visibleRooms": "Visible rooms",
"rows": "rows",
"unnamed": "Unnamed room"
}, },
"helpCenter": { "helpCenter": {
"title": "Help Center", "title": "Help Center",
@@ -1332,7 +1349,9 @@
"saving": "Saving…", "saving": "Saving…",
"deleteEntry": "Delete entry", "deleteEntry": "Delete entry",
"cancel": "Cancel" "cancel": "Cancel"
} },
"withButton": "With a button",
"smallBoxes": "Small boxes"
}, },
"ads": { "ads": {
"title": "Advertisements", "title": "Advertisements",
@@ -1394,8 +1413,17 @@
"save": "Save box", "save": "Save box",
"saving": "Saving…", "saving": "Saving…",
"deleteBox": "Delete box", "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": { "photos": {
"title": "Photo Moderation", "title": "Photo Moderation",
@@ -1522,7 +1550,20 @@
"save": "Save ban", "save": "Save ban",
"saving": "Saving…", "saving": "Saving…",
"cancel": "Cancel" "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": { "ip": {
"title": "IP Management", "title": "IP Management",
@@ -1542,7 +1583,11 @@
"colIp": "IP", "colIp": "IP",
"colReason": "Reason", "colReason": "Reason",
"colAdded": "Added", "colAdded": "Added",
"colActions": "Actions" "colActions": "Actions",
"whitelisted": "Allowed IP addresses",
"blacklisted": "Blocked IP addresses",
"asnPlaceholder": "ASN (optional)",
"colAsn": "ASN"
}, },
"vpn": { "vpn": {
"title": "VPN / Proxy Detection", "title": "VPN / Proxy Detection",
@@ -1555,7 +1600,21 @@
"blockOnDetectHint": "Automatically block users detected behind a VPN or proxy", "blockOnDetectHint": "Automatically block users detected behind a VPN or proxy",
"save": "Save settings", "save": "Save settings",
"saving": "Saving…", "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": { "tickets": {
"title": "CMS support desk", "title": "CMS support desk",
@@ -1685,7 +1744,10 @@
"status": "Status", "status": "Status",
"submitted": "Submitted", "submitted": "Submitted",
"noMessage": "No message provided" "noMessage": "No message provided"
} },
"applicationsTitle": "Applications ({count})",
"userNumber": "User #{id}",
"rankLabel": "Rank {rank}"
}, },
"teams": { "teams": {
"title": "Teams", "title": "Teams",
@@ -1713,7 +1775,26 @@
"save": "Save team definition", "save": "Save team definition",
"saving": "Saving…", "saving": "Saving…",
"cancel": "Cancel" "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": { "permissions": {
"title": "Permissions", "title": "Permissions",
@@ -1888,8 +1969,23 @@
"enabled": "Enabled", "enabled": "Enabled",
"save": "Save package", "save": "Save package",
"saving": "Saving…", "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": { "marketplace": {
"title": "Marketplace", "title": "Marketplace",
@@ -2056,7 +2152,15 @@
"colActions": "Actions", "colActions": "Actions",
"statusActive": "Active", "statusActive": "Active",
"statusCancelled": "Cancelled", "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": { "rareValues": {
"title": "Rare Values", "title": "Rare Values",
@@ -2095,8 +2199,24 @@
"pointTypeDiamonds": "Diamonds", "pointTypeDiamonds": "Diamonds",
"save": "Save item", "save": "Save item",
"saving": "Saving…", "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": { "badges": {
"title": "Badges", "title": "Badges",
@@ -2127,7 +2247,10 @@
"colImage": "Image", "colImage": "Image",
"colName": "Name", "colName": "Name",
"colDescription": "Description", "colDescription": "Description",
"colActions": "Actions" "colActions": "Actions",
"uploadedMessage": "Badge {code} uploaded.",
"errorMessage": "Upload failed: {error}",
"userIdPlaceholder": "User ID"
}, },
"achievements": { "achievements": {
"title": "Achievements", "title": "Achievements",
@@ -2141,7 +2264,14 @@
"colProgress": "Progress", "colProgress": "Progress",
"levelLabel": "Level {level}", "levelLabel": "Level {level}",
"rewardCredits": "{count} credits", "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": { "calendar": {
"title": "Calendar campaigns", "title": "Calendar campaigns",
@@ -2160,8 +2290,32 @@
"days": "Days", "days": "Days",
"dayLabel": "Day {day}", "dayLabel": "Day {day}",
"reward": "Reward", "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": { "radio": {
"title": "Radio Moderation", "title": "Radio Moderation",
@@ -2231,7 +2385,7 @@
"noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.", "noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.",
"goToSettings": "Go to Radio Settings", "goToSettings": "Go to Radio Settings",
"livePreview": "Live preview", "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", "source": "Source",
"audioSnippet": "Audio player snippet", "audioSnippet": "Audio player snippet",
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.", "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.", "pointsPerMinuteHint": "Amount awarded for each full minute of listening.",
"maxPerDayHint": "Daily cap per user. Use 0 for unlimited.", "maxPerDayHint": "Daily cap per user. Use 0 for unlimited.",
"minListenersHint": "Points are only awarded when at least this many people are tuned in.", "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": { "autoDjForm": {
"title": "Radio AutoDJ", "title": "Radio AutoDJ",
@@ -2408,7 +2563,7 @@
"noNowPlayingHint": "no now-playing API set", "noNowPlayingHint": "no now-playing API set",
"noListenersHint": "no listeners API set", "noListenersHint": "no listeners API set",
"endpointDiagnostics": "Endpoint diagnostics", "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", "streamUrl": "Stream URL",
"unreachable": "unreachable", "unreachable": "unreachable",
"refresh": "Refresh" "refresh": "Refresh"
@@ -2589,7 +2744,16 @@
"valuePlaceholder": "Enter value…", "valuePlaceholder": "Enter value…",
"save": "Save changes", "save": "Save changes",
"cancel": "Cancel" "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": { "alerts": {
"title": "Alerts", "title": "Alerts",
@@ -2857,7 +3021,25 @@
"save": "Save settings", "save": "Save settings",
"saving": "Saving…", "saving": "Saving…",
"confirmEnable": "Enabling maintenance will prevent all non-staff users from accessing the hotel. Continue?", "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": { "housekeeping": {
"title": "Permission archive", "title": "Permission archive",
@@ -2941,7 +3123,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Import permissions", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Import", "import": "Import",
"exportTitle": "Export permissions", "exportTitle": "Export permissions",
@@ -3058,8 +3240,13 @@
"variablesHint": "{username}, {email}, {hotel_name}", "variablesHint": "{username}, {email}, {hotel_name}",
"save": "Save template", "save": "Save template",
"saving": "Saving…", "saving": "Saving…",
"cancel": "Cancel" "cancel": "Cancel",
} "variablesPlaceholder": "Variables, separated by commas",
"active": "Active"
},
"create": "Create template",
"save": "Save template",
"updated": "Updated: {date}"
}, },
"theme": { "theme": {
"title": "Theme", "title": "Theme",
@@ -3085,7 +3272,12 @@
"saveCss": "Save CSS", "saveCss": "Save CSS",
"savingCss": "Saving…", "savingCss": "Saving…",
"reset": "Reset to defaults", "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": { "media": {
"title": "Media Library", "title": "Media Library",
@@ -3170,6 +3362,43 @@
"subtitle": "User-facing strings from emulator_settings (commands, wordfilter, pet/matchmaking messages).", "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.", "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" "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": { "settings": {
@@ -3299,7 +3528,14 @@
"pages": "Pages and tools", "pages": "Pages and tools",
"description": "Find an admin page or search hotel records.", "description": "Find an admin page or search hotel records.",
"loadError": "Records could not be loaded. You can still open the pages above.", "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": { "table": {
"search": "Search…", "search": "Search…",
+5 -5
View File
@@ -1679,7 +1679,7 @@
"noNowPlayingHint": "no now-playing API set", "noNowPlayingHint": "no now-playing API set",
"noListenersHint": "no listeners API set", "noListenersHint": "no listeners API set",
"endpointDiagnostics": "Endpoint diagnostics", "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", "streamUrl": "Stream URL",
"unreachable": "unreachable", "unreachable": "unreachable",
"refresh": "Actualizar" "refresh": "Actualizar"
@@ -1738,7 +1738,7 @@
"noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.", "noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.",
"goToSettings": "Go to Radio Settings", "goToSettings": "Go to Radio Settings",
"livePreview": "Live preview", "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", "source": "Source",
"audioSnippet": "Audio player snippet", "audioSnippet": "Audio player snippet",
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.", "audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
@@ -1969,7 +1969,7 @@
"title": "Alertas", "title": "Alertas",
"subtitle": "Enviar alertas a todo el hotel y ver alertas recientes", "subtitle": "Enviar alertas a todo el hotel y ver alertas recientes",
"sendAlert": "Enviar alerta al hotel", "sendAlert": "Enviar alerta al hotel",
"recentAlerts": "Alertas recientes", "recentAlerts": "Alertas recientes ({count})",
"noRecentAlerts": "No hay alertas recientes", "noRecentAlerts": "No hay alertas recientes",
"form": { "form": {
"title": "Título de la alerta", "title": "Título de la alerta",
@@ -2180,7 +2180,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Importar Permisos", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Importar", "import": "Importar",
"exportTitle": "Export Permisos", "exportTitle": "Export Permisos",
@@ -2317,7 +2317,7 @@
"presetsTitle": "Preajustes de tema", "presetsTitle": "Preajustes de tema",
"presetsSubtitle": "Elige un esquema de color predefinido", "presetsSubtitle": "Elige un esquema de color predefinido",
"applyPreset": "Aplicar", "applyPreset": "Aplicar",
"presetApplied": "Preajuste aplicado", "presetApplied": "Preajuste aplicado: {preset}",
"customiseTitle": "Colores personalizados", "customiseTitle": "Colores personalizados",
"primary": "Primario", "primary": "Primario",
"secondary": "Secundario", "secondary": "Secundario",
+5 -5
View File
@@ -1679,7 +1679,7 @@
"noNowPlayingHint": "no now-playing API set", "noNowPlayingHint": "no now-playing API set",
"noListenersHint": "no listeners API set", "noListenersHint": "no listeners API set",
"endpointDiagnostics": "Endpoint diagnostics", "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", "streamUrl": "Stream URL",
"unreachable": "unreachable", "unreachable": "unreachable",
"refresh": "Actualiser" "refresh": "Actualiser"
@@ -1738,7 +1738,7 @@
"noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.", "noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.",
"goToSettings": "Go to Radio Settings", "goToSettings": "Go to Radio Settings",
"livePreview": "Live preview", "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", "source": "Source",
"audioSnippet": "Audio player snippet", "audioSnippet": "Audio player snippet",
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.", "audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
@@ -1969,7 +1969,7 @@
"title": "Alertes", "title": "Alertes",
"subtitle": "Envoyer des alertes à tout l'hôtel et voir les alertes récentes", "subtitle": "Envoyer des alertes à tout l'hôtel et voir les alertes récentes",
"sendAlert": "Envoyer une alerte à l'hôtel", "sendAlert": "Envoyer une alerte à l'hôtel",
"recentAlerts": "Alertes récentes", "recentAlerts": "Alertes récentes ({count})",
"noRecentAlerts": "Aucune alerte récente", "noRecentAlerts": "Aucune alerte récente",
"form": { "form": {
"title": "Titre de l'alerte", "title": "Titre de l'alerte",
@@ -2180,7 +2180,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Importer permissions", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Importer", "import": "Importer",
"exportTitle": "Export permissions", "exportTitle": "Export permissions",
@@ -2317,7 +2317,7 @@
"presetsTitle": "Préréglages de thème", "presetsTitle": "Préréglages de thème",
"presetsSubtitle": "Choisir un jeu de couleurs prédéfini", "presetsSubtitle": "Choisir un jeu de couleurs prédéfini",
"applyPreset": "Appliquer", "applyPreset": "Appliquer",
"presetApplied": "Préréglage appliqué", "presetApplied": "Préréglage appliqué: {preset}",
"customiseTitle": "Couleurs personnalisées", "customiseTitle": "Couleurs personnalisées",
"primary": "Primaire", "primary": "Primaire",
"secondary": "Secondaire", "secondary": "Secondaire",
+5 -5
View File
@@ -1708,7 +1708,7 @@
"noNowPlayingHint": "nema postavljenog API-ja koji se sada igra", "noNowPlayingHint": "nema postavljenog API-ja koji se sada igra",
"noListenersHint": "nije postavljen API slušatelja", "noListenersHint": "nije postavljen API slušatelja",
"endpointDiagnostics": "Dijagnostika krajnje točke", "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", "streamUrl": "URL streama",
"unreachable": "nedostižan", "unreachable": "nedostižan",
"refresh": "Osvježi" "refresh": "Osvježi"
@@ -1767,7 +1767,7 @@
"noStreamUrlText": "Nije postavljen radio_stream_url, tako da još nema ništa za ugraditi.", "noStreamUrlText": "Nije postavljen radio_stream_url, tako da još nema ništa za ugraditi.",
"goToSettings": "Idite na Radio Settings", "goToSettings": "Idite na Radio Settings",
"livePreview": "Pregled uživo", "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", "source": "Izvor",
"audioSnippet": "Isječak audio playera", "audioSnippet": "Isječak audio playera",
"audioSnippetDesc": "Umetnuti HTML5 audio player usmjeren ravno prema streamu. Radi svugdje gdje je dopušten obični HTML.", "audioSnippetDesc": "Umetnuti HTML5 audio player usmjeren ravno prema streamu. Radi svugdje gdje je dopušten obični HTML.",
@@ -1998,7 +1998,7 @@
"title": "upozorenja", "title": "upozorenja",
"subtitle": "Šaljite upozorenja cijelom hotelu i pogledajte nedavna upozorenja", "subtitle": "Šaljite upozorenja cijelom hotelu i pogledajte nedavna upozorenja",
"sendAlert": "Pošalji hotelsko upozorenje", "sendAlert": "Pošalji hotelsko upozorenje",
"recentAlerts": "Nedavna upozorenja", "recentAlerts": "Nedavna upozorenja ({count})",
"noRecentAlerts": "Nema nedavnih upozorenja", "noRecentAlerts": "Nema nedavnih upozorenja",
"form": { "form": {
"title": "Naslov upozorenja", "title": "Naslov upozorenja",
@@ -2209,7 +2209,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Uvoz Dozvole", "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…", "importPlaceholder": "Paste JSON here…",
"import": "uvoz", "import": "uvoz",
"exportTitle": "Export Dozvole", "exportTitle": "Export Dozvole",
@@ -2346,7 +2346,7 @@
"presetsTitle": "Unaprijed postavljene teme", "presetsTitle": "Unaprijed postavljene teme",
"presetsSubtitle": "Odaberite unaprijed pripremljenu shemu boja", "presetsSubtitle": "Odaberite unaprijed pripremljenu shemu boja",
"applyPreset": "primijeniti", "applyPreset": "primijeniti",
"presetApplied": "Unaprijed postavljeno", "presetApplied": "Unaprijed postavljeno: {preset}",
"customiseTitle": "Prilagođene boje", "customiseTitle": "Prilagođene boje",
"primary": "Primarni", "primary": "Primarni",
"secondary": "Sekundarna", "secondary": "Sekundarna",
+5 -5
View File
@@ -1708,7 +1708,7 @@
"noNowPlayingHint": "nincs jelenleg lejátszott API-készlet", "noNowPlayingHint": "nincs jelenleg lejátszott API-készlet",
"noListenersHint": "nincs hallgató API beállítva", "noListenersHint": "nincs hallgató API beállítva",
"endpointDiagnostics": "Végpont diagnosztika", "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", "streamUrl": "Stream URL",
"unreachable": "elérhetetlen", "unreachable": "elérhetetlen",
"refresh": "Frissítés" "refresh": "Frissítés"
@@ -1767,7 +1767,7 @@
"noStreamUrlText": "Nincs beállítva radio_stream_url, így még nincs mit beágyazni.", "noStreamUrlText": "Nincs beállítva radio_stream_url, így még nincs mit beágyazni.",
"goToSettings": "Menjen a Rádióbeállításokhoz", "goToSettings": "Menjen a Rádióbeállításokhoz",
"livePreview": "Élő előnézet", "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", "source": "Forrás",
"audioSnippet": "Hanglejátszó részlet", "audioSnippet": "Hanglejátszó részlet",
"audioSnippetDesc": "A beugró HTML5 audiolejátszó egyenesen a streamre mutatott. Mindenhol működik, ahol a sima HTML megengedett.", "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", "title": "Figyelmeztetések",
"subtitle": "A szálloda egészére kiterjedő figyelmeztetések küldése és a legutóbbi figyelmeztetések megtekintése", "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", "sendAlert": "Szállodai figyelmeztetés küldése",
"recentAlerts": "Legutóbbi figyelmeztetések", "recentAlerts": "Legutóbbi figyelmeztetések ({count})",
"noRecentAlerts": "Nincsenek friss figyelmeztetések", "noRecentAlerts": "Nincsenek friss figyelmeztetések",
"form": { "form": {
"title": "Figyelmeztetés címe", "title": "Figyelmeztetés címe",
@@ -2209,7 +2209,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Importálás Engedélyek", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Importálás", "import": "Importálás",
"exportTitle": "Export Engedélyek", "exportTitle": "Export Engedélyek",
@@ -2346,7 +2346,7 @@
"presetsTitle": "Előre beállított téma", "presetsTitle": "Előre beállított téma",
"presetsSubtitle": "Válasszon egy előre elkészített színsémát", "presetsSubtitle": "Válasszon egy előre elkészített színsémát",
"applyPreset": "Alkalmazni", "applyPreset": "Alkalmazni",
"presetApplied": "Előbeállítás alkalmazva", "presetApplied": "Előbeállítás alkalmazva: {preset}",
"customiseTitle": "Egyedi színek", "customiseTitle": "Egyedi színek",
"primary": "Elsődleges", "primary": "Elsődleges",
"secondary": "Másodlagos", "secondary": "Másodlagos",
+306 -39
View File
@@ -848,7 +848,8 @@
"codePlaceholder": "Codice a 6 cifre", "codePlaceholder": "Codice a 6 cifre",
"confirm": "Conferma", "confirm": "Conferma",
"enableIntro": "Aggiungi un livello di sicurezza in più con un'app di autenticazione.", "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", "title": "Account collegati",
"subtitle": "Collega un account social per poter accedere con un clic", "subtitle": "Collega un account social per poter accedere con un clic",
@@ -906,7 +907,12 @@
"alreadyHaveAccountPre": "Hai già un account?", "alreadyHaveAccountPre": "Hai già un account?",
"alreadyHaveAccountLink": "Accedi", "alreadyHaveAccountLink": "Accedi",
"whoIsOnline": "Chi è online", "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": { "forgot": {
"title": "Reimposta password", "title": "Reimposta password",
@@ -1061,7 +1067,17 @@
"themeBuilder": "Editor del tema", "themeBuilder": "Editor del tema",
"catalogMaintenance": "Manutenzione catalogo", "catalogMaintenance": "Manutenzione catalogo",
"openSidebar": "Apri menu", "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": { "dashboard": {
"title": "Dashboard", "title": "Dashboard",
@@ -1078,7 +1094,8 @@
"rankWithCount": "Grado {rank}: {count} account", "rankWithCount": "Grado {rank}: {count} account",
"recentStaffActivity": "Attività recente dello staff", "recentStaffActivity": "Attività recente dello staff",
"viewAll": "Vedi tutto", "viewAll": "Vedi tutto",
"noStaffActivityLogged": "Nessuna attività dello staff ancora registrata" "noStaffActivityLogged": "Nessuna attività dello staff ancora registrata",
"totalAccounts": "{count} account totali"
}, },
"articles": { "articles": {
"title": "Articoli", "title": "Articoli",
@@ -1119,7 +1136,11 @@
"statusPublished": "Pubblicato", "statusPublished": "Pubblicato",
"publishAt": "Data di pubblicazione", "publishAt": "Data di pubblicazione",
"publishedAt": "Pubblicato il" "publishedAt": "Pubblicato il"
} },
"articlesShown": "Articoli mostrati",
"latest": "Ultimo articolo",
"mostRecentPost": "Pubblicazione più recente",
"recentArticles": "Articoli recenti"
}, },
"events": { "events": {
"newEvent": "+ Nuovo evento", "newEvent": "+ Nuovo evento",
@@ -1240,7 +1261,13 @@
"colColour": "Colore", "colColour": "Colore",
"colUsedBy": "Usato da", "colUsedBy": "Usato da",
"colCreated": "Creato", "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": { "navigation": {
"title": "Navigatore", "title": "Navigatore",
@@ -1264,7 +1291,12 @@
"colName": "Nome", "colName": "Nome",
"colOrder": "Ordine", "colOrder": "Ordine",
"colRooms": "Stanze", "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": { "helpCenter": {
"title": "Centro assistenza", "title": "Centro assistenza",
@@ -1291,7 +1323,9 @@
"saving": "Salvataggio…", "saving": "Salvataggio…",
"deleteEntry": "Elimina voce", "deleteEntry": "Elimina voce",
"cancel": "Annulla" "cancel": "Annulla"
} },
"withButton": "Con pulsante",
"smallBoxes": "Riquadri piccoli"
}, },
"ads": { "ads": {
"title": "Pubblicità", "title": "Pubblicità",
@@ -1354,8 +1388,17 @@
"save": "Salva box", "save": "Salva box",
"saving": "Salvataggio…", "saving": "Salvataggio…",
"deleteBox": "Elimina box", "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": { "photos": {
"title": "Moderazione foto", "title": "Moderazione foto",
@@ -1482,7 +1525,20 @@
"save": "Salva ban", "save": "Salva ban",
"saving": "Salvataggio…", "saving": "Salvataggio…",
"cancel": "Annulla" "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": { "ip": {
"title": "Gestione IP", "title": "Gestione IP",
@@ -1502,7 +1558,11 @@
"colIp": "IP", "colIp": "IP",
"colReason": "Motivo", "colReason": "Motivo",
"colAdded": "Aggiunto", "colAdded": "Aggiunto",
"colActions": "Azioni" "colActions": "Azioni",
"whitelisted": "Indirizzi IP consentiti",
"blacklisted": "Indirizzi IP bloccati",
"asnPlaceholder": "ASN (facoltativo)",
"colAsn": "ASN"
}, },
"vpn": { "vpn": {
"title": "Rilevamento VPN / Proxy", "title": "Rilevamento VPN / Proxy",
@@ -1515,7 +1575,21 @@
"blockOnDetectHint": "Blocca automaticamente gli utenti rilevati dietro VPN o proxy", "blockOnDetectHint": "Blocca automaticamente gli utenti rilevati dietro VPN o proxy",
"save": "Salva impostazioni", "save": "Salva impostazioni",
"saving": "Salvataggio…", "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": { "tickets": {
"title": "Supporto CMS", "title": "Supporto CMS",
@@ -1621,7 +1695,10 @@
"status": "Stato", "status": "Stato",
"submitted": "Inviata", "submitted": "Inviata",
"noMessage": "Nessun messaggio fornito" "noMessage": "Nessun messaggio fornito"
} },
"applicationsTitle": "Candidature ({count})",
"userNumber": "Utente #{id}",
"rankLabel": "Grado {rank}"
}, },
"teams": { "teams": {
"title": "Team", "title": "Team",
@@ -1649,7 +1726,26 @@
"save": "Salva definizione team", "save": "Salva definizione team",
"saving": "Salvataggio…", "saving": "Salvataggio…",
"cancel": "Annulla" "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": { "permissions": {
"title": "Permessi", "title": "Permessi",
@@ -1792,8 +1888,23 @@
"enabled": "Attivo", "enabled": "Attivo",
"save": "Salva pacchetto", "save": "Salva pacchetto",
"saving": "Salvataggio…", "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": { "marketplace": {
"title": "Marketplace", "title": "Marketplace",
@@ -1837,7 +1948,20 @@
"back": "Torna ai gruppi", "back": "Torna ai gruppi",
"viewPublic": "Apri pagina pubblica", "viewPublic": "Apri pagina pubblica",
"disband": "Sciogli gruppo", "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": { "transactions": {
"title": "Transazioni", "title": "Transazioni",
@@ -1926,7 +2050,9 @@
"save": "Salva buono", "save": "Salva buono",
"saving": "Salvataggio…", "saving": "Salvataggio…",
"cancel": "Annulla" "cancel": "Annulla"
} },
"updated": "Voucher aggiornato",
"updateError": "Impossibile aggiornare il voucher"
}, },
"subscriptions": { "subscriptions": {
"title": "Abbonamenti", "title": "Abbonamenti",
@@ -1945,7 +2071,15 @@
"colActions": "Azioni", "colActions": "Azioni",
"statusActive": "Attivo", "statusActive": "Attivo",
"statusCancelled": "Annullato", "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": { "rareValues": {
"title": "Valori rari", "title": "Valori rari",
@@ -1983,8 +2117,25 @@
"pointTypeDiamonds": "Diamanti", "pointTypeDiamonds": "Diamanti",
"save": "Salva articolo", "save": "Salva articolo",
"saving": "Salvataggio…", "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": { "badges": {
"title": "Distintivi", "title": "Distintivi",
@@ -2015,7 +2166,10 @@
"colImage": "Immagine", "colImage": "Immagine",
"colName": "Nome", "colName": "Nome",
"colDescription": "Descrizione", "colDescription": "Descrizione",
"colActions": "Azioni" "colActions": "Azioni",
"uploadedMessage": "Distintivo {code} caricato.",
"errorMessage": "Caricamento non riuscito: {error}",
"userIdPlaceholder": "ID utente"
}, },
"achievements": { "achievements": {
"title": "Trofei", "title": "Trofei",
@@ -2029,7 +2183,14 @@
"colProgress": "Progresso", "colProgress": "Progresso",
"levelLabel": "Livello {level}", "levelLabel": "Livello {level}",
"rewardCredits": "{count} crediti", "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": { "calendar": {
"title": "Campagne calendario", "title": "Campagne calendario",
@@ -2048,8 +2209,32 @@
"days": "Giorni", "days": "Giorni",
"dayLabel": "Giorno {day}", "dayLabel": "Giorno {day}",
"reward": "Ricompensa", "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": { "radio": {
"title": "Moderazione radio", "title": "Moderazione radio",
@@ -2082,7 +2267,7 @@
"noNowPlayingHint": "no now-playing API set", "noNowPlayingHint": "no now-playing API set",
"noListenersHint": "no listeners API set", "noListenersHint": "no listeners API set",
"endpointDiagnostics": "Endpoint diagnostics", "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", "streamUrl": "Stream URL",
"unreachable": "unreachable", "unreachable": "unreachable",
"refresh": "Aggiorna" "refresh": "Aggiorna"
@@ -2141,7 +2326,7 @@
"noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.", "noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.",
"goToSettings": "Go to Radio Settings", "goToSettings": "Go to Radio Settings",
"livePreview": "Live preview", "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", "source": "Source",
"audioSnippet": "Audio player snippet", "audioSnippet": "Audio player snippet",
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.", "audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
@@ -2248,7 +2433,7 @@
"pointsForm": { "pointsForm": {
"title": "Configurazione punti ascoltatore", "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.", "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", "points": "Points",
"enabled": "Enabled", "enabled": "Enabled",
"disabled": "Disabled", "disabled": "Disabled",
@@ -2478,13 +2663,22 @@
"valuePlaceholder": "Inserisci valore…", "valuePlaceholder": "Inserisci valore…",
"save": "Salva modifiche", "save": "Salva modifiche",
"cancel": "Annulla" "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": { "alerts": {
"title": "Avvisi", "title": "Avvisi",
"subtitle": "Invia avvisi a tutto l'hotel e visualizza avvisi recenti", "subtitle": "Invia avvisi a tutto l'hotel e visualizza avvisi recenti",
"sendAlert": "Invia avviso hotel", "sendAlert": "Invia avviso hotel",
"recentAlerts": "Avvisi recenti", "recentAlerts": "Avvisi recenti ({count})",
"noRecentAlerts": "Nessun avviso recente", "noRecentAlerts": "Nessun avviso recente",
"form": { "form": {
"title": "Titolo avviso", "title": "Titolo avviso",
@@ -2619,7 +2813,25 @@
"save": "Salva impostazioni", "save": "Salva impostazioni",
"saving": "Salvataggio…", "saving": "Salvataggio…",
"confirmEnable": "Attivare la manutenzione impedirà a tutti gli utenti non dello staff di accedere all'hotel. Continuare?", "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": { "housekeeping": {
"title": "Archivio permessi", "title": "Archivio permessi",
@@ -2695,7 +2907,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Log di audit", "tabAudit": "Log di audit",
"importPermissions": "Importa Permessi", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Importa", "import": "Importa",
"exportTitle": "Export Permessi", "exportTitle": "Export Permessi",
@@ -2820,8 +3032,13 @@
"variablesHint": "{username}, {email}, {hotel_name}", "variablesHint": "{username}, {email}, {hotel_name}",
"save": "Salva template", "save": "Salva template",
"saving": "Salvataggio…", "saving": "Salvataggio…",
"cancel": "Annulla" "cancel": "Annulla",
} "variablesPlaceholder": "Variabili separate da virgole",
"active": "Attivo"
},
"create": "Crea modello",
"save": "Salva modello",
"updated": "Aggiornato: {date}"
}, },
"theme": { "theme": {
"title": "Tema", "title": "Tema",
@@ -2832,7 +3049,7 @@
"presetsTitle": "Preimpostazioni tema", "presetsTitle": "Preimpostazioni tema",
"presetsSubtitle": "Scegli una combinazione di colori predefinita", "presetsSubtitle": "Scegli una combinazione di colori predefinita",
"applyPreset": "Applica", "applyPreset": "Applica",
"presetApplied": "Preimpostazione applicata", "presetApplied": "Preset applicato: {preset}",
"customiseTitle": "Colori personalizzati", "customiseTitle": "Colori personalizzati",
"primary": "Primario", "primary": "Primario",
"secondary": "Secondario", "secondary": "Secondario",
@@ -2848,7 +3065,11 @@
"saveCss": "Salva CSS", "saveCss": "Salva CSS",
"savingCss": "Salvataggio…", "savingCss": "Salvataggio…",
"reset": "Ripristina valori predefiniti", "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": { "media": {
"title": "Libreria media", "title": "Libreria media",
@@ -2982,7 +3203,9 @@
"alerts": "Alert", "alerts": "Alert",
"toolsGroup": "Strumenti", "toolsGroup": "Strumenti",
"ticketInbox": "Inbox", "ticketInbox": "Inbox",
"cmsErrors": "Errori CMS" "cmsErrors": "Errori CMS",
"themeBuilder": "Editor del tema",
"studio": "Studio"
}, },
"events": "Eventi", "events": "Eventi",
"eventsSubtitle": "Eventi hotel e tipi evento", "eventsSubtitle": "Eventi hotel e tipi evento",
@@ -3105,6 +3328,43 @@
"subtitle": "User-facing strings from emulator_settings (commands, wordfilter, pet/matchmaking messages).", "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.", "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" "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": { "analytics": {
@@ -3243,7 +3503,14 @@
"pages": "Pagine e strumenti", "pages": "Pagine e strumenti",
"description": "Trova una pagina di gestione o cerca nei dati dell’hotel.", "description": "Trova una pagina di gestione o cerca nei dati dell’hotel.",
"loadError": "Impossibile caricare i risultati. Puoi comunque aprire le pagine elencate.", "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": { "table": {
"search": "Cerca…", "search": "Cerca…",
+41 -4
View File
@@ -2045,7 +2045,7 @@
"noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.", "noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.",
"goToSettings": "Go to Radio Settings", "goToSettings": "Go to Radio Settings",
"livePreview": "Live preview", "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", "source": "Source",
"audioSnippet": "Audio player snippet", "audioSnippet": "Audio player snippet",
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.", "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", "noNowPlayingHint": "no now-playing API set",
"noListenersHint": "no listeners API set", "noListenersHint": "no listeners API set",
"endpointDiagnostics": "Endpoint diagnostics", "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", "streamUrl": "Stream URL",
"unreachable": "unreachable", "unreachable": "unreachable",
"refresh": "Vernieuwen" "refresh": "Vernieuwen"
@@ -2297,7 +2297,7 @@
"title": "Meldingen", "title": "Meldingen",
"subtitle": "Hotelbrede meldingen verzenden en recente meldingen bekijken", "subtitle": "Hotelbrede meldingen verzenden en recente meldingen bekijken",
"sendAlert": "Hotelmelding verzenden", "sendAlert": "Hotelmelding verzenden",
"recentAlerts": "Recente meldingen", "recentAlerts": "Recente meldingen ({count})",
"noRecentAlerts": "Geen recente meldingen", "noRecentAlerts": "Geen recente meldingen",
"form": { "form": {
"title": "Melding titel", "title": "Melding titel",
@@ -2516,7 +2516,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Auditlog", "tabAudit": "Auditlog",
"importPermissions": "Permissies importeren", "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…", "importPlaceholder": "Plak JSON hier…",
"import": "Importeren", "import": "Importeren",
"exportTitle": "Permissies exporteren", "exportTitle": "Permissies exporteren",
@@ -2774,6 +2774,43 @@
"subtitle": "User-facing strings from emulator_settings (commands, wordfilter, pet/matchmaking messages).", "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.", "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" "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": { "hubs": {
+5 -5
View File
@@ -1708,7 +1708,7 @@
"noNowPlayingHint": "ingen API-sett som nå spilles", "noNowPlayingHint": "ingen API-sett som nå spilles",
"noListenersHint": "ingen lyttere API satt", "noListenersHint": "ingen lyttere API satt",
"endpointDiagnostics": "Endepunktdiagnostikk", "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", "streamUrl": "Strøm URL",
"unreachable": "uoppnåelig", "unreachable": "uoppnåelig",
"refresh": "Oppdater" "refresh": "Oppdater"
@@ -1767,7 +1767,7 @@
"noStreamUrlText": "Det er ingen radio_stream_url satt, så det er ingenting å bygge inn ennå.", "noStreamUrlText": "Det er ingen radio_stream_url satt, så det er ingenting å bygge inn ennå.",
"goToSettings": "Gå til Radioinnstillinger", "goToSettings": "Gå til Radioinnstillinger",
"livePreview": "Live forhåndsvisning", "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", "source": "Kilde",
"audioSnippet": "Utdrag av lydspiller", "audioSnippet": "Utdrag av lydspiller",
"audioSnippetDesc": "Drop-in HTML5-lydspiller pekte rett på strømmen. Fungerer overalt hvor vanlig HTML er tillatt.", "audioSnippetDesc": "Drop-in HTML5-lydspiller pekte rett på strømmen. Fungerer overalt hvor vanlig HTML er tillatt.",
@@ -1998,7 +1998,7 @@
"title": "Varsler", "title": "Varsler",
"subtitle": "Send varsler for hele hotellet og se nylige varsler", "subtitle": "Send varsler for hele hotellet og se nylige varsler",
"sendAlert": "Send hotellvarsel", "sendAlert": "Send hotellvarsel",
"recentAlerts": "Nylige varsler", "recentAlerts": "Nylige varsler ({count})",
"noRecentAlerts": "Ingen nylige varsler", "noRecentAlerts": "Ingen nylige varsler",
"form": { "form": {
"title": "Varseltittel", "title": "Varseltittel",
@@ -2209,7 +2209,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Importer Tillatelser", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Importer", "import": "Importer",
"exportTitle": "Export Tillatelser", "exportTitle": "Export Tillatelser",
@@ -2346,7 +2346,7 @@
"presetsTitle": "Forhåndsinnstillinger for tema", "presetsTitle": "Forhåndsinnstillinger for tema",
"presetsSubtitle": "Velg et forhåndsbygd fargevalg", "presetsSubtitle": "Velg et forhåndsbygd fargevalg",
"applyPreset": "Søk", "applyPreset": "Søk",
"presetApplied": "Forhåndsinnstilling brukt", "presetApplied": "Forhåndsinnstilling brukt: {preset}",
"customiseTitle": "Egendefinerte farger", "customiseTitle": "Egendefinerte farger",
"primary": "Primær", "primary": "Primær",
"secondary": "Sekundær", "secondary": "Sekundær",
+5 -5
View File
@@ -1709,7 +1709,7 @@
"noNowPlayingHint": "brak aktualnie odtwarzanego zestawu API", "noNowPlayingHint": "brak aktualnie odtwarzanego zestawu API",
"noListenersHint": "brak zestawu API odbiorników", "noListenersHint": "brak zestawu API odbiorników",
"endpointDiagnostics": "Diagnostyka punktów końcowych", "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", "streamUrl": "Adres URL strumienia",
"unreachable": "nieosiągalny", "unreachable": "nieosiągalny",
"refresh": "Odśwież" "refresh": "Odśwież"
@@ -1768,7 +1768,7 @@
"noStreamUrlText": "Nie ma ustawionego radio_stream_url, więc nie ma jeszcze czego osadzać.", "noStreamUrlText": "Nie ma ustawionego radio_stream_url, więc nie ma jeszcze czego osadzać.",
"goToSettings": "Przejdź do Ustawień radia", "goToSettings": "Przejdź do Ustawień radia",
"livePreview": "Podgląd na żywo", "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", "source": "Źródło",
"audioSnippet": "Fragment odtwarzacza audio", "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.", "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", "title": "Alerty",
"subtitle": "Wysyłaj alerty dotyczące całego hotelu i przeglądaj najnowsze alerty", "subtitle": "Wysyłaj alerty dotyczące całego hotelu i przeglądaj najnowsze alerty",
"sendAlert": "Wyślij alert hotelowy", "sendAlert": "Wyślij alert hotelowy",
"recentAlerts": "Ostatnie alerty", "recentAlerts": "Ostatnie alerty ({count})",
"noRecentAlerts": "Brak ostatnich alertów", "noRecentAlerts": "Brak ostatnich alertów",
"form": { "form": {
"title": "Tytuł alertu", "title": "Tytuł alertu",
@@ -2210,7 +2210,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Importuj Uprawnienia", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Importuj", "import": "Importuj",
"exportTitle": "Export Uprawnienia", "exportTitle": "Export Uprawnienia",
@@ -2347,7 +2347,7 @@
"presetsTitle": "Wstępne ustawienia motywu", "presetsTitle": "Wstępne ustawienia motywu",
"presetsSubtitle": "Wybierz gotowy schemat kolorów", "presetsSubtitle": "Wybierz gotowy schemat kolorów",
"applyPreset": "Zastosuj", "applyPreset": "Zastosuj",
"presetApplied": "Zastosowano ustawienie wstępne", "presetApplied": "Zastosowano ustawienie wstępne: {preset}",
"customiseTitle": "Kolory niestandardowe", "customiseTitle": "Kolory niestandardowe",
"primary": "Podstawowy", "primary": "Podstawowy",
"secondary": "Drugorzędne", "secondary": "Drugorzędne",
+5 -5
View File
@@ -1709,7 +1709,7 @@
"noNowPlayingHint": "nenhum conjunto de API em execução", "noNowPlayingHint": "nenhum conjunto de API em execução",
"noListenersHint": "nenhuma API de ouvinte definida", "noListenersHint": "nenhuma API de ouvinte definida",
"endpointDiagnostics": "Diagnóstico de endpoint", "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", "streamUrl": "URL de transmissão",
"unreachable": "inacessível", "unreachable": "inacessível",
"refresh": "Atualizar" "refresh": "Atualizar"
@@ -1768,7 +1768,7 @@
"noStreamUrlText": "Não há radio_stream_url definido, então não há nada para incorporar ainda.", "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", "goToSettings": "Vá para configurações de rádio",
"livePreview": "Visualização ao vivo", "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", "source": "Fonte",
"audioSnippet": "Snippet do player de áudio", "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.", "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", "title": "Alertas",
"subtitle": "Envie alertas para todo o hotel e visualize alertas recentes", "subtitle": "Envie alertas para todo o hotel e visualize alertas recentes",
"sendAlert": "Enviar alerta de hotel", "sendAlert": "Enviar alerta de hotel",
"recentAlerts": "Alertas recentes", "recentAlerts": "Alertas recentes ({count})",
"noRecentAlerts": "Nenhum alerta recente", "noRecentAlerts": "Nenhum alerta recente",
"form": { "form": {
"title": "Título do alerta", "title": "Título do alerta",
@@ -2210,7 +2210,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Importar Permissões", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Importar", "import": "Importar",
"exportTitle": "Export Permissões", "exportTitle": "Export Permissões",
@@ -2347,7 +2347,7 @@
"presetsTitle": "Predefinições de tema", "presetsTitle": "Predefinições de tema",
"presetsSubtitle": "Escolha um esquema de cores pré-construído", "presetsSubtitle": "Escolha um esquema de cores pré-construído",
"applyPreset": "Aplicar", "applyPreset": "Aplicar",
"presetApplied": "Predefinição aplicada", "presetApplied": "Predefinição aplicada: {preset}",
"customiseTitle": "Cores personalizadas", "customiseTitle": "Cores personalizadas",
"primary": "Primário", "primary": "Primário",
"secondary": "Secundário", "secondary": "Secundário",
+5 -5
View File
@@ -1708,7 +1708,7 @@
"noNowPlayingHint": "nici un set API care se joacă acum", "noNowPlayingHint": "nici un set API care se joacă acum",
"noListenersHint": "nu se setează API pentru ascultători", "noListenersHint": "nu se setează API pentru ascultători",
"endpointDiagnostics": "Diagnosticarea punctului final", "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", "streamUrl": "Adresa URL a fluxului",
"unreachable": "de neatins", "unreachable": "de neatins",
"refresh": "Reîmprospătați" "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.", "noStreamUrlText": "Nu există nicio setare radio_stream_url, așa că încă nu există nimic de încorporat.",
"goToSettings": "Accesați Setările radio", "goToSettings": "Accesați Setările radio",
"livePreview": "Previzualizare live", "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", "source": "Sursa",
"audioSnippet": "Fragment de player audio", "audioSnippet": "Fragment de player audio",
"audioSnippetDesc": "Playerul audio HTML5 integrat a îndreptat direct spre flux. Funcționează oriunde este permis HTML simplu.", "audioSnippetDesc": "Playerul audio HTML5 integrat a îndreptat direct spre flux. Funcționează oriunde este permis HTML simplu.",
@@ -1998,7 +1998,7 @@
"title": "Alerte", "title": "Alerte",
"subtitle": "Trimiteți alerte la nivel de hotel și vizualizați alertele recente", "subtitle": "Trimiteți alerte la nivel de hotel și vizualizați alertele recente",
"sendAlert": "Trimiteți o alertă la hotel", "sendAlert": "Trimiteți o alertă la hotel",
"recentAlerts": "Alerte recente", "recentAlerts": "Alerte recente ({count})",
"noRecentAlerts": "Nu există alerte recente", "noRecentAlerts": "Nu există alerte recente",
"form": { "form": {
"title": "Titlul alertei", "title": "Titlul alertei",
@@ -2209,7 +2209,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Import Permisiuni", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Import", "import": "Import",
"exportTitle": "Export Permisiuni", "exportTitle": "Export Permisiuni",
@@ -2346,7 +2346,7 @@
"presetsTitle": "presetări teme", "presetsTitle": "presetări teme",
"presetsSubtitle": "Alegeți o schemă de culori prefabricată", "presetsSubtitle": "Alegeți o schemă de culori prefabricată",
"applyPreset": "Aplicați", "applyPreset": "Aplicați",
"presetApplied": "Presetarea aplicată", "presetApplied": "Presetarea aplicată: {preset}",
"customiseTitle": "Culori personalizate", "customiseTitle": "Culori personalizate",
"primary": "Primar", "primary": "Primar",
"secondary": "Secundar", "secondary": "Secundar",
+5 -5
View File
@@ -1707,7 +1707,7 @@
"noNowPlayingHint": "no now-playing API set", "noNowPlayingHint": "no now-playing API set",
"noListenersHint": "no listeners API set", "noListenersHint": "no listeners API set",
"endpointDiagnostics": "Endpoint diagnostics", "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", "streamUrl": "Stream URL",
"unreachable": "unreachable", "unreachable": "unreachable",
"refresh": "Refresh" "refresh": "Refresh"
@@ -1766,7 +1766,7 @@
"noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.", "noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.",
"goToSettings": "Go to Radio Settings", "goToSettings": "Go to Radio Settings",
"livePreview": "Live preview", "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", "source": "Source",
"audioSnippet": "Audio player snippet", "audioSnippet": "Audio player snippet",
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.", "audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
@@ -1997,7 +1997,7 @@
"title": "Alerts", "title": "Alerts",
"subtitle": "Send hotel-wide alerts and view recent alerts", "subtitle": "Send hotel-wide alerts and view recent alerts",
"sendAlert": "Send hotel alert", "sendAlert": "Send hotel alert",
"recentAlerts": "Recent alerts", "recentAlerts": "Recent alerts ({count})",
"noRecentAlerts": "No recent alerts", "noRecentAlerts": "No recent alerts",
"form": { "form": {
"title": "Alert title", "title": "Alert title",
@@ -2208,7 +2208,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Импорт Разрешения", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Импорт", "import": "Импорт",
"exportTitle": "Export Разрешения", "exportTitle": "Export Разрешения",
@@ -2345,7 +2345,7 @@
"presetsTitle": "Theme presets", "presetsTitle": "Theme presets",
"presetsSubtitle": "Choose a pre-built colour scheme", "presetsSubtitle": "Choose a pre-built colour scheme",
"applyPreset": "Apply", "applyPreset": "Apply",
"presetApplied": "Preset applied", "presetApplied": "Preset applied: {preset}",
"customiseTitle": "Custom colours", "customiseTitle": "Custom colours",
"primary": "Primary", "primary": "Primary",
"secondary": "Secondary", "secondary": "Secondary",
+5 -5
View File
@@ -1708,7 +1708,7 @@
"noNowPlayingHint": "nie je teraz nastavené rozhranie API", "noNowPlayingHint": "nie je teraz nastavené rozhranie API",
"noListenersHint": "nie je nastavené žiadne rozhranie API poslucháčov", "noListenersHint": "nie je nastavené žiadne rozhranie API poslucháčov",
"endpointDiagnostics": "Diagnostika koncového bodu", "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", "streamUrl": "Webová adresa streamu",
"unreachable": "nedosiahnuteľný", "unreachable": "nedosiahnuteľný",
"refresh": "Obnoviť" "refresh": "Obnoviť"
@@ -1767,7 +1767,7 @@
"noStreamUrlText": "Nie je nastavená žiadna adresa_url rádia, takže zatiaľ nie je čo vkladať.", "noStreamUrlText": "Nie je nastavená žiadna adresa_url rádia, takže zatiaľ nie je čo vkladať.",
"goToSettings": "Prejdite do časti Nastavenia rádia", "goToSettings": "Prejdite do časti Nastavenia rádia",
"livePreview": "Živá ukážka", "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", "source": "Zdroj",
"audioSnippet": "Útržok zvukového prehrávača", "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.", "audioSnippetDesc": "Zvukový prehrávač HTML5 nasmerovaný priamo na stream. Funguje všade, kde je povolený obyčajný HTML.",
@@ -1998,7 +1998,7 @@
"title": "Upozornenia", "title": "Upozornenia",
"subtitle": "Odosielajte upozornenia pre celý hotel a zobrazujte posledné upozornenia", "subtitle": "Odosielajte upozornenia pre celý hotel a zobrazujte posledné upozornenia",
"sendAlert": "Odoslať upozornenie na hotel", "sendAlert": "Odoslať upozornenie na hotel",
"recentAlerts": "Nedávne upozornenia", "recentAlerts": "Nedávne upozornenia ({count})",
"noRecentAlerts": "Žiadne nedávne upozornenia", "noRecentAlerts": "Žiadne nedávne upozornenia",
"form": { "form": {
"title": "Názov upozornenia", "title": "Názov upozornenia",
@@ -2209,7 +2209,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Importovať Povolenia", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Importovať", "import": "Importovať",
"exportTitle": "Export Povolenia", "exportTitle": "Export Povolenia",
@@ -2346,7 +2346,7 @@
"presetsTitle": "Prednastavenia tém", "presetsTitle": "Prednastavenia tém",
"presetsSubtitle": "Vyberte si vopred zostavenú farebnú schému", "presetsSubtitle": "Vyberte si vopred zostavenú farebnú schému",
"applyPreset": "Použiť", "applyPreset": "Použiť",
"presetApplied": "Predvoľba bola použitá", "presetApplied": "Predvoľba bola použitá: {preset}",
"customiseTitle": "Vlastné farby", "customiseTitle": "Vlastné farby",
"primary": "Primárne", "primary": "Primárne",
"secondary": "Sekundárne", "secondary": "Sekundárne",
+5 -5
View File
@@ -1708,7 +1708,7 @@
"noNowPlayingHint": "нема скупа АПИ-ја који се тренутно репродукује", "noNowPlayingHint": "нема скупа АПИ-ја који се тренутно репродукује",
"noListenersHint": "није постављен АПИ слушалаца", "noListenersHint": "није постављен АПИ слушалаца",
"endpointDiagnostics": "Ендпоинт диагностицс", "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": "УРЛ стрима", "streamUrl": "УРЛ стрима",
"unreachable": "недостижан", "unreachable": "недостижан",
"refresh": "Освежи" "refresh": "Освежи"
@@ -1767,7 +1767,7 @@
"noStreamUrlText": "Радио_стреам_урл није постављен, тако да још нема шта да се угради.", "noStreamUrlText": "Радио_стреам_урл није постављен, тако да још нема шта да се угради.",
"goToSettings": "Идите на Подешавања радија", "goToSettings": "Идите на Подешавања радија",
"livePreview": "Преглед уживо", "livePreview": "Преглед уживо",
"livePreviewText": "Ово је голи <audio> плејер како ће га посетиоци видети. Притисните плаи да бисте потврдили да је стрим доступан.", "livePreviewText": "Ово је голи audio плејер како ће га посетиоци видети. Притисните плаи да бисте потврдили да је стрим доступан.",
"source": "Извор", "source": "Извор",
"audioSnippet": "Исечак аудио плејера", "audioSnippet": "Исечак аудио плејера",
"audioSnippetDesc": "Улазни ХТМЛ5 аудио плејер усмерен право на ток. Ради свуда где је обичан ХТМЛ дозвољен.", "audioSnippetDesc": "Улазни ХТМЛ5 аудио плејер усмерен право на ток. Ради свуда где је обичан ХТМЛ дозвољен.",
@@ -1998,7 +1998,7 @@
"title": "Алертс", "title": "Алертс",
"subtitle": "Шаљите упозорења широм хотела и погледајте недавна упозорења", "subtitle": "Шаљите упозорења широм хотела и погледајте недавна упозорења",
"sendAlert": "Пошаљите хотелско обавештење", "sendAlert": "Пошаљите хотелско обавештење",
"recentAlerts": "Недавна упозорења", "recentAlerts": "Недавна упозорења ({count})",
"noRecentAlerts": "Нема недавних упозорења", "noRecentAlerts": "Нема недавних упозорења",
"form": { "form": {
"title": "Наслов упозорења", "title": "Наслов упозорења",
@@ -2209,7 +2209,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Увоз Дозволе", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Увоз", "import": "Увоз",
"exportTitle": "Export Дозволе", "exportTitle": "Export Дозволе",
@@ -2346,7 +2346,7 @@
"presetsTitle": "Унапред подешене теме", "presetsTitle": "Унапред подешене теме",
"presetsSubtitle": "Choose a pre-built colour scheme", "presetsSubtitle": "Choose a pre-built colour scheme",
"applyPreset": "Пријавите се", "applyPreset": "Пријавите се",
"presetApplied": "Пресет је примењен", "presetApplied": "Пресет је примењен: {preset}",
"customiseTitle": "Прилагођене боје", "customiseTitle": "Прилагођене боје",
"primary": "Примарни", "primary": "Примарни",
"secondary": "Секундарни", "secondary": "Секундарни",
+5 -5
View File
@@ -1708,7 +1708,7 @@
"noNowPlayingHint": "ingen nu-spelande API-uppsättning", "noNowPlayingHint": "ingen nu-spelande API-uppsättning",
"noListenersHint": "ingen lyssnars API inställd", "noListenersHint": "ingen lyssnars API inställd",
"endpointDiagnostics": "Slutpunktsdiagnostik", "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", "streamUrl": "Streama URL",
"unreachable": "oåtkomlig", "unreachable": "oåtkomlig",
"refresh": "Uppdatera" "refresh": "Uppdatera"
@@ -1767,7 +1767,7 @@
"noStreamUrlText": "Det finns ingen radio_stream_url inställd, så det finns inget att bädda in ännu.", "noStreamUrlText": "Det finns ingen radio_stream_url inställd, så det finns inget att bädda in ännu.",
"goToSettings": "Gå till Radioinställningar", "goToSettings": "Gå till Radioinställningar",
"livePreview": "Live förhandsvisning", "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", "source": "Källa",
"audioSnippet": "Utdrag från ljudspelare", "audioSnippet": "Utdrag från ljudspelare",
"audioSnippetDesc": "Drop-in HTML5-ljudspelare pekade rakt mot strömmen. Fungerar överallt där vanlig HTML är tillåtet.", "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", "title": "Varningar",
"subtitle": "Skicka varningar för hela hotellet och visa de senaste varningarna", "subtitle": "Skicka varningar för hela hotellet och visa de senaste varningarna",
"sendAlert": "Skicka hotellvarning", "sendAlert": "Skicka hotellvarning",
"recentAlerts": "Senaste varningar", "recentAlerts": "Senaste varningar ({count})",
"noRecentAlerts": "Inga senaste varningar", "noRecentAlerts": "Inga senaste varningar",
"form": { "form": {
"title": "Varningsrubrik", "title": "Varningsrubrik",
@@ -2209,7 +2209,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Importera Behörigheter", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Importera", "import": "Importera",
"exportTitle": "Export Behörigheter", "exportTitle": "Export Behörigheter",
@@ -2346,7 +2346,7 @@
"presetsTitle": "Förinställda tema", "presetsTitle": "Förinställda tema",
"presetsSubtitle": "Välj ett förbyggt färgschema", "presetsSubtitle": "Välj ett förbyggt färgschema",
"applyPreset": "Ansök", "applyPreset": "Ansök",
"presetApplied": "Förinställning tillämpas", "presetApplied": "Förinställning tillämpas: {preset}",
"customiseTitle": "Anpassade färger", "customiseTitle": "Anpassade färger",
"primary": "Primär", "primary": "Primär",
"secondary": "Sekundär", "secondary": "Sekundär",
+5 -5
View File
@@ -1709,7 +1709,7 @@
"noNowPlayingHint": "şu anda oynatılan API seti yok", "noNowPlayingHint": "şu anda oynatılan API seti yok",
"noListenersHint": "dinleyici API seti yok", "noListenersHint": "dinleyici API seti yok",
"endpointDiagnostics": "Uç nokta teşhisi", "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", "streamUrl": "Akış URL'si",
"unreachable": "ulaşılamaz", "unreachable": "ulaşılamaz",
"refresh": "Yenile" "refresh": "Yenile"
@@ -1768,7 +1768,7 @@
"noStreamUrlText": "Radio_stream_url kümesi yok, bu nedenle henüz eklenecek bir şey yok.", "noStreamUrlText": "Radio_stream_url kümesi yok, bu nedenle henüz eklenecek bir şey yok.",
"goToSettings": "Radyo Ayarlarına Git", "goToSettings": "Radyo Ayarlarına Git",
"livePreview": "Canlı önizleme", "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", "source": "Kaynak",
"audioSnippet": "Ses oynatıcı snippet'i", "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.", "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", "title": "Uyarılar",
"subtitle": "Otel genelinde uyarılar gönderin ve son uyarıları görüntüleyin", "subtitle": "Otel genelinde uyarılar gönderin ve son uyarıları görüntüleyin",
"sendAlert": "Otel uyarısı gönder", "sendAlert": "Otel uyarısı gönder",
"recentAlerts": "Son uyarılar", "recentAlerts": "Son uyarılar ({count})",
"noRecentAlerts": "Yeni uyarı yok", "noRecentAlerts": "Yeni uyarı yok",
"form": { "form": {
"title": "Uyarı başlığı", "title": "Uyarı başlığı",
@@ -2210,7 +2210,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "İthalat İzinler", "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…", "importPlaceholder": "Paste JSON here…",
"import": "İthalat", "import": "İthalat",
"exportTitle": "Export İzinler", "exportTitle": "Export İzinler",
@@ -2347,7 +2347,7 @@
"presetsTitle": "Tema ön ayarları", "presetsTitle": "Tema ön ayarları",
"presetsSubtitle": "Önceden oluşturulmuş bir renk şeması seçin", "presetsSubtitle": "Önceden oluşturulmuş bir renk şeması seçin",
"applyPreset": "Uygula", "applyPreset": "Uygula",
"presetApplied": "Ön ayar uygulandı", "presetApplied": "Ön ayar uygulandı: {preset}",
"customiseTitle": "Özel renkler", "customiseTitle": "Özel renkler",
"primary": "Birincil", "primary": "Birincil",
"secondary": "İkincil", "secondary": "İkincil",
+5 -5
View File
@@ -1708,7 +1708,7 @@
"noNowPlayingHint": "не налаштовано API поточного відтворення", "noNowPlayingHint": "не налаштовано API поточного відтворення",
"noListenersHint": "API слухачів не встановлено", "noListenersHint": "API слухачів не встановлено",
"endpointDiagnostics": "Діагностика кінцевої точки", "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-адреса потоку", "streamUrl": "URL-адреса потоку",
"unreachable": "недоступний", "unreachable": "недоступний",
"refresh": "Оновити" "refresh": "Оновити"
@@ -1767,7 +1767,7 @@
"noStreamUrlText": "Немає параметра radio_stream_url, тому поки що немає чого вставляти.", "noStreamUrlText": "Немає параметра radio_stream_url, тому поки що немає чого вставляти.",
"goToSettings": "Перейдіть до налаштувань радіо", "goToSettings": "Перейдіть до налаштувань радіо",
"livePreview": "Попередній перегляд", "livePreview": "Попередній перегляд",
"livePreviewText": "Це простий програвач <audio>, як його побачать відвідувачі. Натисніть кнопку відтворення, щоб підтвердити, що потік доступний.", "livePreviewText": "Це простий програвач audio, як його побачать відвідувачі. Натисніть кнопку відтворення, щоб підтвердити, що потік доступний.",
"source": "Джерело", "source": "Джерело",
"audioSnippet": "Фрагмент аудіоплеєра", "audioSnippet": "Фрагмент аудіоплеєра",
"audioSnippetDesc": "Вставний аудіопрогравач HTML5 спрямований прямо на потік. Працює скрізь, де дозволено простий HTML.", "audioSnippetDesc": "Вставний аудіопрогравач HTML5 спрямований прямо на потік. Працює скрізь, де дозволено простий HTML.",
@@ -1998,7 +1998,7 @@
"title": "Оповіщення", "title": "Оповіщення",
"subtitle": "Надсилайте сповіщення для всього готелю та переглядайте останні сповіщення", "subtitle": "Надсилайте сповіщення для всього готелю та переглядайте останні сповіщення",
"sendAlert": "Надіслати сповіщення готелю", "sendAlert": "Надіслати сповіщення готелю",
"recentAlerts": "Останні сповіщення", "recentAlerts": "Останні сповіщення ({count})",
"noRecentAlerts": "Немає останніх сповіщень", "noRecentAlerts": "Немає останніх сповіщень",
"form": { "form": {
"title": "Назва сповіщення", "title": "Назва сповіщення",
@@ -2209,7 +2209,7 @@
"tabTest": "Test", "tabTest": "Test",
"tabAudit": "Audit log", "tabAudit": "Audit log",
"importPermissions": "Імпорт Дозволи", "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…", "importPlaceholder": "Paste JSON here…",
"import": "Імпорт", "import": "Імпорт",
"exportTitle": "Export Дозволи", "exportTitle": "Export Дозволи",
@@ -2346,7 +2346,7 @@
"presetsTitle": "Попередні налаштування теми", "presetsTitle": "Попередні налаштування теми",
"presetsSubtitle": "Виберіть попередньо створену колірну схему", "presetsSubtitle": "Виберіть попередньо створену колірну схему",
"applyPreset": "Застосувати", "applyPreset": "Застосувати",
"presetApplied": "Попереднє налаштування застосовано", "presetApplied": "Попереднє налаштування застосовано: {preset}",
"customiseTitle": "Нестандартні кольори", "customiseTitle": "Нестандартні кольори",
"primary": "Первинний", "primary": "Первинний",
"secondary": "Вторинний", "secondary": "Вторинний",