From e3b792f5a35a0a76b730a2c650ae7ff9176fe153 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 12 Jul 2026 21:45:35 +0200 Subject: [PATCH] Add Portuguese, Polish, and Swedish language support with flags --- src/actions/translations.ts | 2 +- src/app/admin/translations/cms/page.tsx | 2 +- .../translations/translations-client.tsx | 5 +- src/components/language-switcher.tsx | 3 + src/i18n/request.ts | 2 +- src/messages/pl.json | 2034 +++++++++++++++++ src/messages/pt.json | 2034 +++++++++++++++++ src/messages/sv.json | 2034 +++++++++++++++++ 8 files changed, 6112 insertions(+), 4 deletions(-) create mode 100644 src/messages/pl.json create mode 100644 src/messages/pt.json create mode 100644 src/messages/sv.json diff --git a/src/actions/translations.ts b/src/actions/translations.ts index 3a32e910..53c7ce51 100644 --- a/src/actions/translations.ts +++ b/src/actions/translations.ts @@ -10,7 +10,7 @@ import { adminAction } from "@/lib/safe-action"; import { ActionError, actionOk } from "@/lib/safe-action-shared"; const saveTranslationsSchema = z.object({ - locale: z.enum(["en", "it", "nl", "de", "fr", "es"]), + locale: z.enum(["en", "it", "nl", "de", "fr", "es", "pt", "pl", "sv"]), data: z.record(z.string(), z.unknown()), }); diff --git a/src/app/admin/translations/cms/page.tsx b/src/app/admin/translations/cms/page.tsx index 101d30fb..f24d86ba 100644 --- a/src/app/admin/translations/cms/page.tsx +++ b/src/app/admin/translations/cms/page.tsx @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { TranslationsClient } from "../translations-client"; -const LOCALES = ["en", "it", "nl", "de", "fr", "es"] as const; +const LOCALES = ["en", "it", "nl", "de", "fr", "es", "pt", "pl", "sv"] as const; export default async function CmsTranslationsPage() { const messagesDir = path.join(process.cwd(), "messages"); diff --git a/src/app/admin/translations/translations-client.tsx b/src/app/admin/translations/translations-client.tsx index fe36a5c9..cadd5777 100644 --- a/src/app/admin/translations/translations-client.tsx +++ b/src/app/admin/translations/translations-client.tsx @@ -8,7 +8,7 @@ import { Input } from "@/components/ui/input"; import { useServerAction } from "@/hooks/use-server-action"; import { flattenObject, unflattenObject } from "@/lib/translations-utils"; -type Locale = "en" | "it" | "nl" | "de" | "fr" | "es"; +type Locale = "en" | "it" | "nl" | "de" | "fr" | "es" | "pt" | "pl" | "sv"; const LOCALES: { code: Locale; label: string }[] = [ { code: "en", label: "EN" }, @@ -17,6 +17,9 @@ const LOCALES: { code: Locale; label: string }[] = [ { code: "de", label: "DE" }, { code: "fr", label: "FR" }, { code: "es", label: "ES" }, + { code: "pt", label: "PT" }, + { code: "pl", label: "PL" }, + { code: "sv", label: "SV" }, ]; interface Props { diff --git a/src/components/language-switcher.tsx b/src/components/language-switcher.tsx index a5c67f8f..0eb2a861 100644 --- a/src/components/language-switcher.tsx +++ b/src/components/language-switcher.tsx @@ -12,6 +12,9 @@ const LOCALES: { code: string; label: string; lang: string }[] = [ { code: "fr", label: "Français", lang: "fr" }, { code: "es", label: "Español", lang: "es" }, { code: "it", label: "Italiano", lang: "it" }, + { code: "pt", label: "Português", lang: "pt" }, + { code: "pl", label: "Polski", lang: "pl" }, + { code: "sv", label: "Svenska", lang: "sv" }, ]; export function LanguageSwitcher() { diff --git a/src/i18n/request.ts b/src/i18n/request.ts index 66ff4783..9fb2309c 100644 --- a/src/i18n/request.ts +++ b/src/i18n/request.ts @@ -4,7 +4,7 @@ import { getRequestConfig } from "next-intl/server"; // i18n WITHOUT routing: the locale is chosen by a `NEXT_LOCALE` cookie (set by // the language switcher) rather than a URL segment, so every existing route and // the access-guard middleware keep working unchanged. English is the fallback. -export const SUPPORTED_LOCALES = ["en", "it", "nl", "de", "fr", "es"] as const; +export const SUPPORTED_LOCALES = ["en", "it", "nl", "de", "fr", "es", "pt", "pl", "sv"] as const; export type AppLocale = (typeof SUPPORTED_LOCALES)[number]; export const DEFAULT_LOCALE: AppLocale = "en"; diff --git a/src/messages/pl.json b/src/messages/pl.json new file mode 100644 index 00000000..7448d604 --- /dev/null +++ b/src/messages/pl.json @@ -0,0 +1,2034 @@ +{ + "nav": { + "home": "Home", + "community": "Community", + "articles": "Articles", + "photos": "Photos", + "staff": "Staff", + "rankings": "Rankings", + "guilds": "Guilds", + "leaderboards": "Leaderboards", + "rareValues": "Rare values", + "shop": "Shop", + "market": "Market", + "assistance": "Assistance", + "helpCenter": "Help center", + "myTickets": "My tickets", + "badges": "Badges", + "radio": "Radio", + "friends": "Friends", + "messages": "Messages", + "admin": "Admin", + "openMenu": "Open menu", + "closeMenu": "Close menu", + "drawBadge": "Draw badge" + }, + "header": { + "online": "{count} {hotel} online", + "nitroClient": "Nitro client", + "tagline": "An online virtual world where you can create your own avatar, make friends, chat, create rooms and much more!", + "login": "Login", + "or": "Or", + "createAccount": "Create an account" + }, + "footer": { + "copyright": "© {year} - {hotel} is a not for profit educational project" + }, + "common": { + "language": "Language", + "lightMode": "Light mode", + "darkMode": "Dark mode" + }, + "pages": { + "staff": { + "title": "Staff", + "subtitle": "The team behind the hotel", + "ranksTitle": "Ranks", + "ranksSubtitle": "What each part of the team takes care of", + "membersTitle": "Team members", + "membersEmptySubtitle": "Nobody to show right now", + "membersSubtitle": "{count, plural, one {# member keeping things running} other {# members keeping things running}}", + "emptyState": "No staff members to show.", + "colUsername": "Username", + "colRank": "Rank", + "colMotto": "Motto" + }, + "leaderboard": { + "title": "Leaderboard", + "subtitle": "Top 20 players by {currency}", + "tab_credits": "Credits", + "tab_diamonds": "Diamonds", + "tab_duckets": "Duckets", + "tab_achievement": "Achievements", + "tab_respects": "Respects", + "tab_onlinetime": "Online Time", + "emptyState": "No rankings to show yet.", + "colAvatar": "Avatar", + "colPlayer": "Player" + }, + "friends": { + "title": "Friends", + "emptySubtitle": "You have no friends added yet", + "subtitle": "{count, plural, one {You have # friend} other {You have # friends}}", + "emptyState": "Add friends in the hotel and they will show up here.", + "noMotto": "No motto" + }, + "messages": { + "title": "Messages", + "subtitle": "Friend requests and offline messages waiting for you", + "requestsTitle": "Friend requests", + "requestsEmptySubtitle": "Nobody's asked to be your friend yet", + "requestsSubtitle": "{count, plural, one {# pending request} other {# pending requests}}", + "requestsEmptyState": "No pending friend requests.", + "wantsToBeFriend": "wants to be your friend.", + "accept": "Accept", + "offlineTitle": "Offline messages", + "offlineEmptySubtitle": "Your inbox is empty", + "offlineSubtitle": "{count, plural, one {# message received} other {# messages received}}", + "offlineEmptyState": "You have no offline messages.", + "unknownUser": "User #{id}" + }, + "profile": { + "noMotto": "No motto", + "statRank": "Rank", + "statCredits": "Credits", + "statRegistered": "Registered", + "badgesTitle": "Badges", + "badgesEmpty": "No badges yet.", + "photosTitle": "Photos", + "photosEmpty": "No photos yet.", + "guestbookTitle": "Guestbook", + "guestbookSubtitle": "Messages on {username}'s wall", + "leaveMessage": "Leave a message", + "guestbookPlaceholder": "Write something on {username}'s guestbook…", + "post": "Post", + "loginToPost": "Log in to leave a guestbook message.", + "guestbookEmpty": "No guestbook entries yet.", + "unknownUser": "User #{id}", + "statDuckets": "Duckets", + "statDiamonds": "Diamonds", + "walletTitle": "Wallet", + "friendsTitle": "Friends", + "friendsSubtitle": "People in the hotel they hang out with", + "friendsEmpty": "No friends to show.", + "roomsTitle": "Rooms", + "roomsSubtitle": "Rooms they own", + "roomsEmpty": "No rooms to show.", + "roomUntitled": "Untitled room" + }, + "rankings": { + "title": "Rankings", + "subtitle": "The top 12 players ranked by credits", + "emptyState": "No players to rank yet." + }, + "home": { + "welcome": "Welcome to {hotelName}", + "welcomeSubtitle": "Your retro hotel, now powered by Next.js", + "welcomeBody": "Hang out, collect furni, and meet friends.", + "enterHotel": "Enter {hotelName}", + "onlineFriends": "Online Friends", + "noFriendsOnline": "No friends online", + "register": "REGISTER NOW", + "joinNow": "Join {hotelName} today!", + "recentUsers": "Recent Users", + "recentPhotos": "Recent Photos", + "latestNews": "Latest news", + "newsTitle": "Latest news", + "newsSubtitle": "Fresh from the hotel", + "allNews": "All news →", + "noArticles": "No articles yet.", + "noPhotos": "No photos yet.", + "noUsers": "No users online", + "latestArticle": "Latest Article" + }, + "news": { + "title": "News", + "subtitle": "The latest announcements, events and updates from the hotel", + "noArticles": "No articles yet." + }, + "article": { + "backToNews": "← News", + "reactionsTitle": "Reactions", + "reactionsSubtitle": "Tell us what you think", + "reaction": { + "like": "Like", + "love": "Love", + "wow": "Wow" + }, + "removeReaction": "Click to remove your reaction", + "reactWith": "React with {reaction}", + "noReactions": "No reactions yet.", + "logIn": "Log in", + "toReact": "to react.", + "commentsTitle": "Comments", + "commentsSubtitle": "Join the conversation", + "commentsCount": "{count} so far", + "noComments": "No comments yet. Be the first to comment.", + "userNumber": "User #{id}", + "addComment": "Add a comment", + "commentPlaceholder": "Write your comment…", + "postComment": "Post comment", + "toComment": "to leave a comment." + }, + "photos": { + "title": "Photos", + "subtitle": "The latest snapshots taken in the hotel", + "noPhotos": "No photos yet.", + "photoAlt": "Photo by user #{id}", + "byUser": "by user #{id}" + }, + "badges": { + "title": "Badges", + "subtitle": "The badges you can earn and show off around the hotel", + "noBadges": "No badges available yet." + }, + "community": { + "title": "Community", + "subtitle": "Everything happening around the hotel in one place", + "open": "Open →", + "links": { + "rankings": { + "title": "Rankings", + "text": "See the wealthiest players in the hotel, ranked by credits." + }, + "staff": { + "title": "Staff", + "text": "Meet the team that keeps the hotel running every day." + }, + "news": { + "title": "News", + "text": "The latest announcements, events and updates." + }, + "photos": { + "title": "Photos", + "text": "Snapshots shared by the community from around the hotel." + }, + "guilds": { + "title": "Guilds", + "text": "Browse the groups players have created and joined." + }, + "leaderboards": { + "title": "Leaderboards", + "text": "Who's on top this season across the hotel's stats." + } + } + }, + "guilds": { + "title": "Guilds", + "subtitle": "Browse the groups players have created and joined", + "empty": "No guilds yet.", + "unnamedGuild": "Unnamed guild", + "noDescription": "No description" + }, + "guild": { + "allGuilds": "← All guilds", + "unnamedGuild": "Unnamed guild", + "noDescription": "No description", + "memberSingular": "Member", + "memberPlural": "Members", + "founded": "Founded", + "forumLink": "Forum →", + "discussion": "Discussion", + "membersTitle": "Members", + "memberLowerSingular": "member", + "memberLowerPlural": "members", + "noMembers": "No members.", + "owner": "Owner", + "noMotto": "No motto" + }, + "guildForum": { + "backToGuild": "← Back to guild", + "unnamedGuild": "Unnamed guild", + "forumTitle": "{guild} — Forum", + "threadSingular": "thread", + "threadPlural": "threads", + "newThread": "+ New thread", + "empty": "No threads in this forum yet.", + "colSubject": "Subject", + "colAuthor": "Author", + "colPosts": "Posts", + "colDate": "Date", + "noSubject": "(no subject)", + "pinned": "Pinned", + "locked": "Locked", + "userNumber": "User #{id}", + "unknown": "Unknown" + }, + "guildForumNew": { + "backToForum": "← Back to forum", + "unnamedGuild": "Unnamed guild", + "title": "New thread", + "subtitle": "in {guild} forum", + "subjectLabel": "Subject", + "subjectPlaceholder": "Thread subject", + "messageLabel": "Message", + "messagePlaceholder": "Write your opening post…", + "postThread": "Post thread", + "cancel": "Cancel" + }, + "shop": { + "title": "Shop", + "subtitle": "Top up your account and grab exclusive packages, currency and badges.", + "browsingCategory": "Browsing the {category} packages.", + "all": "All", + "allPackages": "All packages", + "packagesAvailable": "{count, plural, one {# package available} other {# packages available}}", + "empty": "No packages available right now.", + "buy": "Buy" + }, + "topup": { + "title": "Top up with PayPal", + "subtitle": "Pay securely with PayPal and your credits are added the moment the payment clears.", + "cancelled": "Checkout was cancelled — no payment was taken.", + "chooseAmount": "Choose an amount", + "notConfiguredPre": "PayPal isn't configured yet. An administrator needs to set", + "notConfiguredAnd": "and", + "notConfiguredOptional": "(and optionally", + "notConfiguredPost": "for sandbox vs. live).", + "balanceTitle": "Your balance", + "signedInAs": "Signed in as {username}", + "credits": "Credits", + "creditsPerUnit": "Credits per 1.00 {currency}", + "rateLine": "You receive {amount} credits per 1.00 {currency}. Credits appear in-game the next time you log in." + }, + "marketplace": { + "title": "Marketplace", + "subtitle": "Browse furniture and rares listed by players. Listings are placed in-game and update live — head into the hotel to make an offer.", + "activeOffers": "Active offers", + "sellers": "Sellers", + "totalValue": "Total value", + "activeListings": "Active listings", + "mostRecentFirst": "Most recently listed first", + "empty": "There are no active marketplace offers right now. Check back later.", + "colOffer": "Offer", + "colItem": "Item", + "colSeller": "Seller", + "colPrice": "Price", + "colListed": "Listed", + "itemLabel": "Item #{id}", + "userLabel": "User #{id}", + "justNow": "just now", + "minutesAgo": "{count}m ago", + "hoursAgo": "{count}h ago", + "daysAgo": "{count}d ago" + }, + "rares": { + "title": "Rare values", + "subtitle": "Get an overview of all of the rares on the hotel, grouped by category.", + "empty": "We currently have no rares listed here.", + "rareCount": "{count, plural, one {# rare} other {# rares}}", + "viewRares": "View rares →" + }, + "rareCategory": { + "rareCount": "{count, plural, one {# rare in this category} other {# rares in this category}}", + "allRares": "← All rares", + "empty": "No rares listed in this category yet.", + "colRare": "Rare", + "colCredits": "Credits", + "colCurrency": "Currency", + "creditsValue": "{value} credits", + "duckets": "Duckets", + "diamonds": "Diamonds", + "credits": "Credits" + }, + "redeem": { + "title": "Redeem a voucher", + "subtitle": "Got a voucher code? Enter it below to add the reward to your account.", + "enterCode": "Enter your code", + "oncePerAccount": "Each voucher can only be redeemed once per account.", + "balanceTitle": "Your balance", + "signedInAs": "Signed in as {username}", + "credits": "Credits", + "instantCredit": "Redeemed rewards are credited instantly and appear in-game the next time you log in." + }, + "help": { + "title": "Help Center", + "subtitle": "Find guides, support topics, and the hotel rules — everything you need to get help.", + "myTickets": "My tickets →", + "topicsTitle": "Support topics", + "topicsSubtitle": "Browse guides and answers to common questions", + "topicsEmpty": "No help topics yet.", + "rulesTitle": "Hotel rules", + "rulesSubtitle": "Please read these before hanging out in the hotel", + "rulesEmpty": "No rules published yet.", + "categoryEmpty": "No rules in this category." + }, + "helpCategory": { + "back": "← Help Center", + "topicSubtitle": "Help Center topic", + "moreTitle": "More help topics", + "moreSubtitle": "Jump to another support topic" + }, + "helpTickets": { + "title": "Help Tickets", + "subtitle": "Submit a ticket to staff and track your open requests.", + "statTotal": "Total tickets", + "statOpen": "Open", + "statClosed": "Closed", + "newTitle": "New ticket", + "newSubtitle": "Describe your issue and staff will follow up", + "subjectPlaceholder": "Subject", + "contentPlaceholder": "Describe your issue…", + "submit": "Submit ticket", + "listTitle": "Your tickets", + "listSubtitle": "Every request you have sent to staff", + "listEmpty": "You have no tickets yet.", + "colId": "ID", + "colSubject": "Subject", + "colStatus": "Status", + "colCreated": "Created", + "statusOpen": "Open", + "statusClosed": "Closed" + }, + "applyStaff": { + "title": "Apply for {hotel} staff", + "subtitle": "We open staff applications every now and then. If you come across a position you feel you would fit perfectly into, do not hesitate to apply for it.", + "noPositionsTitle": "No positions open", + "noPositionsBody": "There are currently no positions open. Please come back at a later time to check if we have any openings by then. Thank you for your interest.", + "noDeadline": "No deadline set", + "deadline": "Application deadline: {date}", + "alreadyApplied": "You have already applied for {rank}", + "aboutYou": "About you", + "aboutPlaceholder": "Tell us why you'd be a great {rank}…", + "applyFor": "Apply for {rank}" + }, + "applyTeam": { + "title": "Apply for the {hotel} team", + "subtitle": "We open team applications periodically. If you see a team you fit, do not hesitate to apply!", + "noPositionsTitle": "No team positions open", + "noPositionsBody": "There are currently no open team positions. Please come back later to check for new openings. Thank you!", + "pending": "Your application is pending", + "aboutYou": "About you", + "aboutPlaceholder": "Tell us why you'd be a great fit for {rank}…", + "applyFor": "Apply for {rank}" + }, + "radio": { + "title": "Radio", + "subtitle": "Tune in to the hotel radio — live DJ sets, requests and shoutouts.", + "nowPlaying": "Now playing", + "nothingPlaying": "Nothing playing right now.", + "onAir": "On air: {dj}", + "noDj": "No DJ on air.", + "audioUnsupported": "Your browser does not support the audio element.", + "streamOffline": "The stream is currently offline.", + "exploreTitle": "Explore the radio", + "exploreSubtitle": "Schedule, shouts, requests and more", + "linkSchedule": "Schedule", + "linkScheduleText": "Our weekly line-up of DJs and shows.", + "linkShouts": "Shouts", + "linkShoutsText": "Shout-outs and requests from the community.", + "linkRequests": "Requests", + "linkRequestsText": "Ask the DJ to spin your favourite track.", + "linkApply": "Apply as DJ", + "linkApplyText": "Join the team and take to the airwaves.", + "linkLeaderboard": "Leaderboard", + "linkLeaderboardText": "Top listeners ranked by points earned.", + "todayTitle": "Today's schedule", + "todayTitleWithDay": "Today's schedule · {day}", + "todaySubtitle": "What's on the airwaves today", + "fullWeek": "Full week →", + "noShowsToday": "No shows scheduled for today.", + "colTime": "Time", + "colShow": "Show", + "colDj": "DJ", + "untitledShow": "Untitled show", + "userNumber": "User #{id}" + }, + "radioSchedule": { + "title": "Radio Schedule", + "subtitle": "Our weekly line-up of DJs and shows", + "empty": "No shows scheduled yet.", + "colDay": "Day", + "colTime": "Time", + "colDj": "DJ", + "colShow": "Show", + "noShows": "No shows" + }, + "radioShouts": { + "title": "Radio Shouts", + "subtitle": "Shout-outs and requests from the community", + "postTitle": "Post a shout", + "postSubtitle": "Send a message to the DJ on air", + "messageLabel": "Your message", + "messagePlaceholder": "Send a shout to the DJ…", + "shoutButton": "Shout", + "loginToPost": "Log in to post a shout.", + "latestTitle": "Latest shouts", + "latestSubtitle": "The most recent messages from listeners", + "empty": "No shouts yet. Be the first!" + }, + "radioContests": { + "title": "Radio Contests", + "subtitle": "Tune in and take part in our latest on-air contests", + "empty": "No contests yet.", + "contestLabel": "Contest #{id}", + "dateUnknown": "Date unknown" + }, + "radioGiveaways": { + "title": "Radio Giveaways", + "subtitle": "Listen in for your chance to win in our radio giveaways", + "empty": "No giveaways yet.", + "giveawayLabel": "Giveaway #{id}", + "dateUnknown": "Date unknown" + }, + "radioLeaderboard": { + "title": "Radio Listener Leaderboard", + "subtitle": "Top 50 listeners by points earned tuning in to the radio", + "empty": "No listener points to show yet.", + "colRank": "Rank", + "colAvatar": "Avatar", + "colListener": "Listener", + "colPoints": "Points" + }, + "radioRequests": { + "formTitle": "Submit a request", + "formSubtitle": "Ask the DJ to play your favourite track", + "songLabel": "Song", + "songPlaceholder": "Title of the song", + "artistLabel": "Artist", + "artistPlaceholder": "Name of the artist (optional)", + "submitButton": "Submit request", + "loginToRequest": "Log in to submit a request.", + "latestTitle": "Recent requests", + "latestSubtitle": "The most recently requested songs", + "empty": "No requests yet.", + "colSong": "Song", + "colArtist": "Artist", + "colRequester": "Requester", + "colDate": "Date", + "colStatus": "Status", + "statusPending": "Pending", + "statusApproved": "Approved", + "statusPlayed": "Played" + }, + "client": { + "title": "Nitro client", + "loading": "Loading client…", + "generatingTicket": "Generating SSO ticket…", + "connected": "Connected", + "welcome": "Welcome to {hotel}", + "openingClient": "Opening the client in a new tab…", + "fallbackLink": "If it doesn't open,", + "fallbackClick": "click here", + "errors": { + "unauthorized": "You need to be logged in to open the client.", + "generic": "Could not generate SSO ticket", + "loginRedirect": "Log in" + } + }, + "radioApply": { + "title": "Apply to be a Radio DJ", + "subtitle": "Tell us about yourself and why you'd be a great fit for the airwaves", + "latestTitle": "Your latest application", + "statusLabel": "Status:", + "submittedOn": "· submitted {date}", + "submitAgain": "You can submit again below if your details have changed.", + "formTitle": "Application form", + "formSubtitle": "All fields marked required must be filled in", + "realNameLabel": "Real name", + "realNamePlaceholder": "Your full name", + "ageLabel": "Age", + "availabilityLabel": "Availability", + "availabilityPlaceholder": "Which days and times can you broadcast?", + "musicStyleLabel": "Music style", + "musicStylePlaceholder": "e.g. House, Pop, Hip-Hop", + "experienceLabel": "Experience", + "optional": "(optional)", + "experiencePlaceholder": "Any prior DJ or hosting experience?", + "motivationLabel": "Motivation", + "motivationPlaceholder": "Why do you want to join the radio team?", + "submitButton": "Submit application" + }, + "settings": { + "accountSettings": "Account settings", + "accountSettingsSubtitle": "Manage your profile, security and connected accounts", + "publicProfile": "Your public profile", + "noMotto": "No motto", + "noEmail": "No e-mail on file", + "changeMotto": "Change motto", + "changeMottoSubtitle": "Updates instantly in-game if you are online", + "mottoPlaceholder": "Your motto", + "saveMotto": "Save motto", + "securityTitle": "Security", + "securitySubtitle": "Two-factor authentication", + "securityDescription": "Add a second layer of protection to your account.", + "twoFactorLink": "Two-factor authentication", + "connectionsTitle": "Connected accounts", + "connectionsSubtitle": "Sign in with one click", + "connectionsDescription": "Link Discord or Google to your hotel account.", + "manageConnections": "Manage connections" + }, + "settings2fa": { + "backToSettings": "← Settings", + "title": "Two-factor authentication", + "subtitle": "Add a second layer of security with an authenticator app", + "nowEnabled": "2FA is now enabled. 🔒", + "disabledNotice": "2FA has been disabled.", + "badCode": "That code wasn't valid — try again.", + "noAppKeyPrefix": "2FA is unavailable until", + "noAppKeyMiddle": "is configured (the same Laravel", + "noAppKeySuffix": "as your AtomCMS install, so existing secrets stay readable).", + "isEnabled": "2FA is enabled", + "onYourAccount": "on your account.", + "disable": "Disable 2FA", + "scanOrEnter": "Scan or enter this key", + "scanInstructions": "Add this to Google Authenticator / Authy, then enter the 6-digit code to confirm.", + "manualKey": "Manual key:", + "codePlaceholder": "6-digit code", + "confirm": "Confirm", + "enableIntro": "Add a second layer of security with an authenticator app.", + "enable": "Enable 2FA" + }, + "connections": { + "backToSettings": "← Settings", + "title": "Connected accounts", + "subtitle": "Link a social account so you can sign in with one click", + "matchByEmail": "We match it to your hotel account by e-mail, so use the address on file:", + "lookupFailed": "We couldn't read your linked accounts right now. You can still connect below — your status will show once it's available.", + "statusUnavailable": "Not available on this hotel.", + "statusConnected": "Connected — sign in with this account.", + "statusNotConnected": "Not connected.", + "connected": "Connected", + "connectProvider": "Connect {provider}", + "unavailable": "Unavailable", + "footerNote": "Connecting takes you to the provider to authorise, then back here. If your social account's e-mail doesn't match a hotel account, you'll be returned to the login screen with a note." + }, + "login": { + "title": "Sign in", + "subtitle": "Welcome back — log in to enter the hotel.", + "subtitle2fa": "Enter the 6-digit code from your authenticator.", + "usernamePlaceholder": "Username", + "passwordPlaceholder": "Password", + "codePlaceholder": "2FA code", + "pleaseWait": "Please wait…", + "verify": "Verify", + "signIn": "Sign in", + "or": "or", + "continueWithDiscord": "Continue with Discord", + "continueWithGoogle": "Continue with Google", + "noAccount": "No account?", + "createOne": "Create one", + "forgotPassword": "Forgot password?", + "errorInvalidCredentials": "Invalid username or password", + "errorInvalid2fa": "Invalid 2FA code" + }, + "register": { + "title": "Create account", + "subtitle": "Join the hotel — it only takes a moment.", + "username": "Username", + "email": "Email", + "password": "Password", + "confirmPassword": "Repeat password", + "usernamePlaceholder": "Username", + "emailPlaceholder": "Email", + "passwordPlaceholder": "Password (min 6 chars)", + "confirmPasswordPlaceholder": "Repeat password", + "selectOutfit": "Select your outfit", + "termsAccept": "I am 18+ and accept the {hotel} terms & rules.", + "createAccount": "Create account", + "creatingAccount": "Creating account...", + "alreadyHaveAccount": "Already have an account? Login!", + "register": "Register", + "termsError": "You must accept the terms & rules to register.", + "usernameError": "Username is required.", + "emailError": "Valid email is required.", + "passwordError": "Password must be at least 6 characters.", + "confirmPasswordError": "Passwords do not match.", + "termsRequired": "You must accept the terms & rules.", + "usernameRequired": "Username is required.", + "emailRequired": "Email is required.", + "passwordRequired": "Password is required.", + "passwordsDontMatch": "Passwords do not match." + }, + "forgot": { + "title": "Reset password", + "subtitle": "We'll email you a secure link to set a new password.", + "sentNotice": "If that email is registered, a reset link has been sent. Check your inbox.", + "emailPlaceholder": "Your email", + "sendResetLink": "Send reset link", + "backToLogin": "Back to login" + }, + "reset": { + "title": "Set a new password", + "subtitle": "Choose a strong password you don't use elsewhere.", + "passwordPlaceholder": "New password (min 6 chars)", + "resetPassword": "Reset password", + "backToLogin": "Back to login" + }, + "verify": { + "verifiedTitle": "You're all set", + "verifiedSubtitle": "Email verified", + "verifiedBody": "Your email address has been verified. You can now log in and play.", + "goToLogin": "Go to login", + "alreadyTitle": "Already verified", + "alreadySubtitle": "Nothing left to do", + "alreadyBody": "This email address has already been confirmed.", + "unavailableTitle": "Try again shortly", + "unavailableSubtitle": "Server unreachable", + "unavailableBody": "We couldn't reach the server to verify your email. Please retry the link in a moment.", + "invalidTitle": "Email verification", + "invalidSubtitle": "Invalid or expired link", + "invalidBody": "This verification link is not valid. Make sure you opened the full link from your email, or request a new one.", + "backToLogin": "Back to login" + }, + "banned": { + "title": "You are banned", + "subtitle": "Account suspended", + "body": "Your account has been suspended from the hotel.", + "reasonLabel": "Reason:", + "permanent": "This ban is permanent.", + "expires": "Expires {date}.", + "contactStaff": "If you believe this is a mistake, contact the staff team." + }, + "maintenance": { + "title": "{hotel} is under maintenance", + "subtitle": "We'll be back shortly", + "staffCanLogIn": "Staff can still log in." + }, + "admin": { + "nav": { + "overview": "Overview", + "dashboard": "Dashboard", + "content": "Content", + "articles": "Articles", + "tags": "Tags", + "navigator": "Navigator", + "helpCenter": "Help center", + "advertisements": "Advertisements", + "writeableBoxes": "Writeable boxes", + "photos": "Photos", + "tickets": "Tickets", + "usersAndAccess": "Users & access", + "users": "Users", + "bans": "Bans", + "ipManagement": "IP management", + "vpn": "VPN", + "applications": "Applications", + "teams": "Teams", + "permissions": "Permissions", + "economy": "Economy", + "catalog": "Catalog", + "shop": "Shop", + "transactions": "Transactions", + "vouchers": "Vouchers", + "subscriptions": "Subscriptions", + "rareValues": "Rare values", + "badges": "Badges", + "achievements": "Achievements", + "calendar": "Calendar", + "radio": "Radio", + "radioHistory": "Radio history", + "radioBanners": "Banners", + "radioEmbed": "Embed", + "radioApiKeys": "API keys", + "radioModeration": "Moderation", + "radioSettings": "Settings", + "radioMonitoring": "Monitoring", + "radioPoints": "Points", + "radioRanks": "Ranks", + "radioAutodj": "Auto DJ", + "system": "System", + "commandocentrum": "Commandocentrum", + "emulator": "Emulator", + "alerts": "Alerts", + "logs": "Logs", + "wordFilter": "Word filter", + "maintenance": "Maintenance", + "rooms": "Rooms", + "emailTemplates": "Email templates", + "theme": "Theme", + "media": "Media", + "favicon": "Favicon", + "sounds": "Sounds", + "translations": "Translations", + "import": "Import", + "settings": "Settings", + "backToSite": "Back to site", + "housekeeping": "Housekeeping", + "hotelManagementPanel": "Hotel management panel", + "rankLabel": "Rank {rank}" + }, + "dashboard": { + "title": "Dashboard", + "subtitle": "Hotel overview & live status", + "playersOnline": "Players online", + "articles": "Articles", + "activeBans": "Active bans", + "database": "Database", + "databaseConnected": "Connected", + "databaseDown": "Down", + "usersByRank": "Users by rank", + "noAccountsToBreakDown": "No accounts to break down yet", + "rankLabel": "Rank {rank}", + "rankWithCount": "Rank {rank}: {count} accounts", + "recentStaffActivity": "Recent staff activity", + "viewAll": "View all", + "noStaffActivityLogged": "No staff activity logged yet" + }, + "articles": { + "title": "Articles", + "subtitle": "News articles — 50 most recent", + "newArticle": "+ New article", + "edit": "Edit", + "delete": "Delete", + "noArticles": "No articles yet", + "confirmDelete": "Are you sure you want to delete \"{title}\"?", + "colTitle": "Title", + "colSlug": "Slug", + "colAuthor": "Author", + "colCreated": "Created", + "colActions": "Actions", + "form": { + "title": "Title", + "titlePlaceholder": "Article title", + "slug": "Slug", + "slugPlaceholder": "article-url-slug", + "category": "Category", + "categoryPlaceholder": "Select a category", + "body": "Body", + "bodyPlaceholder": "Write your article content…", + "image": "Image", + "imagePlaceholder": "https://example.com/image.png", + "thumbnail": "Thumbnail", + "thumbnailPlaceholder": "https://example.com/thumb.png", + "published": "Published", + "featured": "Featured", + "save": "Save article", + "saving": "Saving…", + "deleteArticle": "Delete article", + "deleteConfirm": "This action cannot be undone. The article will be permanently removed.", + "cancel": "Cancel" + } + }, + "tags": { + "title": "Tags", + "subtitle": "Article tags / categories", + "addTag": "Add tag", + "name": "Name", + "namePlaceholder": "Tag name", + "backgroundColour": "Background colour", + "createTag": "Create tag", + "editTag": "Edit tag", + "updateTag": "Update tag", + "deleteTag": "Delete tag", + "confirmDelete": "Delete \"{name}\"?", + "noTags": "No tags yet", + "colId": "ID", + "colTag": "Tag", + "colColour": "Colour", + "colUsedBy": "Used by", + "colCreated": "Created", + "colActions": "Actions" + }, + "navigation": { + "title": "Navigator", + "flatCategories": "Flat categories", + "publicRooms": "Public rooms", + "addCategory": "Add category", + "editCategory": "Edit category", + "deleteCategory": "Delete category", + "categoryName": "Category name", + "categoryNamePlaceholder": "e.g. Chill & Chat", + "categoryOrder": "Order", + "roomName": "Room name", + "roomNamePlaceholder": "e.g. Welcome Lounge", + "roomId": "Room ID", + "roomIdPlaceholder": "Numeric room ID", + "save": "Save", + "cancel": "Cancel", + "noCategories": "No categories yet", + "noPublicRooms": "No public rooms yet", + "colId": "ID", + "colName": "Name", + "colOrder": "Order", + "colRooms": "Rooms", + "colActions": "Actions" + }, + "helpCenter": { + "title": "Help Center", + "subtitle": "Support entries and FAQ management", + "entries": "Entries", + "newEntry": "+ New entry", + "edit": "Edit", + "delete": "Delete", + "noEntries": "No entries yet", + "confirmDelete": "Delete \"{title}\"?", + "colId": "ID", + "colTitle": "Title", + "colCategory": "Category", + "colCreated": "Created", + "colActions": "Actions", + "form": { + "title": "Title", + "titlePlaceholder": "Entry title", + "category": "Category", + "categoryPlaceholder": "Select category", + "body": "Body", + "bodyPlaceholder": "Write the answer…", + "save": "Save entry", + "saving": "Saving…", + "deleteEntry": "Delete entry", + "cancel": "Cancel" + } + }, + "ads": { + "title": "Advertisements", + "subtitle": "Manage hotel advertisements and promotional banners", + "newAd": "+ New ad", + "edit": "Edit", + "delete": "Delete", + "noAds": "No advertisements yet", + "confirmDelete": "Delete this ad?", + "colId": "ID", + "colName": "Name", + "colImage": "Image", + "colUrl": "URL", + "colViews": "Views", + "colStatus": "Status", + "colCreated": "Created", + "colActions": "Actions", + "form": { + "name": "Name", + "namePlaceholder": "Ad name", + "image": "Image URL", + "imagePlaceholder": "https://example.com/banner.png", + "url": "Target URL", + "urlPlaceholder": "https://example.com", + "enabled": "Enabled", + "save": "Save ad", + "saving": "Saving…", + "deleteAd": "Delete ad", + "cancel": "Cancel" + } + }, + "writeableBoxes": { + "title": "Writeable Boxes", + "subtitle": "Content panels shown across the hotel and CMS", + "newBox": "New box", + "edit": "Edit", + "delete": "Delete", + "noBoxes": "No writeable boxes yet", + "confirmDelete": "Delete this box?", + "colId": "ID", + "colName": "Name", + "colLocation": "Location", + "colUpdated": "Updated", + "colActions": "Actions", + "form": { + "name": "Name", + "namePlaceholder": "Box name", + "location": "Location", + "locationPlaceholder": "e.g. homepage, sidebar", + "content": "Content", + "contentPlaceholder": "Write the box content…", + "enabled": "Enabled", + "save": "Save box", + "saving": "Saving…", + "deleteBox": "Delete box", + "cancel": "Cancel" + } + }, + "photos": { + "title": "Photo Moderation", + "subtitle": "Recent in-game camera photos", + "delete": "Delete", + "approve": "Approve", + "noPhotos": "No photos to moderate", + "confirmDelete": "Delete this photo permanently?", + "colId": "ID", + "colPreview": "Preview", + "colUser": "User", + "colRoom": "Room", + "colTimestamp": "Timestamp", + "colActions": "Actions", + "photoAlt": "User photo #{id}", + "byUser": "by {username}" + }, + "users": { + "title": "Users", + "subtitle": "Browse, search and manage hotel accounts", + "search": "Search", + "searchPlaceholder": "Search by username, email or ID…", + "searchResults": "{count, plural, one {# result} other {# results}} for \"{query}\"", + "noResults": "No users found", + "colId": "ID", + "colUsername": "Username", + "colEmail": "Email", + "colRank": "Rank", + "colOnline": "Online", + "colRegistered": "Registered", + "colLastOnline": "Last online", + "colActions": "Actions", + "yes": "Yes", + "no": "No", + "edit": "Edit", + "setRank": "Set rank", + "disconnect": "Disconnect", + "alert": "Alert", + "ban": "Ban", + "detail": { + "title": "User: {username}", + "subtitle": "Account details and management", + "overview": "Overview", + "actions": "Actions", + "username": "Username", + "email": "Email", + "motto": "Motto", + "rank": "Rank", + "credits": "Credits", + "duckets": "Duckets", + "diamonds": "Diamonds", + "online": "Online", + "registered": "Registered", + "lastOnline": "Last online", + "ipAddress": "IP address", + "machineId": "Machine ID", + "sendAlert": "Send alert", + "sendMessage": "Send message", + "disconnectUser": "Disconnect user", + "banUser": "Ban user", + "resetPassword": "Reset password", + "deleteUser": "Delete user", + "confirmDelete": "Are you sure you want to delete {username}? This cannot be undone." + }, + "editForm": { + "title": "Edit user: {username}", + "subtitle": "Modify account details", + "username": "Username", + "usernamePlaceholder": "Username", + "email": "Email", + "emailPlaceholder": "user@example.com", + "motto": "Motto", + "mottoPlaceholder": "User motto", + "rank": "Rank", + "credits": "Credits", + "duckets": "Duckets", + "diamonds": "Diamonds", + "save": "Save changes", + "saving": "Saving…", + "cancel": "Cancel" + } + }, + "bans": { + "title": "Bans", + "subtitle": "Manage account, IP and machine bans", + "newBan": "New ban", + "edit": "Edit", + "delete": "Delete", + "noBans": "No bans yet", + "confirmDelete": "Remove this ban?", + "colId": "ID", + "colValue": "Value", + "colType": "Type", + "colReason": "Reason", + "colBannedBy": "Banned by", + "colExpires": "Expires", + "colCreated": "Created", + "colActions": "Actions", + "form": { + "value": "Value", + "valuePlaceholder": "Username, IP, or machine ID", + "type": "Type", + "typeUser": "User", + "typeIp": "IP", + "typeMachine": "Machine", + "reason": "Reason", + "reasonPlaceholder": "Ban reason", + "expiresAt": "Expires at", + "permanent": "Permanent", + "save": "Save ban", + "saving": "Saving…", + "cancel": "Cancel" + } + }, + "ip": { + "title": "IP Management", + "subtitle": "Manage IP whitelist and blacklist", + "whitelist": "Whitelist", + "blacklist": "Blacklist", + "addIp": "Add IP", + "ipAddress": "IP address", + "ipAddressPlaceholder": "e.g. 127.0.0.1", + "reason": "Reason", + "reasonPlaceholder": "Reason for listing", + "add": "Add", + "remove": "Remove", + "confirmRemove": "Remove this IP from the {list}?", + "noWhitelistedIps": "No whitelisted IPs", + "noBlacklistedIps": "No blacklisted IPs", + "colIp": "IP", + "colReason": "Reason", + "colAdded": "Added", + "colActions": "Actions" + }, + "vpn": { + "title": "VPN / Proxy Detection", + "subtitle": "Configure VPN and proxy detection settings", + "enabled": "Enabled", + "apiKey": "API key", + "apiKeyPlaceholder": "Your VPN detection API key", + "apiKeyHint": "Leave empty to disable", + "blockOnDetect": "Block on detection", + "blockOnDetectHint": "Automatically block users detected behind a VPN or proxy", + "save": "Save settings", + "saving": "Saving…", + "settingsSaved": "VPN settings saved successfully" + }, + "applications": { + "title": "Staff Applications", + "subtitle": "Pending and reviewed staff applications", + "pending": "Pending", + "approved": "Approved", + "rejected": "Rejected", + "noApplications": "No applications yet", + "colId": "ID", + "colApplicant": "Applicant", + "colPosition": "Position", + "colStatus": "Status", + "colSubmitted": "Submitted", + "colActions": "Actions", + "review": "Review", + "approve": "Approve", + "reject": "Reject", + "confirmApprove": "Approve {username} for {position}?", + "confirmReject": "Reject {username}'s application for {position}?", + "detail": { + "title": "Application by {username}", + "subtitle": "For {position}", + "about": "About the applicant", + "message": "Application message", + "status": "Status", + "submitted": "Submitted", + "noMessage": "No message provided" + } + }, + "teams": { + "title": "Teams", + "subtitle": "Define staff rank names and descriptions", + "addRank": "Add rank", + "editRank": "Edit rank", + "deleteRank": "Delete rank", + "noTeams": "No rank definitions yet", + "confirmDelete": "Delete rank {id} definition?", + "colId": "ID", + "colRank": "Rank", + "colName": "Name", + "colBadge": "Badge", + "colColour": "Colour", + "colActions": "Actions", + "form": { + "rankId": "Rank ID", + "rankIdPlaceholder": "Numeric rank ID", + "name": "Name", + "namePlaceholder": "e.g. Administrator", + "badge": "Badge code", + "badgePlaceholder": "e.g. ADM001", + "colour": "Colour", + "colourPlaceholder": "e.g. #ff0000", + "save": "Save team definition", + "saving": "Saving…", + "cancel": "Cancel" + } + }, + "permissions": { + "title": "Permissions", + "subtitle": "Manage rank-based permissions for the admin panel", + "addPermission": "Add permission", + "editPermission": "Edit permission", + "deletePermission": "Delete permission", + "noPermissions": "No permissions configured yet", + "confirmDelete": "Delete this permission?", + "colId": "ID", + "colRank": "Rank", + "colPermission": "Permission", + "colCreated": "Created", + "colActions": "Actions", + "form": { + "rankId": "Rank ID", + "rankIdPlaceholder": "Minimum rank required", + "permission": "Permission key", + "permissionPlaceholder": "e.g. admin.articles.manage", + "save": "Save permission", + "saving": "Saving…", + "cancel": "Cancel" + } + }, + "catalog": { + "title": "Catalog", + "subtitle": "Browse and manage catalog pages and items", + "pages": "Pages", + "items": "Items", + "addPage": "Add page", + "editPage": "Edit page", + "deletePage": "Delete page", + "addItem": "Add item", + "editItem": "Edit item", + "deleteItem": "Delete item", + "noPages": "No catalog pages yet", + "noItems": "No items in this page", + "confirmDeletePage": "Delete catalog page \"{name}\"?", + "confirmDeleteItem": "Delete this item?", + "colId": "ID", + "colName": "Name", + "colIcon": "Icon", + "colOrder": "Order", + "colItems": "Items", + "colEnabled": "Enabled", + "colActions": "Actions", + "colItemId": "Item ID", + "colItemName": "Item name", + "colCostCredits": "Cost (credits)", + "colCostPoints": "Cost (points)", + "colLimited": "Limited", + "pageForm": { + "name": "Name", + "namePlaceholder": "Page name", + "icon": "Icon ID", + "iconPlaceholder": "Numeric icon ID", + "order": "Order", + "enabled": "Enabled", + "parent": "Parent page", + "parentNone": "None (root)", + "save": "Save page", + "saving": "Saving…", + "cancel": "Cancel" + }, + "itemForm": { + "itemId": "Item ID", + "itemName": "Item name", + "costCredits": "Cost in credits", + "costDuckets": "Cost in duckets", + "costDiamonds": "Cost in diamonds", + "limitedStack": "Limited stack size", + "limitedSold": "Limited sold", + "save": "Save item", + "saving": "Saving…", + "cancel": "Cancel" + } + }, + "shop": { + "title": "Shop", + "subtitle": "Manage packages and orders", + "packages": "Packages", + "orders": "Orders", + "newPackage": "+ New package", + "edit": "Edit", + "delete": "Delete", + "viewOrder": "View", + "noPackages": "No shop packages yet", + "noOrders": "No orders yet", + "confirmDelete": "Delete this package?", + "colId": "ID", + "colName": "Name", + "colPrice": "Price", + "colCurrency": "Currency", + "colType": "Type", + "colSold": "Sold", + "colEnabled": "Enabled", + "colActions": "Actions", + "colOrderId": "Order ID", + "colCustomer": "Customer", + "colPackage": "Package", + "colAmount": "Amount", + "colStatus": "Status", + "colDate": "Date", + "statusPending": "Pending", + "statusCompleted": "Completed", + "statusRefunded": "Refunded", + "form": { + "name": "Name", + "namePlaceholder": "Package name", + "description": "Description", + "descriptionPlaceholder": "Package description", + "price": "Price", + "currency": "Currency", + "currencyPlaceholder": "EUR, USD, GBP…", + "type": "Type", + "typeCredits": "Credits", + "typeDuckets": "Duckets", + "typeDiamonds": "Diamonds", + "typeBadge": "Badge", + "typeFurni": "Furniture", + "typeRank": "Rank", + "amount": "Amount", + "badgeCode": "Badge code", + "badgeCodePlaceholder": "e.g. VIP001", + "rankId": "Rank ID", + "furniItems": "Furniture items", + "enabled": "Enabled", + "save": "Save package", + "saving": "Saving…", + "cancel": "Cancel" + } + }, + "transactions": { + "title": "Transactions", + "subtitle": "PayPal payment log", + "noTransactions": "No transactions yet", + "colId": "ID", + "colUser": "User", + "colAmount": "Amount", + "colCurrency": "Currency", + "colPackage": "Package", + "colStatus": "Status", + "colDate": "Date", + "colActions": "Actions", + "statusCompleted": "Completed", + "statusPending": "Pending", + "statusFailed": "Failed", + "statusRefunded": "Refunded", + "detail": { + "title": "Transaction #{id}", + "paypalId": "PayPal ID", + "payerEmail": "Payer email", + "grossAmount": "Gross amount", + "fee": "Fee", + "netAmount": "Net amount" + } + }, + "vouchers": { + "title": "Shop Vouchers", + "subtitle": "Manage redeemable voucher codes", + "newVoucher": "+ New voucher", + "edit": "Edit", + "delete": "Delete", + "noVouchers": "No vouchers yet", + "confirmDelete": "Delete voucher \"{code}\"?", + "colId": "ID", + "colCode": "Code", + "colValue": "Value", + "colType": "Type", + "colUses": "Uses / Max", + "colExpires": "Expires", + "colCreated": "Created", + "colActions": "Actions", + "form": { + "code": "Code", + "codePlaceholder": "e.g. SUMMER2025", + "value": "Value", + "type": "Type", + "typeCredits": "Credits", + "typeDuckets": "Duckets", + "typeDiamonds": "Diamonds", + "typeBadge": "Badge", + "typeFurni": "Furniture", + "typeRank": "Rank", + "badgeCode": "Badge code", + "badgeCodePlaceholder": "e.g. VIP001", + "rankId": "Rank ID", + "maxUses": "Max uses", + "maxUsesPlaceholder": "0 = unlimited", + "expiresAt": "Expires at", + "enabled": "Enabled", + "save": "Save voucher", + "saving": "Saving…", + "cancel": "Cancel" + } + }, + "subscriptions": { + "title": "Subscriptions", + "subtitle": "Active recurring subscriptions", + "noSubscriptions": "No active subscriptions", + "cancel": "Cancel", + "confirmCancel": "Cancel subscription for {username}?", + "colId": "ID", + "colUser": "User", + "colPackage": "Package", + "colAmount": "Amount", + "colInterval": "Interval", + "colStarted": "Started", + "colNextBilling": "Next billing", + "colStatus": "Status", + "colActions": "Actions", + "statusActive": "Active", + "statusCancelled": "Cancelled", + "statusExpired": "Expired" + }, + "rareValues": { + "title": "Rare Values", + "subtitle": "Manage rare value categories and item prices", + "categories": "Categories", + "items": "Items", + "addCategory": "Add category", + "editCategory": "Edit category", + "deleteCategory": "Delete category", + "noCategories": "No rare value categories yet", + "confirmDeleteCategory": "Delete category \"{name}\" and all its items?", + "colId": "ID", + "colName": "Name", + "colItems": "Items", + "colUpdated": "Updated", + "colActions": "Actions", + "colItemName": "Item name", + "colCredits": "Credits", + "colPoints": "Points", + "colPointType": "Point type", + "categoryForm": { + "name": "Name", + "namePlaceholder": "Category name", + "save": "Save category", + "saving": "Saving…", + "cancel": "Cancel" + }, + "itemForm": { + "itemName": "Item name", + "itemNamePlaceholder": "e.g. Throne", + "credits": "Credits value", + "points": "Points value", + "pointType": "Point type", + "pointTypeDuckets": "Duckets", + "pointTypeDiamonds": "Diamonds", + "save": "Save item", + "saving": "Saving…", + "cancel": "Cancel" + } + }, + "badges": { + "title": "Badges", + "subtitle": "Upload, give and manage badge catalogue", + "upload": "Upload", + "giveBadge": "Give badge", + "catalogue": "Catalogue", + "uploadTitle": "Upload a new badge", + "uploadSubtitle": "Upload a badge image file", + "badgeCode": "Badge code", + "badgeCodePlaceholder": "e.g. VIP001", + "badgeFile": "Badge file", + "badgeFileHint": "PNG or GIF, recommended 40x40", + "uploadButton": "Upload", + "uploading": "Uploading…", + "giveTitle": "Give badge to user", + "giveSubtitle": "Grant a badge to a specific user", + "username": "Username", + "usernamePlaceholder": "Target username", + "giveButton": "Give badge", + "giving": "Giving…", + "catalogueTitle": "Badge catalogue", + "catalogueSubtitle": "All available badges in the hotel", + "noBadges": "No badges in the catalogue", + "search": "Search badges", + "searchPlaceholder": "Search by code or name…", + "colCode": "Code", + "colImage": "Image", + "colName": "Name", + "colDescription": "Description", + "colActions": "Actions" + }, + "achievements": { + "title": "Achievements", + "subtitle": "Read-only list of in-game achievements", + "noAchievements": "No achievements configured", + "colId": "ID", + "colName": "Name", + "colCategory": "Category", + "colLevels": "Levels", + "colReward": "Reward", + "colProgress": "Progress", + "levelLabel": "Level {level}", + "rewardCredits": "{count} credits", + "rewardPoints": "{count} {points}" + }, + "calendar": { + "title": "Calendar campaigns", + "subtitle": "Read-only list of calendar campaigns and events", + "noCampaigns": "No calendar campaigns yet", + "colId": "ID", + "colName": "Name", + "colStart": "Start", + "colEnd": "End", + "colType": "Type", + "colReward": "Reward", + "colActions": "Actions", + "detail": { + "title": "Campaign: {name}", + "subtitle": "Calendar event details", + "days": "Days", + "dayLabel": "Day {day}", + "reward": "Reward", + "noDays": "No days configured for this campaign" + } + }, + "radio": { + "title": "Radio Moderation", + "subtitle": "Latest shouts, DJ applications and on-air schedules (25 most recent of each).", + "moderation": "Moderation", + "shouts": "Shouts", + "applications": "Applications", + "schedules": "Schedules", + "settings": "Settings", + "embed": "Embed", + "ranks": "Ranks", + "history": "History", + "apiKeys": "API Keys", + "points": "Points", + "autoDj": "AutoDJ", + "monitoring": "Monitoring", + "user": "User", + "message": "Message", + "posted": "Posted", + "realName": "Real Name", + "age": "Age", + "style": "Style", + "applied": "Applied", + "day": "Day", + "start": "Start", + "end": "End", + "show": "Show", + "status": "Status", + "active": "active", + "inactive": "inactive", + "hidden": "hidden", + "activeSchedules": "Active schedules", + "noShouts": "No shouts to moderate", + "noApplications": "No applications submitted.", + "noSchedules": "No schedules configured.", + "deleteShout": "Delete", + "confirmDeleteShout": "Delete this shout?", + "pendingApplications": "Pending applications", + "settingsForm": { + "title": "Radio Settings", + "subtitle": "Edits write to website_settings (radio_* keys) and bust the runtime settings cache. Booleans use 0 / 1.", + "enabled": "Enabled", + "disabled": "Disabled", + "totalKeys": "Total keys", + "configured": "Configured", + "curatedHint": "curated keys with a value", + "customKeys": "Custom keys", + "saveAll": "Save all", + "singleKey": "Add / overwrite a single key", + "keyPlaceholder": "radio_custom_key", + "valuePlaceholder": "value", + "commentPlaceholder": "comment (optional)", + "saveSingle": "Save", + "otherSettings": "Other radio settings", + "dbError": "Could not load settings (database unavailable). Defaults will be created on save." + }, + "embedPage": { + "title": "Radio Embed", + "subtitle": "Copy-ready embed code for the live stream. Read-only \u2014 nothing here writes to the database. Configure the stream URL in Radio Settings (radio_stream_url).", + "streamUrl": "Stream URL", + "configured": "Configured", + "missing": "Missing", + "radioName": "Radio name", + "nowPlayingApi": "Now-playing API", + "set": "Set", + "notSet": "Not set", + "noStreamUrl": "No stream URL configured", + "noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.", + "goToSettings": "Go to Radio Settings", + "livePreview": "Live preview", + "livePreviewText": "This is the bare