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

+268 -32
View File
@@ -878,7 +878,8 @@
"codePlaceholder": "6-digit code",
"confirm": "Confirm",
"enableIntro": "Add a second layer of security with an authenticator app.",
"enable": "Enable 2FA"
"enable": "Enable 2FA",
"rateLimit": "Too many attempts. Please wait before trying again."
},
"login": {
"title": "Sign in",
@@ -1119,7 +1120,8 @@
"rankWithCount": "Rank {rank}: {count} accounts",
"recentStaffActivity": "Recent staff activity",
"viewAll": "View all",
"noStaffActivityLogged": "No staff activity logged yet"
"noStaffActivityLogged": "No staff activity logged yet",
"totalAccounts": "{count} total accounts"
},
"articles": {
"title": "Articles",
@@ -1160,7 +1162,11 @@
"statusPublished": "Published",
"publishAt": "Publish at",
"publishedAt": "Published at"
}
},
"articlesShown": "Articles shown",
"latest": "Latest article",
"mostRecentPost": "Most recent publication",
"recentArticles": "Recent articles"
},
"events": {
"newEvent": "+ New event",
@@ -1281,7 +1287,13 @@
"colColour": "Colour",
"colUsedBy": "Used by",
"colCreated": "Created",
"colActions": "Actions"
"colActions": "Actions",
"databaseError": "Tags could not be loaded. Please try again.",
"totalTags": "Total tags",
"inUse": "In use",
"attachedToContent": "Attached to content",
"unused": "Unused",
"allTags": "All tags"
},
"navigation": {
"title": "Navigator",
@@ -1305,7 +1317,12 @@
"colName": "Name",
"colOrder": "Order",
"colRooms": "Rooms",
"colActions": "Actions"
"colActions": "Actions",
"publicFlatcats": "Public room categories",
"markedPublic": "Categories marked as public",
"visibleRooms": "Visible rooms",
"rows": "rows",
"unnamed": "Unnamed room"
},
"helpCenter": {
"title": "Help Center",
@@ -1332,7 +1349,9 @@
"saving": "Saving…",
"deleteEntry": "Delete entry",
"cancel": "Cancel"
}
},
"withButton": "With a button",
"smallBoxes": "Small boxes"
},
"ads": {
"title": "Advertisements",
@@ -1394,8 +1413,17 @@
"save": "Save box",
"saving": "Saving…",
"deleteBox": "Delete box",
"cancel": "Cancel"
}
"cancel": "Cancel",
"iconEmoji": "Icon or emoji"
},
"totalBoxes": "Total boxes",
"active": "Active",
"shownOnHomePage": "Shown on the home page",
"position": "Position",
"boxes": "Content boxes",
"hidden": "Hidden",
"hide": "Hide",
"activate": "Activate"
},
"photos": {
"title": "Photo Moderation",
@@ -1522,7 +1550,20 @@
"save": "Save ban",
"saving": "Saving…",
"cancel": "Cancel"
}
},
"activeBans": "Active bans",
"permanent": "Permanent bans",
"accountBans": "Account bans",
"userIdPlaceholder": "User ID",
"typeAccount": "Account",
"typeIp": "IP address",
"typeMachine": "Device",
"typeSuper": "Account, IP and device",
"hoursPlaceholder": "Duration in hours",
"reasonPlaceholder": "Reason for the ban",
"ban": "Ban user",
"colUserId": "User ID",
"lift": "Lift ban"
},
"ip": {
"title": "IP Management",
@@ -1542,7 +1583,11 @@
"colIp": "IP",
"colReason": "Reason",
"colAdded": "Added",
"colActions": "Actions"
"colActions": "Actions",
"whitelisted": "Allowed IP addresses",
"blacklisted": "Blocked IP addresses",
"asnPlaceholder": "ASN (optional)",
"colAsn": "ASN"
},
"vpn": {
"title": "VPN / Proxy Detection",
@@ -1555,7 +1600,21 @@
"blockOnDetectHint": "Automatically block users detected behind a VPN or proxy",
"save": "Save settings",
"saving": "Saving…",
"settingsSaved": "VPN settings saved successfully"
"settingsSaved": "VPN settings saved successfully",
"protection": "VPN protection",
"on": "Enabled",
"off": "Disabled",
"providerLabel": "Provider: {provider}",
"notScreened": "Connections are not checked for VPN use.",
"settings": "VPN settings",
"provider": "Provider",
"providerHint": "Choose the service used to check incoming connections.",
"blockMessage": "Blocked connection message",
"blockMessagePlaceholder": "Please disable your VPN and try again.",
"blockMessageHint": "Shown when a connection is blocked by VPN protection.",
"rawIpTitle": "IP address lists",
"rawIpDescription": "Manage individual addresses in <ipLink>IP management</ipLink>.",
"openIpManagement": "Open IP management"
},
"tickets": {
"title": "CMS support desk",
@@ -1685,7 +1744,10 @@
"status": "Status",
"submitted": "Submitted",
"noMessage": "No message provided"
}
},
"applicationsTitle": "Applications ({count})",
"userNumber": "User #{id}",
"rankLabel": "Rank {rank}"
},
"teams": {
"title": "Teams",
@@ -1713,7 +1775,26 @@
"save": "Save team definition",
"saving": "Saving…",
"cancel": "Cancel"
}
},
"teamRanks": "Staff ranks",
"visible": "Visible",
"hidden": "Hidden",
"newTeamRank": "New staff rank",
"rankName": "Rank name",
"rankNamePlaceholder": "For example: Moderator",
"badgeCode": "Badge code",
"badgeCodePlaceholder": "For example: ADM",
"jobDescription": "Job description",
"jobDescriptionPlaceholder": "Describe this staff role",
"staffColor": "Staff colour",
"staffColorPlaceholder": "For example: #3366cc",
"hiddenRank": "Hide from the staff page",
"createTeam": "Create staff rank",
"ranksTitle": "Staff ranks ({count})",
"badge": "Badge",
"color": "Colour",
"visibility": "Visibility",
"delete": "Delete"
},
"permissions": {
"title": "Permissions",
@@ -1888,8 +1969,23 @@
"enabled": "Enabled",
"save": "Save package",
"saving": "Saving…",
"cancel": "Cancel"
}
"cancel": "Cancel",
"infoPlaceholder": "Describe the package contents",
"iconPlaceholder": "Icon URL",
"colorPlaceholder": "For example: #3366cc",
"costsPlaceholder": "Price",
"creditsPlaceholder": "Number of credits",
"ducketsPlaceholder": "Number of duckets",
"diamondsPlaceholder": "Number of diamonds",
"giveRankPlaceholder": "Rank ID (0 for none)",
"badgesPlaceholder": "Badge codes separated by semicolons",
"positionPlaceholder": "Display order",
"create": "Create package",
"deletePackage": "Delete package"
},
"newPackageTitle": "New shop package",
"backToShop": "Back to shop",
"editPackageTitle": "Edit shop package"
},
"marketplace": {
"title": "Marketplace",
@@ -2056,7 +2152,15 @@
"colActions": "Actions",
"statusActive": "Active",
"statusCancelled": "Cancelled",
"statusExpired": "Expired"
"statusExpired": "Expired",
"activeSubs": "Active subscriptions",
"types": "Subscription types",
"expiringSoon": "Expiring soon",
"activeSubscriptions": "Active subscriptions",
"colSubId": "Subscription ID",
"colUserId": "User ID",
"colType": "Type",
"colExpires": "Expires"
},
"rareValues": {
"title": "Rare Values",
@@ -2095,8 +2199,24 @@
"pointTypeDiamonds": "Diamonds",
"save": "Save item",
"saving": "Saving…",
"cancel": "Cancel"
}
"cancel": "Cancel",
"itemIdPlaceholder": "Item ID",
"iconPlaceholder": "Icon URL",
"creditsPlaceholder": "Credits",
"currencyValuePlaceholder": "Currency value",
"addValue": "Add value"
},
"badgePlaceholder": "Category badge code",
"priorityPlaceholder": "Priority",
"addCategoryButton": "Add category",
"badgeLabel": "Badge: {badge}",
"priorityLabel": "Priority: {priority}",
"itemCount": "{count} items",
"colItemId": "Item ID",
"colCurrencyValue": "Currency value",
"colCurrency": "Currency",
"colIcon": "Icon",
"noValuesInCategory": "No values in this category yet."
},
"badges": {
"title": "Badges",
@@ -2127,7 +2247,10 @@
"colImage": "Image",
"colName": "Name",
"colDescription": "Description",
"colActions": "Actions"
"colActions": "Actions",
"uploadedMessage": "Badge {code} uploaded.",
"errorMessage": "Upload failed: {error}",
"userIdPlaceholder": "User ID"
},
"achievements": {
"title": "Achievements",
@@ -2141,7 +2264,14 @@
"colProgress": "Progress",
"levelLabel": "Level {level}",
"rewardCredits": "{count} credits",
"rewardPoints": "{count} {points}"
"rewardPoints": "{count} {points}",
"achievementRows": "Achievement levels",
"distinctAchievements": "Distinct achievements",
"categories": "Categories",
"colLevel": "Level",
"colRewardType": "Reward type",
"colRewardAmount": "Reward amount",
"colPoints": "Points"
},
"calendar": {
"title": "Calendar campaigns",
@@ -2160,8 +2290,32 @@
"days": "Days",
"dayLabel": "Day {day}",
"reward": "Reward",
"noDays": "No days configured for this campaign"
}
"noDays": "No days configured for this campaign",
"campaignMeta": "#{id} · {days} days · starts {date}",
"disabled": "Disabled",
"enabled": "Enabled",
"locksExpired": "Expired days are locked",
"rewards": "Rewards",
"noRewards": "No rewards configured.",
"colId": "ID",
"colProduct": "Product",
"colItem": "Item",
"colCredits": "Credits",
"colPixels": "Duckets",
"colPoints": "Points",
"colBadge": "Badge",
"colSubscription": "Subscription",
"typeLabel": "Type {type}",
"rewardCount": "{count} rewards"
},
"campaigns": "Campaigns",
"enabled": "Enabled campaigns",
"totalRewards": "Total rewards",
"statusEnabled": "Enabled",
"statusDisabled": "Disabled",
"campaignMeta": "#{id} · {days} days · {rewards} rewards",
"startsAt": "Starts: {date}",
"locksExpired": "Expired days are locked"
},
"radio": {
"title": "Radio Moderation",
@@ -2231,7 +2385,7 @@
"noStreamUrlText": "There is no radio_stream_url set, so there is nothing to embed yet.",
"goToSettings": "Go to Radio Settings",
"livePreview": "Live preview",
"livePreviewText": "This is the bare <audio> player as visitors will see it. Press play to confirm the stream is reachable.",
"livePreviewText": "This is the bare audio player as visitors will see it. Press play to confirm the stream is reachable.",
"source": "Source",
"audioSnippet": "Audio player snippet",
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
@@ -2355,7 +2509,8 @@
"pointsPerMinuteHint": "Amount awarded for each full minute of listening.",
"maxPerDayHint": "Daily cap per user. Use 0 for unlimited.",
"minListenersHint": "Points are only awarded when at least this many people are tuned in.",
"save": "Save points settings"
"save": "Save points settings",
"saved": "Settings saved."
},
"autoDjForm": {
"title": "Radio AutoDJ",
@@ -2408,7 +2563,7 @@
"noNowPlayingHint": "no now-playing API set",
"noListenersHint": "no listeners API set",
"endpointDiagnostics": "Endpoint diagnostics",
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> reachable · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]\">WARN</span> not configured · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">ERROR</span> fetch failed / timed out.",
"diagnosticsLegend": "OK: reachable · WARN: not configured · ERROR: request failed or timed out.",
"streamUrl": "Stream URL",
"unreachable": "unreachable",
"refresh": "Refresh"
@@ -2589,7 +2744,16 @@
"valuePlaceholder": "Enter value…",
"save": "Save changes",
"cancel": "Cancel"
}
},
"settingsKeys": "Settings",
"textKeys": "Text entries",
"keysCount": "{count} keys",
"addOverwriteSetting": "Add or update a setting",
"key": "Key",
"value": "Value",
"showingFirst": "Showing the first {count}",
"addOverwriteText": "Add or update a text",
"truncatedHint": "Showing {shown} of {total} entries."
},
"alerts": {
"title": "Alerts",
@@ -2857,7 +3021,25 @@
"save": "Save settings",
"saving": "Saving…",
"confirmEnable": "Enabling maintenance will prevent all non-staff users from accessing the hotel. Continue?",
"confirmDisable": "Disable maintenance mode and restore access for all users?"
"confirmDisable": "Disable maintenance mode and restore access for all users?",
"maintenance": "Maintenance",
"hotelLockedDown": "Access to the hotel is restricted.",
"hotelOpen": "The hotel is open.",
"minLoginRank": "Minimum login rank",
"duringMaintenance": "During maintenance",
"splashMessage": "Maintenance message",
"set": "Configured",
"empty": "Empty",
"currentState": "Current status",
"maintenanceCurrently": "Maintenance: {status}",
"onlyRankCanLogin": "Only users with rank {rank} or higher can log in.",
"hotelOpenToEveryone": "All eligible users can log in.",
"settings": "Settings",
"enableMaintenance": "Enable maintenance mode",
"minLoginRankLabel": "Minimum rank allowed during maintenance",
"maintenanceMessageLabel": "Message shown to visitors",
"maintenanceMessagePlaceholder": "Explain why the hotel is unavailable.",
"saveChanges": "Save changes"
},
"housekeeping": {
"title": "Permission archive",
@@ -2941,7 +3123,7 @@
"tabTest": "Test",
"tabAudit": "Audit log",
"importPermissions": "Import permissions",
"importHint": "Paste a JSON array of permission objects. Format: [{ \"permission\": \"...\", \"minRank\": 7, \"description\": \"...\", \"groupName\": \"...\", \"dependsOn\": \"...\" }]",
"importHint": "Paste a JSON array of permission objects with the fields permission, minRank, description, groupName and dependsOn.",
"importPlaceholder": "Paste JSON here…",
"import": "Import",
"exportTitle": "Export permissions",
@@ -3058,8 +3240,13 @@
"variablesHint": "{username}, {email}, {hotel_name}",
"save": "Save template",
"saving": "Saving…",
"cancel": "Cancel"
}
"cancel": "Cancel",
"variablesPlaceholder": "Variables, separated by commas",
"active": "Active"
},
"create": "Create template",
"save": "Save template",
"updated": "Updated: {date}"
},
"theme": {
"title": "Theme",
@@ -3085,7 +3272,12 @@
"saveCss": "Save CSS",
"savingCss": "Saving…",
"reset": "Reset to defaults",
"confirmReset": "Reset theme to default values?"
"confirmReset": "Reset theme to default values?",
"saved": "Theme settings saved.",
"presetApplied": "Preset applied: {preset}",
"typographyLayout": "Typography and layout",
"font": "Font",
"borderRadius": "Corner radius"
},
"media": {
"title": "Media Library",
@@ -3170,6 +3362,43 @@
"subtitle": "User-facing strings from emulator_settings (commands, wordfilter, pet/matchmaking messages).",
"infoBox": "Configuration settings (ports, timeouts, IDs) do not appear here — use the full settings link. Saved changes are synced to the emulator via RCON.",
"fullSettings": "Full settings"
},
"cms": {
"title": "CMS translations",
"description": "Edit the text shown on the website and in housekeeping. Saved changes are applied on the next page load and survive deployments.",
"export": "Export JSON",
"saving": "Saving…",
"saveLocale": "Save {locale}",
"saved": "Translations saved and applied.",
"readOnly": "You can review and export translations. Editing requires permission to change settings.",
"language": "Language",
"coverage": "{total} keys · {missing} missing · {identical} identical to English",
"search": "Search",
"searchPlaceholder": "Key, translation or English source…",
"group": "Group",
"allGroups": "All groups",
"status": "Show",
"all": "All texts",
"missing": "Missing translations",
"identical": "Identical to English",
"modified": "Unsaved changes",
"fallbackHint": "Missing translations use English. Identical text may be intentional. Keep variable names inside braces unchanged.",
"unsaved": "{count} unsaved changes in this language",
"discard": "Discard changes",
"discardConfirm": "Discard unsaved changes in this language?",
"fixErrors": "Fix {count} invalid messages before saving.",
"noResults": "No texts match these filters.",
"source": "English reference",
"missingPlaceholder": "Translation missing; English is used.",
"page": "Page {page} of {total} · {count} keys",
"previous": "Previous",
"next": "Next",
"leaveWarning": "You have unsaved translations. Leave this page?",
"conflict": "These translations changed since you opened the editor. Export your changes, then reload before saving.",
"unknownKey": "Unknown translation key: {key}",
"empty": "Enter a translation for {key}, or discard this change.",
"invalid": "Invalid message syntax in {key}. Check braces and rich-text tags.",
"variables": "Keep the same variable names and rich-text tags as the English source in {key}."
}
},
"settings": {
@@ -3299,7 +3528,14 @@
"pages": "Pages and tools",
"description": "Find an admin page or search hotel records.",
"loadError": "Records could not be loaded. You can still open the pages above.",
"placeholder": "Search pages, users, rooms…"
"placeholder": "Search pages, users, rooms…",
"users": "Users",
"articles": "Articles",
"rooms": "Rooms",
"guilds": "Groups",
"shop": "Shop",
"rareValues": "Rare values",
"noResults": "No results found."
},
"table": {
"search": "Search…",