🆙 Fix texts

This commit is contained in:
duckietm
2026-05-21 08:59:26 +02:00
parent e9591acc0e
commit 690a196d42
3 changed files with 296 additions and 372 deletions
+266 -256
View File
@@ -1,258 +1,268 @@
{ {
"notification.badge.received": "Nuovo Distintivo!", "notification.badge.received": "Nuovo Distintivo!",
"wiredfurni.badgereceived.title": "Distintivo ricevuto!", "wiredfurni.badgereceived.title": "Distintivo ricevuto!",
"wiredfurni.badgereceived.body": "Hai appena ricevuto un nuovo Distintivo! Controlla nel tuo Inventario!", "wiredfurni.badgereceived.body": "Hai appena ricevuto un nuovo Distintivo! Controlla nel tuo Inventario!",
"friendlist.search": "Search friends", "friendlist.search": "Search friends",
"purse.seasonal.currency.101": "cash", "purse.seasonal.currency.101": "cash",
"widget.chooser.checkall": "Select furniture", "widget.chooser.checkall": "Select furniture",
"widget.chooser.btn.pickall": "pick up selected items!", "widget.chooser.btn.pickall": "pick up selected items!",
"wiredfurni.params.requireall.2": "If one of the selected furni has an avatar", "wiredfurni.params.requireall.2": "If one of the selected furni has an avatar",
"wiredfurni.params.requireall.3": "If all selected furni have avatars on them", "wiredfurni.params.requireall.3": "If all selected furni have avatars on them",
"widget.settings.general": "General", "widget.settings.general": "General",
"widget.settings.general.title": "Adjust the default Nitro settings", "widget.settings.general.title": "Adjust the default Nitro settings",
"widget.settings.volume": "Volume", "widget.settings.volume": "Volume",
"widget.settings.interface": "Interface", "widget.settings.interface": "Interface",
"widget.settings.interface.title": "Adjust the interface settings", "widget.settings.interface.title": "Adjust the interface settings",
"widget.settings.interface.fps.automatic": "Set FPS to unlimited", "widget.settings.interface.fps.automatic": "Set FPS to unlimited",
"widget.settings.interface.fps.warning": "Setting FPS to unlimited may cause performance issues!", "widget.settings.interface.fps.warning": "Setting FPS to unlimited may cause performance issues!",
"widget.settings.interface.secondary": "Change the window header color", "widget.settings.interface.secondary": "Change the window header color",
"widget.settings.interface.reset": "Reset header color to default", "widget.settings.interface.reset": "Reset header color to default",
"widget.room.chat.hide_pets": "Hide pets", "widget.room.chat.hide_pets": "Hide pets",
"widget.room.chat.hide_avatars": "Hide avatars", "widget.room.chat.hide_avatars": "Hide avatars",
"widget.room.chat.hide_balloon": "Hide speech bubble", "widget.room.chat.hide_balloon": "Hide speech bubble",
"widget.room.chat.show_balloon": "Speech bubble", "widget.room.chat.show_balloon": "Speech bubble",
"widget.room.chat.clear_history": "clear history", "widget.room.chat.clear_history": "clear history",
"widget.room.youtube.shared": "YouTube is being shared", "widget.room.youtube.shared": "YouTube is being shared",
"widget.room.youtube.open_video": "Open the video", "widget.room.youtube.open_video": "Open the video",
"wiredfurni.tooltip.select.tile": "Select tile", "wiredfurni.tooltip.select.tile": "Select tile",
"wiredfurni.tooltip.remove.tile": "Deselect tile", "wiredfurni.tooltip.remove.tile": "Deselect tile",
"wiredfurni.tooltip.remove.5x5_tile": "select 5x5 tiles", "wiredfurni.tooltip.remove.5x5_tile": "select 5x5 tiles",
"wiredfurni.tooltip.remove.clear_tile": "Clear all selections", "wiredfurni.tooltip.remove.clear_tile": "Clear all selections",
"wiredfurni.params.furni_neighborhood.group.user": "Players", "wiredfurni.params.furni_neighborhood.group.user": "Players",
"wiredfurni.params.furni_neighborhood.group.furni": "Furniture", "wiredfurni.params.furni_neighborhood.group.furni": "Furniture",
"wiredfurni.params.selector_option.bot": "No bots", "wiredfurni.params.selector_option.bot": "No bots",
"wiredfurni.params.selector_option.pet": "No pets", "wiredfurni.params.selector_option.pet": "No pets",
"catalog.title": "Catalog", "catalog.title": "Catalog",
"catalog.favorites": "Favorites", "catalog.favorites": "Favorites",
"catalog.favorites.pages": "Pages", "catalog.favorites.pages": "Pages",
"catalog.favorites.furni": "Furni", "catalog.favorites.furni": "Furni",
"catalog.favorites.empty": "No favorites", "catalog.favorites.empty": "No favorites",
"catalog.favorites.empty.hint": "Click the heart on furni or the star on pages to add them.", "catalog.favorites.empty.hint": "Click the heart on furni or the star on pages to add them.",
"catalog.admin": "Admin", "catalog.admin": "Admin",
"catalog.admin.new": "New", "catalog.admin.new": "New",
"catalog.admin.root": "Root", "catalog.admin.root": "Root",
"catalog.admin.new.root.category": "New root category", "catalog.admin.new.root.category": "New root category",
"catalog.admin.edit.root": "Edit Root", "catalog.admin.edit.root": "Edit Root",
"catalog.admin.edit": "Edit:", "catalog.admin.edit": "Edit:",
"catalog.admin.edit.page": "Edit Page", "catalog.admin.edit.page": "Edit Page",
"catalog.admin.hidden": "hidden", "catalog.admin.hidden": "hidden",
"catalog.admin.edit.title": "Edit \"%name%\"", "catalog.admin.edit.title": "Edit \"%name%\"",
"catalog.admin.show": "Show", "catalog.admin.show": "Show",
"catalog.admin.hide": "Hide", "catalog.admin.hide": "Hide",
"catalog.admin.delete": "Delete", "catalog.admin.delete": "Delete",
"catalog.admin.delete.title": "Delete \"%name%\"", "catalog.admin.delete.title": "Delete \"%name%\"",
"catalog.admin.delete.category.confirm": "Delete category \"%name%\" and all its content?", "catalog.admin.delete.category.confirm": "Delete category \"%name%\" and all its content?",
"catalog.admin.delete.page": "Delete page", "catalog.admin.delete.page": "Delete page",
"catalog.admin.delete.page.confirm": "Delete page \"%name%\"?", "catalog.admin.delete.page.confirm": "Delete page \"%name%\"?",
"catalog.admin.delete.offer.confirm": "Are you sure you want to delete this offer?", "catalog.admin.delete.offer.confirm": "Are you sure you want to delete this offer?",
"catalog.admin.create": "Create", "catalog.admin.create": "Create",
"catalog.admin.save": "Save", "catalog.admin.save": "Save",
"catalog.admin.create.subpage": "Create sub-page", "catalog.admin.create.subpage": "Create sub-page",
"catalog.admin.order": "Order", "catalog.admin.order": "Order",
"catalog.admin.visible": "Visible", "catalog.admin.visible": "Visible",
"catalog.admin.enabled": "Enabled", "catalog.admin.enabled": "Enabled",
"catalog.admin.offer.new": "New Offer", "catalog.admin.offer.new": "New Offer",
"catalog.admin.offer.edit": "Edit Offer", "catalog.admin.offer.edit": "Edit Offer",
"catalog.admin.offer.name": "Catalog Name", "catalog.admin.offer.name": "Catalog Name",
"catalog.admin.offer.general": "General", "catalog.admin.offer.general": "General",
"catalog.admin.offer.quantity": "Quantity", "catalog.admin.offer.quantity": "Quantity",
"catalog.admin.offer.prices": "Prices", "catalog.admin.offer.prices": "Prices",
"catalog.admin.offer.credits": "Credits", "catalog.admin.offer.credits": "Credits",
"catalog.admin.offer.points": "Points", "catalog.admin.offer.points": "Points",
"catalog.admin.offer.points.type": "Points Type", "catalog.admin.offer.points.type": "Points Type",
"catalog.admin.offer.options": "Options", "catalog.admin.offer.options": "Options",
"catalog.admin.offer.club.only": "Club Only", "catalog.admin.offer.club.only": "Club Only",
"catalog.admin.offer.extradata": "Extra Data (optional)....", "catalog.admin.offer.extradata": "Extra Data (optional)....",
"catalog.admin.offer.have.offer": "Multi-discount (have_offer)", "catalog.admin.offer.have.offer": "Multi-discount (have_offer)",
"catalog.trophies.title": "Trophies", "catalog.trophies.title": "Trophies",
"catalog.trophies.write.hint": "Write a text for the trophy before purchasing", "catalog.trophies.write.hint": "Write a text for the trophy before purchasing",
"catalog.trophies.inscription": "Trophy Inscription", "catalog.trophies.inscription": "Trophy Inscription",
"catalog.trophies.inscription.placeholder": "Write the text that will appear on the trophy...", "catalog.trophies.inscription.placeholder": "Write the text that will appear on the trophy...",
"catalog.pets.show.colors": "Show colors", "catalog.pets.show.colors": "Show colors",
"catalog.pets.choose.color": "Choose color", "catalog.pets.choose.color": "Choose color",
"catalog.pets.choose.breed": "Choose breed", "catalog.pets.choose.breed": "Choose breed",
"catalog.pets.back.breeds": "? Breeds", "catalog.pets.back.breeds": "? Breeds",
"catalog.prefix.text": "Text", "catalog.prefix.text": "Text",
"catalog.prefix.text.placeholder": "Enter text...", "catalog.prefix.text.placeholder": "Enter text...",
"catalog.prefix.icon": "Icon", "catalog.prefix.icon": "Icon",
"catalog.prefix.icon.remove": "Remove icon", "catalog.prefix.icon.remove": "Remove icon",
"catalog.prefix.effect": "Effect", "catalog.prefix.effect": "Effect",
"catalog.prefix.color": "Color", "catalog.prefix.color": "Color",
"catalog.prefix.color.single": "?? Single", "catalog.prefix.color.single": "?? Single",
"catalog.prefix.color.per.letter": "?? Per Letter", "catalog.prefix.color.per.letter": "?? Per Letter",
"catalog.prefix.color.hint": "Select a letter, then choose the color. Auto-advances.", "catalog.prefix.color.hint": "Select a letter, then choose the color. Auto-advances.",
"catalog.prefix.color.apply.all.title": "Apply current color to all letters", "catalog.prefix.color.apply.all.title": "Apply current color to all letters",
"catalog.prefix.color.apply.all": "Apply to all", "catalog.prefix.color.apply.all": "Apply to all",
"catalog.prefix.color.selected": "Selected letter:", "catalog.prefix.color.selected": "Selected letter:",
"catalog.prefix.price": "Price:", "catalog.prefix.price": "Price:",
"catalog.prefix.price.amount": "5 Credits", "catalog.prefix.price.amount": "5 Credits",
"catalog.prefix.purchased": "? Purchased!", "catalog.prefix.purchased": "? Purchased!",
"catalog.prefix.purchase": "Purchase", "catalog.prefix.purchase": "Purchase",
"modtools.userinfo.title": "User Info: %username%", "modtools.userinfo.title": "User Info: %username%",
"modtools.userinfo.userName": "Name", "modtools.userinfo.userName": "Name",
"modtools.userinfo.cfhCount": "CFHs", "modtools.userinfo.cfhCount": "CFHs",
"modtools.userinfo.abusiveCfhCount": "Abusive CFHs", "modtools.userinfo.abusiveCfhCount": "Abusive CFHs",
"modtools.userinfo.cautionCount": "Cautions", "modtools.userinfo.cautionCount": "Cautions",
"modtools.userinfo.banCount": "Bans", "modtools.userinfo.banCount": "Bans",
"modtools.userinfo.lastSanctionTime": "Last Sanction", "modtools.userinfo.lastSanctionTime": "Last Sanction",
"modtools.userinfo.tradingLockCount": "Trade Locks", "modtools.userinfo.tradingLockCount": "Trade Locks",
"modtools.userinfo.tradingExpiryDate": "Lock Expires", "modtools.userinfo.tradingExpiryDate": "Lock Expires",
"modtools.userinfo.minutesSinceLastLogin": "Last Login", "modtools.userinfo.minutesSinceLastLogin": "Last Login",
"modtools.userinfo.lastPurchaseDate": "Last Purchase", "modtools.userinfo.lastPurchaseDate": "Last Purchase",
"modtools.userinfo.primaryEmailAddress": "Email", "modtools.userinfo.primaryEmailAddress": "Email",
"modtools.userinfo.identityRelatedBanCount": "Banned Accs", "modtools.userinfo.identityRelatedBanCount": "Banned Accs",
"modtools.userinfo.registrationAgeInMinutes": "Registered", "modtools.userinfo.registrationAgeInMinutes": "Registered",
"modtools.userinfo.userClassification": "Rank", "modtools.userinfo.userClassification": "Rank",
"modtools.window.title": "Mod Tools", "modtools.window.title": "Mod Tools",
"modtools.window.tools.room": "Room Tool", "modtools.window.tools.room": "Room Tool",
"modtools.window.tools.chatlog": "Chatlog Tool", "modtools.window.tools.chatlog": "Chatlog Tool",
"modtools.window.tools.report": "Report Tool", "modtools.window.tools.report": "Report Tool",
"modtools.window.select.user": "Select a user", "modtools.window.select.user": "Select a user",
"modtools.window.no.room": "Enter a room first", "modtools.window.no.room": "Enter a room first",
"modtools.window.user.in_room": "Still in this room", "modtools.window.user.in_room": "Still in this room",
"modtools.window.user.left_room": "No longer in this room", "modtools.window.user.left_room": "No longer in this room",
"modtools.window.user.clear": "Clear selection", "modtools.window.user.clear": "Clear selection",
"modtools.window.tickets.open": "%count% open ticket", "modtools.window.tickets.open": "%count% open ticket",
"modtools.window.tickets.open.many": "%count% open tickets", "modtools.window.tickets.open.many": "%count% open tickets",
"modtools.window.section.room": "Room", "modtools.window.section.room": "Room",
"modtools.window.section.user": "User", "modtools.window.section.user": "User",
"modtools.window.section.reports": "Reports", "modtools.window.section.reports": "Reports",
"modtools.window.user.open_info": "Open Info", "modtools.window.user.open_info": "Open Info",
"modtools.userinfo.refresh": "Refresh user info", "modtools.userinfo.refresh": "Refresh user info",
"modtools.userinfo.presence.in_room": "In room", "modtools.userinfo.presence.in_room": "In room",
"modtools.userinfo.presence.in_room.title": "In the room you are observing", "modtools.userinfo.presence.in_room.title": "In the room you are observing",
"modtools.userinfo.presence.online": "Online", "modtools.userinfo.presence.online": "Online",
"modtools.userinfo.presence.online.title": "Online on the hotel", "modtools.userinfo.presence.online.title": "Online on the hotel",
"modtools.userinfo.presence.offline": "Offline", "modtools.userinfo.presence.offline": "Offline",
"modtools.userinfo.presence.offline.title": "Offline at panel open", "modtools.userinfo.presence.offline.title": "Offline at panel open",
"modtools.userinfo.section.account": "Account", "modtools.userinfo.section.account": "Account",
"modtools.userinfo.section.activity": "Activity", "modtools.userinfo.section.activity": "Activity",
"modtools.userinfo.section.sanctions": "Sanctions", "modtools.userinfo.section.sanctions": "Sanctions",
"modtools.userinfo.section.trading": "Trading", "modtools.userinfo.section.trading": "Trading",
"modtools.userinfo.button.room.chat": "Room Chat", "modtools.userinfo.button.room.chat": "Room Chat",
"modtools.userinfo.button.send.message": "Send Message", "modtools.userinfo.button.send.message": "Send Message",
"modtools.userinfo.button.room.visits": "Room Visits", "modtools.userinfo.button.room.visits": "Room Visits",
"modtools.userinfo.button.mod.action": "Mod Action", "modtools.userinfo.button.mod.action": "Mod Action",
"modtools.userinfo.stat.cfh": "CFH", "modtools.userinfo.stat.cfh": "CFH",
"modtools.userinfo.stat.cautions": "Cautions", "modtools.userinfo.stat.cautions": "Cautions",
"modtools.userinfo.stat.bans": "Bans", "modtools.userinfo.stat.bans": "Bans",
"modtools.userinfo.stat.trade.locks": "Trade locks", "modtools.userinfo.stat.trade.locks": "Trade locks",
"modtools.roominfo.title": "Room Info", "modtools.roominfo.title": "Room Info",
"modtools.roominfo.refresh": "Refresh room info", "modtools.roominfo.refresh": "Refresh room info",
"modtools.roominfo.loading": "Loading…", "modtools.roominfo.loading": "Loading…",
"modtools.roominfo.owner.here": "Owner here", "modtools.roominfo.owner.here": "Owner here",
"modtools.roominfo.owner.away": "Owner away", "modtools.roominfo.owner.away": "Owner away",
"modtools.roominfo.owner.title.here": "The room owner is currently inside", "modtools.roominfo.owner.title.here": "The room owner is currently inside",
"modtools.roominfo.owner.title.away": "The room owner is NOT inside", "modtools.roominfo.owner.title.away": "The room owner is NOT inside",
"modtools.roominfo.stat.users": "Users", "modtools.roominfo.stat.users": "Users",
"modtools.roominfo.stat.owner": "Owner", "modtools.roominfo.stat.owner": "Owner",
"modtools.roominfo.owner.open": "Open %username%'s info", "modtools.roominfo.owner.open": "Open %username%'s info",
"modtools.roominfo.button.visit": "Visit Room", "modtools.roominfo.button.visit": "Visit Room",
"modtools.roominfo.button.chatlog": "Chatlog", "modtools.roominfo.button.chatlog": "Chatlog",
"modtools.roominfo.moderate.title": "Moderate room", "modtools.roominfo.moderate.title": "Moderate room",
"modtools.roominfo.moderate.kick": "Kick everyone out", "modtools.roominfo.moderate.kick": "Kick everyone out",
"modtools.roominfo.moderate.doorbell": "Enable the doorbell", "modtools.roominfo.moderate.doorbell": "Enable the doorbell",
"modtools.roominfo.moderate.rename": "Change room name", "modtools.roominfo.moderate.rename": "Change room name",
"modtools.roominfo.moderate.message.placeholder": "Mandatory message to deliver with the action…", "modtools.roominfo.moderate.message.placeholder": "Mandatory message to deliver with the action…",
"modtools.roominfo.moderate.send.caution": "Send Caution", "modtools.roominfo.moderate.send.caution": "Send Caution",
"modtools.roominfo.moderate.send.alert": "Send Alert", "modtools.roominfo.moderate.send.alert": "Send Alert",
"modtools.user.message.title": "Send Message", "modtools.user.message.title": "Send Message",
"modtools.user.message.recipient": "Message to", "modtools.user.message.recipient": "Message to",
"modtools.user.message.label": "Message", "modtools.user.message.label": "Message",
"modtools.user.message.placeholder": "Write something useful — the user will see it as a moderator message.", "modtools.user.message.placeholder": "Write something useful — the user will see it as a moderator message.",
"modtools.user.message.empty": "Empty", "modtools.user.message.empty": "Empty",
"modtools.user.message.chars": "%count% chars", "modtools.user.message.chars": "%count% chars",
"modtools.user.message.send": "Send Message", "modtools.user.message.send": "Send Message",
"modtools.user.modaction.title": "Mod Action: %username%", "modtools.user.modaction.title": "Mod Action: %username%",
"modtools.user.modaction.sanctioning": "Sanctioning", "modtools.user.modaction.sanctioning": "Sanctioning",
"modtools.user.modaction.step.topic": "1. CFH Topic", "modtools.user.modaction.step.topic": "1. CFH Topic",
"modtools.user.modaction.step.topic.placeholder": "Select a topic…", "modtools.user.modaction.step.topic.placeholder": "Select a topic…",
"modtools.user.modaction.step.sanction": "2. Sanction", "modtools.user.modaction.step.sanction": "2. Sanction",
"modtools.user.modaction.step.sanction.placeholder": "Select a sanction…", "modtools.user.modaction.step.sanction.placeholder": "Select a sanction…",
"modtools.user.modaction.step.message": "3. Custom message", "modtools.user.modaction.step.message": "3. Custom message",
"modtools.user.modaction.step.message.optional": "(optional — overrides default)", "modtools.user.modaction.step.message.optional": "(optional — overrides default)",
"modtools.user.modaction.message.placeholder": "Leave empty to use the default topic message", "modtools.user.modaction.message.placeholder": "Leave empty to use the default topic message",
"modtools.user.modaction.preview": "Preview", "modtools.user.modaction.preview": "Preview",
"modtools.user.modaction.button.default": "Default Sanction", "modtools.user.modaction.button.default": "Default Sanction",
"modtools.user.modaction.button.apply": "Apply Sanction", "modtools.user.modaction.button.apply": "Apply Sanction",
"modtools.user.modaction.error.no.topic": "You must select a CFH topic", "modtools.user.modaction.error.no.topic": "You must select a CFH topic",
"modtools.user.modaction.error.no.action": "You must select a CFH topic and Sanction", "modtools.user.modaction.error.no.action": "You must select a CFH topic and Sanction",
"modtools.user.modaction.error.no.permission": "You do not have permission to do this", "modtools.user.modaction.error.no.permission": "You do not have permission to do this",
"modtools.user.modaction.error.no.message": "Please write a message to user", "modtools.user.modaction.error.no.message": "Please write a message to user",
"modtools.user.modaction.error.no.permission.alert": "You have insufficient permissions", "modtools.user.modaction.error.no.permission.alert": "You have insufficient permissions",
"modtools.user.visits.title": "User Visits", "modtools.user.visits.title": "User Visits",
"modtools.user.visits.recent": "Recent visited rooms", "modtools.user.visits.recent": "Recent visited rooms",
"modtools.user.visits.entries.one": "%count% entry", "modtools.user.visits.entries.one": "%count% entry",
"modtools.user.visits.entries.many": "%count% entries", "modtools.user.visits.entries.many": "%count% entries",
"modtools.user.visits.empty": "No recent visits", "modtools.user.visits.empty": "No recent visits",
"modtools.user.visits.time": "Time", "modtools.user.visits.time": "Time",
"modtools.user.visits.room": "Room name", "modtools.user.visits.room": "Room name",
"modtools.user.visits.action": "Action", "modtools.user.visits.action": "Action",
"modtools.user.visits.visit": "Visit", "modtools.user.visits.visit": "Visit",
"modtools.user.visits.visit.title": "Visit room", "modtools.user.visits.visit.title": "Visit room",
"modtools.user.chatlog.title": "User Chatlog", "modtools.user.chatlog.title": "User Chatlog",
"modtools.user.chatlog.title.with": "User Chatlog: %username%", "modtools.user.chatlog.title.with": "User Chatlog: %username%",
"modtools.user.chatlog.loading": "Loading chatlog…", "modtools.user.chatlog.loading": "Loading chatlog…",
"modtools.room.chatlog.title": "Room Chatlog", "modtools.room.chatlog.title": "Room Chatlog",
"modtools.chatlog.column.time": "Time", "modtools.chatlog.column.time": "Time",
"modtools.chatlog.column.user": "User", "modtools.chatlog.column.user": "User",
"modtools.chatlog.column.message": "Message", "modtools.chatlog.column.message": "Message",
"modtools.chatlog.empty": "No messages", "modtools.chatlog.empty": "No messages",
"modtools.chatlog.visit": "Visit", "modtools.chatlog.visit": "Visit",
"modtools.chatlog.tools": "Tools", "modtools.chatlog.tools": "Tools",
"modtools.tickets.title": "Tickets", "modtools.tickets.title": "Tickets",
"modtools.tickets.tab.open": "Open", "modtools.tickets.tab.open": "Open",
"modtools.tickets.tab.mine": "Mine", "modtools.tickets.tab.mine": "Mine",
"modtools.tickets.tab.picked": "All picked", "modtools.tickets.tab.picked": "All picked",
"modtools.tickets.column.type": "Type", "modtools.tickets.column.type": "Type",
"modtools.tickets.column.reported": "Reported", "modtools.tickets.column.reported": "Reported",
"modtools.tickets.column.opened": "Opened", "modtools.tickets.column.opened": "Opened",
"modtools.tickets.column.picker": "Picker", "modtools.tickets.column.picker": "Picker",
"modtools.tickets.empty.open": "No open issues", "modtools.tickets.empty.open": "No open issues",
"modtools.tickets.empty.mine": "No issues picked by you", "modtools.tickets.empty.mine": "No issues picked by you",
"modtools.tickets.empty.picked": "No picked issues", "modtools.tickets.empty.picked": "No picked issues",
"modtools.tickets.action.pick": "Pick", "modtools.tickets.action.pick": "Pick",
"modtools.tickets.action.handle": "Handle", "modtools.tickets.action.handle": "Handle",
"modtools.tickets.action.release": "Release", "modtools.tickets.action.release": "Release",
"modtools.tickets.issue.title": "Resolving issue #%issueId%", "modtools.tickets.issue.title": "Resolving issue #%issueId%",
"modtools.tickets.issue.label": "Issue #%issueId%", "modtools.tickets.issue.label": "Issue #%issueId%",
"modtools.tickets.issue.details": "Details", "modtools.tickets.issue.details": "Details",
"modtools.tickets.issue.field.source": "Source", "modtools.tickets.issue.field.source": "Source",
"modtools.tickets.issue.field.category": "Category", "modtools.tickets.issue.field.category": "Category",
"modtools.tickets.issue.field.description": "Description", "modtools.tickets.issue.field.description": "Description",
"modtools.tickets.issue.field.caller": "Caller", "modtools.tickets.issue.field.caller": "Caller",
"modtools.tickets.issue.field.reported": "Reported", "modtools.tickets.issue.field.reported": "Reported",
"modtools.tickets.issue.chatlog.view": "View chatlog", "modtools.tickets.issue.chatlog.view": "View chatlog",
"modtools.tickets.issue.chatlog.close": "Close chatlog", "modtools.tickets.issue.chatlog.close": "Close chatlog",
"modtools.tickets.issue.resolve.heading": "Resolve as", "modtools.tickets.issue.resolve.heading": "Resolve as",
"modtools.tickets.issue.resolve.resolved": "Resolved", "modtools.tickets.issue.resolve.resolved": "Resolved",
"modtools.tickets.issue.resolve.useless": "Useless", "modtools.tickets.issue.resolve.useless": "Useless",
"modtools.tickets.issue.resolve.abusive": "Abusive", "modtools.tickets.issue.resolve.abusive": "Abusive",
"modtools.tickets.issue.release": "Release back to queue", "modtools.tickets.issue.release": "Release back to queue",
"modtools.tickets.cfh.chatlog.title": "Issue #%issueId% Chatlog", "modtools.tickets.cfh.chatlog.title": "Issue #%issueId% Chatlog",
"groupforum.list.tab.most_active": "Most active threads", "groupforum.list.tab.most_active": "Most active threads",
"groupforum.list.tab.my_forums": "My group forums", "groupforum.list.tab.my_forums": "My group forums",
"groupforum.list.no_forums": "There are no forums", "groupforum.list.no_forums": "There are no forums",
"groupforum.view.threads": "Number of threads", "groupforum.view.threads": "Number of threads",
"groupforum.thread.pin": "Pin thread", "groupforum.thread.pin": "Pin thread",
"groupforum.thread.unpin": "Unpin thread", "groupforum.thread.unpin": "Unpin thread",
"groupforum.thread.lock": "Lock thread", "groupforum.thread.lock": "Lock thread",
"groupforum.thread.unlock": "Unlock thread", "groupforum.thread.unlock": "Unlock thread",
"groupforum.thread.hide": "Hide thread", "groupforum.thread.hide": "Hide thread",
"groupforum.thread.restore": "Restore thread", "groupforum.thread.restore": "Restore thread",
"groupforum.thread.delete": "Delete thread + posts", "groupforum.thread.delete": "Delete thread + posts",
"groupforum.message.hide": "Hide message", "groupforum.message.hide": "Hide message",
"group.forum.enable.caption": "Enable / Disable group forum", "group.forum.enable.caption": "Enable / Disable group forum",
"group.forum.enable.help": "If you disable the group forum, all posts will also be deleted!", "group.forum.enable.help": "If you disable the group forum, all posts will also be deleted!",
"groupforum.view.no_threads": "There are currently no active threads" "groupforum.view.no_threads": "There are currently no active threads",
"loading.task.session": "Verifying session...",
"loading.task.renderer": "Initializing renderer...",
"loading.task.assets": "loading game assets...",
"loading.task.localization": "loading translations...",
"loading.task.avatar": "loading wardrobe...",
"loading.task.sounds": "loading sounds...",
"loading.task.startsession": "Starting session...",
"loading.task.userdata": "loading user data...",
"loading.task.rooms": "loading rooms...",
"loading.task.engine": "loading graphics engine...",
} }
@@ -48,26 +48,9 @@
"timezone.settings": "Europe/Amsterdam", "timezone.settings": "Europe/Amsterdam",
"youtube.publish.disabled": false, "youtube.publish.disabled": false,
"user.badges.group.slot.enabled": true, "user.badges.group.slot.enabled": true,
"_comment_loading_screen": "Schermata di caricamento — sostituibili per traduzione/branding. Logo, sfondo e colore della barra usano lo standard CSS (URL, gradient, colore esadecimale). Le label compaiono sotto la barra di progresso man mano che ogni fase del boot completa.",
"loading.logo.url": "", "loading.logo.url": "",
"loading.background": "", "loading.background": "",
"loading.progress.color": "linear-gradient(90deg,#4f8cff,#2563eb)", "loading.progress.color": "linear-gradient(90deg,#4f8cff,#2563eb)",
"loading.task.boot": "Avvio in corso...",
"loading.task.session": "Verifica sessione",
"loading.task.renderer": "Inizializzazione renderer",
"loading.task.warmup": "Caricamento contenuti...",
"loading.task.assets": "Sto caricando gli asset di gioco",
"loading.task.localization": "Sto caricando le traduzioni",
"loading.task.avatar": "Sto caricando il guardaroba",
"loading.task.sounds": "Sto caricando i suoni",
"loading.task.startsession": "Avvio sessione",
"loading.task.userdata": "Caricamento dati utente",
"loading.task.rooms": "Caricamento stanze",
"loading.task.engine": "Caricamento engine grafico",
"loading.task.connect": "Connessione al server",
"loading.task.ready": "Pronto!",
"login.screen.enabled": true, "login.screen.enabled": true,
"login.endpoint": "${api.url}/api/auth/login", "login.endpoint": "${api.url}/api/auth/login",
"login.register.endpoint": "${api.url}/api/auth/register", "login.register.endpoint": "${api.url}/api/auth/register",
+30 -99
View File
@@ -74,21 +74,30 @@ export const App: FC<{}> = props =>
const [ prepareTrigger, setPrepareTrigger ] = useState(0); const [ prepareTrigger, setPrepareTrigger ] = useState(0);
const [ loadingProgress, setLoadingProgress ] = useState(0); const [ loadingProgress, setLoadingProgress ] = useState(0);
const [ loadingTask, setLoadingTask ] = useState(''); const [ loadingTask, setLoadingTask ] = useState('');
// Look up a loader-stage label from renderer-config so the strings the user
// sees during the boot ("Sto caricando il guardaroba", "Connessione…") can
// be translated by editing the JSON/JSON5 config — fallback keeps the
// Italian baseline shipped with the client.
const taskLabel = useCallback((key: string, fallback: string): string => const taskLabel = useCallback((key: string, fallback: string): string =>
{ {
try try
{ {
const raw = GetConfiguration().getValue<string>(key, ''); const locManager = GetLocalizationManager();
return (typeof raw === 'string' && raw.length) ? raw : fallback; if(locManager && typeof locManager.getValue === 'function')
{
const fromLoc = locManager.getValue(key, false);
if(typeof fromLoc === 'string' && fromLoc.length && fromLoc !== key) return fromLoc;
}
} }
catch catch
{ }
try
{ {
return fallback; const fromConfig = GetConfiguration().getValue<string>(key, '');
if(typeof fromConfig === 'string' && fromConfig.length) return fromConfig;
} }
catch
{ }
return fallback;
}, []); }, []);
const bumpProgress = useCallback((value: number, task?: string) => const bumpProgress = useCallback((value: number, task?: string) =>
{ {
@@ -111,9 +120,6 @@ export const App: FC<{}> = props =>
ClearRememberLogin(); ClearRememberLogin();
try { delete (window as any).NitroConfig?.['sso.ticket']; } catch {} try { delete (window as any).NitroConfig?.['sso.ticket']; } catch {}
try { GetConfiguration().setValue('sso.ticket', ''); } catch {} try { GetConfiguration().setValue('sso.ticket', ''); } catch {}
// Drop `?sso=` from the URL too — otherwise the next reload re-applies
// the same already-consumed ticket via bootstrap.ts and we loop right
// back into "Session expired" without ever showing the login form.
try try
{ {
const url = new URL(window.location.href); const url = new URL(window.location.href);
@@ -142,11 +148,6 @@ export const App: FC<{}> = props =>
const fallbackToLogin = useCallback(() => const fallbackToLogin = useCallback(() =>
{ {
// When login.screen.enabled is false this hotel uses SSO-only auth
// (CMS issues the ticket and redirects here). Surfacing a login form
// on init failure would just dump an empty/broken placeholder, since
// the form's backgrounds and Turnstile aren't even configured. Send
// the user back to the hotel home page instead.
const rawLoginEnabled = GetConfiguration().getValue<unknown>('login.screen.enabled', false); const rawLoginEnabled = GetConfiguration().getValue<unknown>('login.screen.enabled', false);
const loginScreenEnabled = rawLoginEnabled === true || rawLoginEnabled === 'true' || rawLoginEnabled === 1; const loginScreenEnabled = rawLoginEnabled === true || rawLoginEnabled === 'true' || rawLoginEnabled === 1;
@@ -156,14 +157,7 @@ export const App: FC<{}> = props =>
showSessionExpired(); showSessionExpired();
return; return;
} }
// Using console.warn (not NitroLogger.log) on purpose: NitroLogger
// is gated on LOG_DEBUG, which only flips to true once startWarmup's
// GetConfiguration().init() completes. Auth-failure paths fire before
// that, so NitroLogger swallows their messages silently.
console.warn('[App] fallbackToLogin — surfacing login form, credentials cleared'); console.warn('[App] fallbackToLogin — surfacing login form, credentials cleared');
// Wipe whatever credential the server just rejected so the form is
// pristine and the next attempt isn't sabotaged by the same stale data.
clearStoredCredentials(); clearStoredCredentials();
setHomeUrl(''); setHomeUrl('');
setErrorMessage(''); setErrorMessage('');
@@ -200,8 +194,6 @@ export const App: FC<{}> = props =>
if(!remembered?.token?.length) if(!remembered?.token?.length)
{ {
// No remember token means we'd be reusing a one-shot ssoTicket that
// the server already consumed. Force the login screen instead.
if(remembered) ClearRememberLogin(); if(remembered) ClearRememberLogin();
console.warn('[App] tryRememberLogin → no token, returning empty'); console.warn('[App] tryRememberLogin → no token, returning empty');
return ''; return '';
@@ -250,9 +242,6 @@ export const App: FC<{}> = props =>
console.warn('[App] tryRememberLogin → fetch threw', error); console.warn('[App] tryRememberLogin → fetch threw', error);
} }
// Any failure (rejected token, bad payload, network error) — drop the
// stored credentials. Never fall back to the cached ssoTicket: it's
// one-shot and reusing it leads straight to "Session expired".
ClearRememberLogin(); ClearRememberLogin();
console.warn('[App] tryRememberLogin → cleared remember, returning empty'); console.warn('[App] tryRememberLogin → cleared remember, returning empty');
@@ -303,35 +292,12 @@ export const App: FC<{}> = props =>
} }
}, []); }, []);
// Mirror isReady into a ref so the socket handlers below can read the
// freshest value without needing to re-subscribe on every state change.
useEffect(() => { isReadyRef.current = isReady; }, [ isReady ]); useEffect(() => { isReadyRef.current = isReady; }, [ isReady ]);
// Track whether a reconnect cycle is active. The renderer dispatches
// SOCKET_RECONNECTING when it starts retrying after an abnormal close
// (code != 1000/1001). On exhausted retries it fires SOCKET_RECONNECT_FAILED
// *and* a final SOCKET_CLOSED — we keep the flag set through that pair
// so ReconnectView's own overlay owns the UX and we don't double-render.
useNitroEvent(NitroEventType.SOCKET_RECONNECTING, () => { reconnectInProgressRef.current = true; }); useNitroEvent(NitroEventType.SOCKET_RECONNECTING, () => { reconnectInProgressRef.current = true; });
useNitroEvent(NitroEventType.SOCKET_REAUTHENTICATED, () => { reconnectInProgressRef.current = false; }); useNitroEvent(NitroEventType.SOCKET_REAUTHENTICATED, () => { reconnectInProgressRef.current = false; });
useNitroEvent(NitroEventType.SOCKET_CLOSED, () => useNitroEvent(NitroEventType.SOCKET_CLOSED, () =>
{ {
// Three distinct close scenarios converge here:
//
// 1. !isReady — initial handshake just failed (server replied
// with "Bye" / code 1000 to a bad SSO ticket). The user never
// had a session. Surface the login form instead of the
// misleading "Session expired" diagnostic.
//
// 2. isReady && reconnect in progress — ReconnectView already
// owns the UX (its overlay shows attempts and the "Session
// expired" message on RECONNECT_FAILED). Stay out of its way.
//
// 3. isReady && no reconnect — instant server kick mid-game
// (code 1000 from the server side). No reconnect path will
// run. Show the legacy session-expired diagnostic so the
// user knows to reload.
console.warn('[App] SOCKET_CLOSED fired', { console.warn('[App] SOCKET_CLOSED fired', {
isReady: isReadyRef.current, isReady: isReadyRef.current,
reconnectInProgress: reconnectInProgressRef.current reconnectInProgress: reconnectInProgressRef.current
@@ -390,7 +356,7 @@ export const App: FC<{}> = props =>
warmupPromiseRef.current = (async () => warmupPromiseRef.current = (async () =>
{ {
await GetConfiguration().init(); await GetConfiguration().init();
bumpProgress(25, taskLabel('loading.task.warmup', 'Caricamento contenuti...')); bumpProgress(25, taskLabel('loader.waiting', 'Loading content...'));
GetTicker().maxFPS = GetConfiguration().getValue<number>('system.fps.max', 24); GetTicker().maxFPS = GetConfiguration().getValue<number>('system.fps.max', 24);
NitroLogger.LOG_DEBUG = GetConfiguration().getValue<boolean>('system.log.debug', true); NitroLogger.LOG_DEBUG = GetConfiguration().getValue<boolean>('system.log.debug', true);
@@ -427,15 +393,11 @@ export const App: FC<{}> = props =>
loginImageUrls.forEach(preloadImage); loginImageUrls.forEach(preloadImage);
gamedataUrls.forEach(url => preloadUrl(url)); gamedataUrls.forEach(url => preloadUrl(url));
// Wire each warmup task to a progress bump so the bar reflects
// real subsystem-init completion, not a fake timer. Range 25→70.
// Each task carries a friendly label so the user sees what is
// currently being prepared instead of raw file names.
const warmupTasks: { promise: Promise<any>; label: string }[] = [ const warmupTasks: { promise: Promise<any>; label: string }[] = [
{ promise: GetAssetManager().downloadAssets(assetUrls), label: taskLabel('loading.task.assets', 'Sto caricando gli asset di gioco') }, { promise: GetAssetManager().downloadAssets(assetUrls), label: taskLabel('loading.task.assets', 'Loading game assets...') },
{ promise: GetLocalizationManager().init(), label: taskLabel('loading.task.localization', 'Sto caricando le traduzioni') }, { promise: GetLocalizationManager().init(), label: taskLabel('loading.task.localization', 'Loading translations...') },
{ promise: GetAvatarRenderManager().init(), label: taskLabel('loading.task.avatar', 'Sto caricando il guardaroba') }, { promise: GetAvatarRenderManager().init(), label: taskLabel('loading.task.avatar', 'Loading wardrobe...') },
{ promise: GetSoundManager().init(), label: taskLabel('loading.task.sounds', 'Sto caricando i suoni') } { promise: GetSoundManager().init(), label: taskLabel('loading.task.sounds', 'Loading sounds...') }
]; ];
let warmupDone = 0; let warmupDone = 0;
const warmupStart = 25; const warmupStart = 25;
@@ -477,11 +439,6 @@ export const App: FC<{}> = props =>
{ {
const prepare = async (width: number, height: number) => const prepare = async (width: number, height: number) =>
{ {
// Don't dump the actual SSO ticket — it's a one-shot bearer
// credential that grants access to the user's session, so
// logging it in console.warn would leak it via copied logs
// / screen shares / browser extension hooks. Boolean flag is
// enough for the diagnostic.
console.warn('[App] prepare() start', { console.warn('[App] prepare() start', {
hasNitroConfig: !!window.NitroConfig, hasNitroConfig: !!window.NitroConfig,
ssoTicketInConfig: !!window.NitroConfig?.['sso.ticket'], ssoTicketInConfig: !!window.NitroConfig?.['sso.ticket'],
@@ -489,7 +446,7 @@ export const App: FC<{}> = props =>
hasUrlSso: !!new URLSearchParams(window.location.search).get('sso') hasUrlSso: !!new URLSearchParams(window.location.search).get('sso')
}); });
const bootLabel = taskLabel('loading.task.boot', 'Avvio in corso...'); const bootLabel = taskLabel('loader', 'Booting...');
setLoadingProgress(0); setLoadingProgress(0);
setLoadingTask(bootLabel); setLoadingTask(bootLabel);
bumpProgress(5, bootLabel); bumpProgress(5, bootLabel);
@@ -501,11 +458,6 @@ export const App: FC<{}> = props =>
let ssoTicket = window.NitroConfig['sso.ticket']; let ssoTicket = window.NitroConfig['sso.ticket'];
if(ssoTicket) GetConfiguration().setValue('sso.ticket', ssoTicket); if(ssoTicket) GetConfiguration().setValue('sso.ticket', ssoTicket);
// Cattura il remember-token passato via URL (?token=&token_exp=)
// dal CMS Inertia /client e salvalo in localStorage. Serve a
// tryRememberLogin() in reconnect: chiama POST /api/auth/remember
// col token UUID, riceve un nuovo SSO ticket fresco invece di
// riusare quello cleared da Arcturus dopo il primo consume.
try try
{ {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
@@ -525,12 +477,10 @@ export const App: FC<{}> = props =>
console.warn('[App] failed to persist remember token from URL', e); console.warn('[App] failed to persist remember token from URL', e);
} }
bumpProgress(10, taskLabel('loading.task.session', 'Verifica sessione')); bumpProgress(10, taskLabel('loading.task.session', 'Verifying session...'));
if(!ssoTicket || ssoTicket === '') if(!ssoTicket || ssoTicket === '')
{ {
// Configuration is loaded lazily — fetch it up-front so the login
// screen toggle and Turnstile keys are available before we decide.
let configInitError: unknown = null; let configInitError: unknown = null;
try try
{ {
@@ -592,23 +542,23 @@ export const App: FC<{}> = props =>
} }
const renderer = await startRenderer(width, height); const renderer = await startRenderer(width, height);
bumpProgress(20, taskLabel('loading.task.renderer', 'Inizializzazione renderer')); bumpProgress(20, taskLabel('loading.task.renderer', 'Initializing renderer...'));
await startWarmup(width, height); await startWarmup(width, height);
bumpProgress(70, taskLabel('loading.task.startsession', 'Avvio sessione')); bumpProgress(70, taskLabel('loading.task.startsession', 'Starting session...'));
if(!gameInitPromiseRef.current) if(!gameInitPromiseRef.current)
{ {
gameInitPromiseRef.current = (async () => gameInitPromiseRef.current = (async () =>
{ {
await GetSessionDataManager().init(); await GetSessionDataManager().init();
bumpProgress(78, taskLabel('loading.task.userdata', 'Caricamento dati utente')); bumpProgress(78, taskLabel('loading.task.userdata', 'Loading user data...'));
await GetRoomSessionManager().init(); await GetRoomSessionManager().init();
bumpProgress(85, taskLabel('loading.task.rooms', 'Caricamento stanze')); bumpProgress(85, taskLabel('loading.task.rooms', 'Loading rooms...'));
await GetRoomEngine().init(); await GetRoomEngine().init();
bumpProgress(92, taskLabel('loading.task.engine', 'Caricamento engine grafico')); bumpProgress(92, taskLabel('loading.task.engine', 'Loading graphics engine...'));
await GetCommunication().init(); await GetCommunication().init();
bumpProgress(98, taskLabel('loading.task.connect', 'Connessione al server')); bumpProgress(98, taskLabel('generic.reconnecting', 'Connecting to server...'));
})(); })();
} }
@@ -637,7 +587,7 @@ export const App: FC<{}> = props =>
GetTicker().add(ticker => GetTexturePool().run()); GetTicker().add(ticker => GetTexturePool().run());
} }
bumpProgress(100, taskLabel('loading.task.ready', 'Pronto!')); bumpProgress(100, taskLabel('onboarding.button.ready', 'Ready!'));
setIsReady(true); setIsReady(true);
setShowLogin(false); setShowLogin(false);
setIsEnteringHotel(false); setIsEnteringHotel(false);
@@ -645,23 +595,10 @@ export const App: FC<{}> = props =>
catch(err) catch(err)
{ {
NitroLogger.error('[App] Initialization failed — falling back to login', err); NitroLogger.error('[App] Initialization failed — falling back to login', err);
// Anything thrown out of the post-auth chain (renderer init,
// asset download, GetCommunication().init()) is an init/connect
// failure, not session expiration. The credential we used is
// suspect — drop it and present the login form so the user
// can retry instead of getting stuck on a stale "Session expired".
onInitFailure(); onInitFailure();
} }
}; };
// React Strict Mode in dev runs every effect twice (mount → cleanup → mount).
// `prepare()` is full of one-shot side effects (renderer init, websocket
// connect, NitroConfig mutation) — calling it twice with the same trigger
// value causes the second pass to race with the first and clobber state
// (e.g. the second pass falls through to onSessionExpired while the first
// had just set showLogin=true). Guard by trigger value: skip duplicate
// runs at the same trigger, but still re-run when handleAuthenticated
// bumps prepareTrigger after a successful login.
if(lastPrepareTriggerRef.current === prepareTrigger) return; if(lastPrepareTriggerRef.current === prepareTrigger) return;
lastPrepareTriggerRef.current = prepareTrigger; lastPrepareTriggerRef.current = prepareTrigger;
@@ -682,12 +619,6 @@ export const App: FC<{}> = props =>
<LoadingView isError={ errorMessage.length > 0 } message={ errorMessage } homeUrl={ homeUrl } progress={ loadingProgress } currentTask={ loadingTask } /> } <LoadingView isError={ errorMessage.length > 0 } message={ errorMessage } homeUrl={ homeUrl } progress={ loadingProgress } currentTask={ loadingTask } /> }
{ !isReady && showLogin && <LoginView onAuthenticated={ handleAuthenticated } isEntering={ isEnteringHotel } /> } { !isReady && showLogin && <LoginView onAuthenticated={ handleAuthenticated } isEntering={ isEnteringHotel } /> }
{ isReady && <MainView /> } { isReady && <MainView /> }
{ /* Reconnect overlay must NOT render before we've actually entered
the hotel — otherwise the renderer's auto-retry on an initial
handshake failure (e.g. emulator unreachable) would cover the
login form with "Reconnecting..." → "Session expired" and the
user wouldn't be able to interact with the form we just put up
via fallbackToLogin. */ }
{ isReady && <ReconnectView /> } { isReady && <ReconnectView /> }
<Base id="draggable-windows-container" /> <Base id="draggable-windows-container" />
</Base> </Base>