diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md
new file mode 100644
index 0000000..54f5702
--- /dev/null
+++ b/docs/PERFORMANCE.md
@@ -0,0 +1,586 @@
+# Nitro V3 — Cold-load performance
+
+Practical recipe to take a Nitro V3 cold load from the typical
+60-90 s (and intermittent "Session expired") baseline down to ~4 s.
+The wins compound: each section below has measurable impact, in
+roughly the order of cost vs benefit.
+
+Three things matter on the client (this repo): granular code split,
+a real progress bar driven by boot stages, and capturing the
+remember-token from the iframe URL on first boot. The other three —
+gzip on the static server, long cache on gamedata, and a server
+endpoint to mint fresh SSO tickets — are documented further down
+under §5 (nginx) and §6 (IIS) plus a quick note in §4 about the
+CMS contract.
+
+---
+
+## 1. The three Nitro-side changes that matter
+
+1. **Granular code split** (`vite.config.mjs`) — a 1 MB vendor bundle
+ is replaced by ~12 smaller chunks the browser fetches in parallel
+ via HTTP/2 multiplexing.
+2. **Loading screen with a real progress bar** + per-stage labels
+ (`src/components/loading/LoadingView.tsx`, driven by
+ `src/App.tsx::prepare()`) so a slow boot looks like progress, not
+ a frozen GIF.
+3. **Remember-token capture from URL** (`src/App.tsx::prepare()`) so
+ that when the WS drops the existing `tryRememberLogin()` round
+ can hit the CMS `POST /api/auth/remember` endpoint and get a
+ fresh SSO ticket instead of falling through to "Session expired".
+
+The server-side wins (gzip, cache, SSO TTL) live outside this repo —
+without them this client still loads, but you stay at the 60-90 s
+baseline.
+
+---
+
+## 2. Vite `manualChunks` — split the vendor blob
+
+Default `yarn build` ships:
+
+- `vendor` ~1 MB (react + tanstack-query + framer-motion + jodit +
+ emoji-mart + react-icons + howler + zustand + json5 — everything
+ merged)
+- `nitro-renderer` ~2.5 MB (renderer source + pixi.js inlined)
+- `src` ~1.7 MB (app code)
+
+The vendor blob forces the browser to wait on the slowest dependency
+before it can hydrate. Split it by domain — see
+[`vite.config.mjs`](../vite.config.mjs) for the live version, the
+intent is captured below:
+
+```js
+manualChunks: id => {
+ const norm = id.replace(/\\/g, '/');
+
+ // Vendors first — pixi.js / howler / emoji-mart / jodit are aliased
+ // to ../Nitro_Render_V3/node_modules, so they would otherwise be
+ // swallowed by the `Nitro_Render_V3` branch lower down and pulled
+ // into the renderer chunk.
+ if(norm.includes('pixi.js') || norm.includes('pixi-filters')) return 'vendor-pixi';
+ if(norm.includes('howler')) return 'vendor-audio';
+ if(norm.includes('@emoji-mart')) return 'vendor-emoji';
+ if(norm.includes('jodit') || norm.includes('@react-page')) return 'vendor-editor';
+
+ if(id.includes('Nitro_Render_V3') || id.includes(`${ rendererRoot }`)) {
+ if(id.includes('/packages/avatar/')) return 'nitro-renderer-avatar';
+ if(id.includes('/packages/communication/')) return 'nitro-renderer-comm';
+ if(id.includes('/packages/room/')) return 'nitro-renderer-room';
+ if(id.includes('/packages/assets/')) return 'nitro-renderer-assets';
+ return 'nitro-renderer';
+ }
+
+ if(id.includes('node_modules')) {
+ if(id.includes('@nitrots/nitro-renderer') || id.includes('renderer3')) return 'nitro-renderer';
+ if(id.match(/\/react(-dom)?\/|\/scheduler\//) || id.includes('react-error-boundary')) return 'vendor-react';
+ if(id.includes('framer-motion')) return 'vendor-motion';
+ if(id.includes('@tanstack')) return 'vendor-query';
+ if(id.includes('zustand') || id.includes('use-between')) return 'vendor-state';
+ if(id.includes('react-icons')) return 'vendor-icons';
+ if(id.includes('json5')) return 'vendor-json5';
+ return 'vendor';
+ }
+}
+```
+
+Two practical points the comments don't make obvious:
+
+- **Vendor checks come first.** Pixi.js, howler, emoji-mart and jodit
+ are pulled in via an alias to `../Nitro_Render_V3/node_modules`,
+ so their `id` matches `Nitro_Render_V3`. If the renderer branch
+ runs before the vendor one, those modules end up bundled into the
+ renderer chunk instead of their own — defeating the whole point.
+
+- **Pixi often stays inlined.** Rollup keeps a module in the chunk
+ of its sole importer, and `pixi.js` is consumed only through the
+ `@nitrots/nitro-renderer` umbrella. Expect `vendor-pixi` to be
+ near-empty until something *outside* the renderer also imports
+ pixi. This is fine — pixi gets the renderer chunk's cache lifetime
+ anyway.
+
+Verify after `yarn build`:
+
+```
+dist/assets/nitro-renderer-*.js ~2.5 MB raw, ~765 KB gzip
+dist/assets/vendor-*.js ~12 chunks, 4-430 KB each
+dist/assets/src-*.js ~1.7 MB raw, ~550 KB gzip
+```
+
+If you see a single `vendor-*.js` over 800 KB raw, the chunk
+function isn't matching the way you expect — log `id` from inside
+`manualChunks` during build to find out what's actually being
+handed in.
+
+Also add the connection hint to [`index.html`](../index.html):
+
+```html
+
+```
+
+Saves one TLS handshake on cold load — the Turnstile script tag
+already loads from that domain.
+
+---
+
+## 3. LoadingView — real progress, real labels
+
+[`src/components/loading/LoadingView.tsx`](../src/components/loading/LoadingView.tsx)
+renders the dark-blue boot screen the user sees before `isReady`
+flips. It accepts a `progress` number (0-100) and a `currentTask`
+string. The progress bar is hidden when `progress` is `undefined`
+(error / Suspense fallback path) and animates between updates.
+
+The state lives in [`src/App.tsx`](../src/App.tsx):
+
+```ts
+const [ loadingProgress, setLoadingProgress ] = useState(0);
+const [ loadingTask, setLoadingTask ] = useState('');
+
+const taskLabel = useCallback((key: string, fallback: string): string => {
+ // … reads from renderer-config so the strings are translatable
+});
+
+const bumpProgress = useCallback((value: number, task?: string) => {
+ setLoadingProgress(prev => (value > prev ? value : prev));
+ if(task !== undefined) setLoadingTask(task);
+}, []);
+```
+
+`prepare()` bumps the progress through 12 stages as it goes:
+
+| % | Stage | Default label |
+|---|---|---|
+| 5 | App start | `Avvio in corso...` |
+| 10 | NitroConfig validated | `Verifica sessione` |
+| 20 | Renderer constructed | `Inizializzazione renderer` |
+| 25 | Config init done | `Caricamento contenuti...` |
+| 36, 47, 58, 70 | each warmup task resolves | per-task (`Sto caricando il guardaroba`, …) |
+| 78 | `GetSessionDataManager().init()` done | `Caricamento dati utente` |
+| 85 | `GetRoomSessionManager().init()` done | `Caricamento stanze` |
+| 92 | `GetRoomEngine().init()` done | `Caricamento engine grafico` |
+| 98 | `GetCommunication().init()` done | `Connessione al server` |
+| 100 | `setIsReady(true)` about to fire | `Pronto!` |
+
+The labels are config-driven — `taskLabel('loading.task.boot', 'Avvio in corso...')`
+reads `loading.task.*` keys from the renderer-config and falls back
+to the Italian baseline if unset. To localise, add the keys to
+`public/configuration/renderer-config.json` (see the `.example`
+file for the full list).
+
+Logo and background are also configurable via the same mechanism —
+`loading.logo.url`, `loading.background`, `loading.progress.color`.
+Leaving them empty keeps the shipped dark-blue radial + Nitro V3
+logo top-left.
+
+### 3.1 The pre-React shell (asset-loader.js)
+
+There is a second, tiny loading screen that the asset loader writes
+into `#root` *before* React mounts. It used to be a light-blue
+login-skeleton with two grey rectangles — visible for ~200 ms before
+React took over, producing a hated flash. The template lives in
+[`scripts/write-asset-loader.mjs`](../scripts/write-asset-loader.mjs)
+(`renderShell`) and now paints the same `radial-gradient(#1d1a24,#003a6b)`
+as the React `LoadingView`, so the handoff is invisible.
+
+Don't hand-edit `public/configuration/asset-loader.js` — the
+`prebuild` hook regenerates it from the template every `yarn build`.
+
+---
+
+## 4. Remember-token capture — making reconnect work
+
+Arcturus clears `auth_ticket` to `''` the moment it consumes an SSO
+ticket. Without a remember-token the client retries reconnect with
+the same (now empty) ticket and falls through to "Session expired"
+after 2-7 attempts.
+
+The CMS issues a UUID family token when it serves `/client`, and
+passes it on the iframe URL as `&token=&token_exp=`. Nitro
+captures it on first boot:
+
+```ts
+// src/App.tsx::prepare()
+try {
+ const urlParams = new URLSearchParams(window.location.search);
+ const tokenParam = urlParams.get('token');
+ const tokenExpParam = urlParams.get('token_exp');
+ if(tokenParam && !GetRememberLogin()) {
+ const parsedExpiry = Number(tokenExpParam || 0);
+ const expiresAt = (Number.isFinite(parsedExpiry) && parsedExpiry > 0)
+ ? parsedExpiry
+ : Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60);
+ SetRememberLogin({ token: tokenParam, expiresAt });
+ }
+} catch(e) {
+ console.warn('[App] failed to persist remember token from URL', e);
+}
+```
+
+The capture is guarded by `!GetRememberLogin()` — if the user has
+visited before, the stored token wins and the URL one is ignored.
+
+Once stored, the existing `tryRememberLogin()` machinery picks it up
+on every reconnect: it POSTs to
+`${api.url}/api/auth/remember` (configurable via the
+`login.remember.endpoint` renderer-config key), receives a fresh
+SSO ticket back, and rotates the connection. See the CMS doc for the
+server endpoint's contract.
+
+Verify the stored token in browser DevTools:
+
+```
+Application → Local Storage → https://
+ Key: nitro.auth.remember
+ Value: {"token":"","expiresAt":1781912345,"username":""}
+```
+
+If `nitro.auth.remember` is missing after a successful first load,
+the CMS isn't passing `token=` on the iframe URL. Check
+`AuthController.client` on the CMS side.
+
+---
+
+## 5. Server-side: nginx gzip + long cache (the single biggest win)
+
+The Nitro client ships ~4.3 MB raw across the main bundle, renderer
+chunk and vendor splits. If the server doesn't compress and doesn't
+let the browser cache, every visitor pays the full price on every
+load — that's exactly the 60-90 s baseline you avoid by configuring
+nginx properly.
+
+### 5.1 Enable gzip globally
+
+Default nginx ships with the `gzip` block commented out. Edit
+`/etc/nginx/nginx.conf` and replace the `#gzip on;` line inside the
+`http {}` block:
+
+```nginx
+gzip on;
+gzip_vary on;
+gzip_proxied any;
+gzip_comp_level 6;
+gzip_min_length 1024;
+gzip_types
+ text/plain
+ text/css
+ text/xml
+ text/javascript
+ application/javascript
+ application/x-javascript
+ application/json
+ application/xml
+ application/rss+xml
+ application/atom+xml
+ image/svg+xml
+ font/ttf
+ font/otf
+ application/font-woff
+ application/vnd.ms-fontobject;
+```
+
+Back up the file before editing, then reload:
+
+```bash
+cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak-$(date +%Y%m%d-%H%M%S)
+nginx -t # validate syntax first
+systemctl reload nginx
+```
+
+The impact is *enormous* — `palettes.json5` drops from 330 KB to 18 KB
+on the wire (~17×), and the renderer JS bundle from 2.5 MB to 765 KB
+(~3.3×). Verify:
+
+```bash
+curl -sI -H 'Accept-Encoding: gzip' \
+ 'https:///nitro/assets/nitro-renderer-XXXXX.js' \
+ | grep -i 'content-encoding'
+# expected: content-encoding: gzip
+```
+
+If you forget `application/json` from `gzip_types` you lose the
+gamedata compression — that's the one that matters the most because
+the gamedata files are by far the heaviest payload.
+
+### 5.2 Long Cache-Control on gamedata
+
+Inside the `/nitro-assets/` or `/nitro-assets/` location
+block, the gamedata `.json5` files deserve a 30-day cache because
+they only change on deploy:
+
+```nginx
+location /nitro-assets/ {
+ alias /var/www/cmsjs/public/nitro-assets/;
+ try_files $uri ${uri}manifest.json5 ${uri}manifest.json =404;
+ autoindex off;
+ default_type application/json;
+ expires 7d;
+ add_header Cache-Control "public, max-age=604800, immutable";
+
+ location ~ \.json5?$ {
+ types {} default_type application/json;
+ expires 30d;
+ add_header Cache-Control "public, max-age=2592000";
+ }
+}
+```
+
+The outer 7-day cache covers PNG / nitro / mp3 files. The inner
+location block raises the JSON5 lifetime to 30 days because the
+content is effectively immutable per deploy. Cloudflare honours
+`Last-Modified` so revalidation still works — you don't need to
+cache-bust by filename.
+
+For the JS / CSS chunks the filenames are content-hashed by Vite, so
+a long cache is safe — apply the same `Cache-Control: max-age=2592000`
+to the `/nitro/assets/` location.
+
+### 5.3 The `try_files → manifest.json5` fallback
+
+`loadGamedata(url)` in the renderer SDK can be pointed at either a
+single JSON file or a directory containing `manifest.json5` + tier
+sub-directories. The directory pattern is what we use in production,
+so requests like `/nitro-assets/gamedata/figuremap/` (note the
+trailing slash) need to resolve to the directory's manifest.
+
+The `try_files $uri ${uri}manifest.json5 ${uri}manifest.json =404;`
+above does exactly that — try the URI as-is, fall back to the
+`manifest.json5` inside the directory, fall back to `.json` for
+legacy deploys, then 404. Without it nginx returns 403 (autoindex
+off) on directory URLs and the loader cascades into the manifest 404
+path.
+
+---
+
+## 6. Server-side: Windows + IIS deployment
+
+You can reach the same 4 s cold load on Windows Server with IIS. The
+same three wins (gzip, long cache, JSON5 fallback) are replicable —
+syntax changes, performance ceiling doesn't.
+
+### 6.1 Don't host Node inside IIS
+
+`IISNode` is unmaintained. The current MS recommendation is to run
+Node as a Windows service and let IIS reverse-proxy to it:
+
+1. Install Node 22 LTS, run the CMS app as a Windows Service (via
+ `nssm`, `pm2-windows-startup`, or a scheduled task on boot) bound
+ to `127.0.0.1:3003` — same layout as `docker-compose.yml` on the
+ Linux host.
+2. Install Arcturus separately as a Windows service running
+ `Habbo-x.y.z-jar-with-dependencies.jar` against MariaDB. WS ports
+ 30001 + 30002 stay on `127.0.0.1`.
+3. IIS handles HTTPS termination, static file serving, compression
+ and reverse-proxying `/api/*` + `/client` + the Inertia entry
+ point to `127.0.0.1:3003`.
+
+Install these IIS features (Server Manager → Web Server → Add Roles
+& Features):
+
+- **URL Rewrite** — proxy rules
+- **Application Request Routing (ARR)** — lets IIS act as a forward
+ proxy; *enable proxy in the ARR feature page* after install
+- **WebSocket Protocol** — required for the Arcturus WS upgrade
+- **Static Content** + **Static Content Compression**
+- **Dynamic Content Compression** — **off by default**, this is the
+ single most important toggle on a vanilla Windows Server
+
+### 6.2 Enable compression site-wide
+
+IIS Manager → site → **Compression** feature → tick *both*
+"Enable dynamic content compression" and "Enable static content
+compression". Equivalent of nginx's `gzip on;`.
+
+Without ticking both you ship raw bytes. Static covers JS / CSS /
+JSON files, Dynamic covers Node responses (HTML from the Inertia
+render). Add `application/json` to the compressor (and `.json5` to
+its MIME map) in `applicationHost.config` or the site's `web.config`:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Verify with PowerShell:
+
+```powershell
+Invoke-WebRequest -Uri 'https:///nitro-assets/gamedata/figuredata/core/palettes.json5' `
+ -Headers @{ 'Accept-Encoding' = 'gzip' } `
+ -MaximumRedirection 0 | Select-Object -ExpandProperty Headers
+# expected: Content-Encoding = gzip
+```
+
+### 6.3 Long cache for gamedata
+
+Drop a `web.config` inside the `nitro-assets/` virtual
+directory (or nest under ``):
+
+```xml
+
+
+
+
+
+
+
+
+
+```
+
+`30.00:00:00` is the IIS TimeSpan for 30 days — same effect as
+`Cache-Control: public, max-age=2592000` on nginx.
+
+Set a separate, shorter cache (e.g. 5 minutes) on `index.html` so
+deploys propagate without forcing visitors to clear their cache.
+
+### 6.4 Directory → manifest.json5 fallback
+
+nginx's `try_files $uri ${uri}manifest.json5 ${uri}manifest.json =404;`
+has no native IIS equivalent. Use **URL Rewrite** to chain two rules
+inside the same ``:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### 6.5 Reverse proxy to Node + WebSocket upgrade
+
+Once ARR is installed and proxy enabled (IIS Manager → server node →
+ARR → Server Proxy Settings → check "Enable proxy"), add a top-level
+rule that forwards everything *not* matching a static file:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+ARR transparently handles the WebSocket upgrade once the WebSocket
+Protocol IIS feature is installed.
+
+### 6.6 IIS trade-offs (honest)
+
+- **Compression CPU**: IIS dynamic compression is more CPU-hungry than
+ nginx's worker-pool gzip. On 2-vCPU droplets expect ~10-15 % extra
+ CPU during peak concurrency.
+- **Docker overhead**: Docker Desktop on Windows goes through the
+ WSL2 file-system bridge. Bind-mounting Linux-style paths into a
+ container is measurably slower than the same on a native Linux
+ host. Recommendation: run Node + Arcturus as native Windows
+ services, *not* containerised.
+- **Java JDBC on Windows**: Arcturus's JDBC pool exhibits slightly
+ higher lock-wait under concurrent room load on Windows than on
+ Linux. Re-tune `db.pool.maxsize` if you saturate.
+
+Browser-perceived performance is identical to nginx once the config
+above is in place. The 4 s cold-load target is achievable on any
+Windows Server 2019 / 2022 box.
+
+The one deployment to **avoid**: shared Windows hosting where the
+hoster doesn't let you enable Dynamic Compression at the application
+host level. You stay stuck at the 60-90 s baseline because neither
+Node's gzip nor IIS's compressor can be turned on.
+
+---
+
+## 7. End-to-end verification
+
+Run each probe in order — they walk the request through every layer
+covered above. A green light on all four means the cold load is
+correctly tuned.
+
+```bash
+# 1. Build artefact has the granular chunks
+yarn build
+ls dist/assets/ | grep -E '^(vendor|nitro-renderer)-' | wc -l
+# expected: ~12-14 chunks
+
+# 2. Server is compressing JSON5 (or JS — pick either)
+curl -sI -H 'Accept-Encoding: gzip' \
+ 'https:///nitro-assets/gamedata/figuredata/core/palettes.json5' \
+ | grep -iE 'content-encoding|cache-control'
+# expected:
+# content-encoding: gzip
+# cache-control: public, max-age=2592000
+
+# 3. Directory → manifest.json5 fallback
+curl -sI 'https:///nitro-assets/gamedata/figuremap/' \
+ | head -1
+# expected: HTTP/2 200 (not 403 or 404)
+
+# 4. LoadingView renders the progress bar — easiest from the live site:
+# DevTools → Performance → Record → reload /client
+# Look for the progress bar transitioning 5→100% within 4s on a
+# warm-cache load, ~10-20s on a cold one with empty CF cache.
+
+# 5. Remember-token captured to localStorage:
+# DevTools → Application → Local Storage → check nitro.auth.remember
+# is populated after the first successful load.
+```
+
+If the build artefact is correct but the live site doesn't pick up
+the new chunks, the deploy didn't replace `dist/` on the server.
+Wipe the target dir's `assets/*.js` and `src/assets/*.css` before
+extracting the new tarball — old chunk filenames stick around
+otherwise.
diff --git a/index.html b/index.html
index 48e1e0a..b162833 100644
--- a/index.html
+++ b/index.html
@@ -4,6 +4,9 @@
Nitro
+
+
diff --git a/public/configuration/UITexts.example b/public/configuration/UITexts.example
index acf246e..2b87ccb 100644
--- a/public/configuration/UITexts.example
+++ b/public/configuration/UITexts.example
@@ -1,116 +1,268 @@
{
- "notification.badge.received": "Nuovo Distintivo!",
- "wiredfurni.badgereceived.title": "Distintivo ricevuto!",
- "wiredfurni.badgereceived.body": "Hai appena ricevuto un nuovo Distintivo! Controlla nel tuo Inventario!",
- "friendlist.search": "Search friends",
- "purse.seasonal.currency.101": "cash",
- "widget.chooser.checkall": "Select furniture",
- "widget.chooser.btn.pickall": "pick up selected items!",
- "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",
- "widget.settings.general": "General",
- "widget.settings.general.title": "Adjust the default Nitro settings",
- "widget.settings.volume": "Volume",
- "widget.settings.interface": "Interface",
- "widget.settings.interface.title": "Adjust the interface settings",
- "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.secondary": "Change the window header color",
- "widget.settings.interface.reset": "Reset header color to default",
- "widget.room.chat.hide_pets": "Hide pets",
- "widget.room.chat.hide_avatars": "Hide avatars",
- "widget.room.chat.hide_balloon": "Hide speech bubble",
- "widget.room.chat.show_balloon": "Speech bubble",
- "widget.room.chat.clear_history": "clear history",
- "widget.room.youtube.shared": "YouTube is being shared",
- "widget.room.youtube.open_video": "Open the video",
- "wiredfurni.tooltip.select.tile": "Select tile",
- "wiredfurni.tooltip.remove.tile": "Deselect tile",
- "wiredfurni.tooltip.remove.5x5_tile": "select 5x5 tiles",
- "wiredfurni.tooltip.remove.clear_tile": "Clear all selections",
- "wiredfurni.params.furni_neighborhood.group.user": "Players",
- "wiredfurni.params.furni_neighborhood.group.furni": "Furniture",
- "wiredfurni.params.selector_option.bot": "No bots",
- "wiredfurni.params.selector_option.pet": "No pets",
- "catalog.title": "Catalog",
- "catalog.favorites": "Favorites",
- "catalog.favorites.pages": "Pages",
- "catalog.favorites.furni": "Furni",
- "catalog.favorites.empty": "No favorites",
- "catalog.favorites.empty.hint": "Click the heart on furni or the star on pages to add them.",
- "catalog.admin": "Admin",
- "catalog.admin.new": "New",
- "catalog.admin.root": "Root",
- "catalog.admin.new.root.category": "New root category",
- "catalog.admin.edit.root": "Edit Root",
- "catalog.admin.edit": "Edit:",
- "catalog.admin.edit.page": "Edit Page",
- "catalog.admin.hidden": "hidden",
- "catalog.admin.edit.title": "Edit \"%name%\"",
- "catalog.admin.show": "Show",
- "catalog.admin.hide": "Hide",
- "catalog.admin.delete": "Delete",
- "catalog.admin.delete.title": "Delete \"%name%\"",
- "catalog.admin.delete.category.confirm": "Delete category \"%name%\" and all its content?",
- "catalog.admin.delete.page": "Delete page",
- "catalog.admin.delete.page.confirm": "Delete page \"%name%\"?",
- "catalog.admin.delete.offer.confirm": "Are you sure you want to delete this offer?",
- "catalog.admin.create": "Create",
- "catalog.admin.save": "Save",
- "catalog.admin.create.subpage": "Create sub-page",
- "catalog.admin.order": "Order",
- "catalog.admin.visible": "Visible",
- "catalog.admin.enabled": "Enabled",
- "catalog.admin.offer.new": "New Offer",
- "catalog.admin.offer.edit": "Edit Offer",
- "catalog.admin.offer.name": "Catalog Name",
- "catalog.admin.offer.general": "General",
- "catalog.admin.offer.quantity": "Quantity",
- "catalog.admin.offer.prices": "Prices",
- "catalog.admin.offer.credits": "Credits",
- "catalog.admin.offer.points": "Points",
- "catalog.admin.offer.points.type": "Points Type",
- "catalog.admin.offer.options": "Options",
- "catalog.admin.offer.club.only": "Club Only",
- "catalog.admin.offer.extradata": "Extra Data (optional)....",
- "catalog.admin.offer.have.offer": "Multi-discount (have_offer)",
- "catalog.trophies.title": "Trophies",
- "catalog.trophies.write.hint": "Write a text for the trophy before purchasing",
- "catalog.trophies.inscription": "Trophy Inscription",
- "catalog.trophies.inscription.placeholder": "Write the text that will appear on the trophy...",
- "catalog.pets.show.colors": "Show colors",
- "catalog.pets.choose.color": "Choose color",
- "catalog.pets.choose.breed": "Choose breed",
- "catalog.pets.back.breeds": "? Breeds",
- "catalog.prefix.text": "Text",
- "catalog.prefix.text.placeholder": "Enter text...",
- "catalog.prefix.icon": "Icon",
- "catalog.prefix.icon.remove": "Remove icon",
- "catalog.prefix.effect": "Effect",
- "catalog.prefix.color": "Color",
- "catalog.prefix.color.single": "?? Single",
- "catalog.prefix.color.per.letter": "?? Per Letter",
- "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": "Apply to all",
- "catalog.prefix.color.selected": "Selected letter:",
- "catalog.prefix.price": "Price:",
- "catalog.prefix.price.amount": "5 Credits",
- "catalog.prefix.purchased": "? Purchased!",
- "catalog.prefix.purchase": "Purchase",
- "groupforum.list.tab.most_active": "Most active threads",
- "groupforum.list.tab.my_forums": "My group forums",
- "groupforum.list.no_forums": "There are no forums",
- "groupforum.view.threads": "Number of threads",
- "groupforum.thread.pin": "Pin thread",
- "groupforum.thread.unpin": "Unpin thread",
- "groupforum.thread.lock": "Lock thread",
- "groupforum.thread.unlock": "Unlock thread",
- "groupforum.thread.hide": "Hide thread",
- "groupforum.thread.restore": "Restore thread",
- "groupforum.thread.delete": "Delete thread + posts",
- "groupforum.message.hide": "Hide message",
- "group.forum.enable.caption": "Enable / Disable group forum",
- "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"
+ "notification.badge.received": "Nuovo Distintivo!",
+ "wiredfurni.badgereceived.title": "Distintivo ricevuto!",
+ "wiredfurni.badgereceived.body": "Hai appena ricevuto un nuovo Distintivo! Controlla nel tuo Inventario!",
+ "friendlist.search": "Search friends",
+ "purse.seasonal.currency.101": "cash",
+ "widget.chooser.checkall": "Select furniture",
+ "widget.chooser.btn.pickall": "pick up selected items!",
+ "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",
+ "widget.settings.general": "General",
+ "widget.settings.general.title": "Adjust the default Nitro settings",
+ "widget.settings.volume": "Volume",
+ "widget.settings.interface": "Interface",
+ "widget.settings.interface.title": "Adjust the interface settings",
+ "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.secondary": "Change the window header color",
+ "widget.settings.interface.reset": "Reset header color to default",
+ "widget.room.chat.hide_pets": "Hide pets",
+ "widget.room.chat.hide_avatars": "Hide avatars",
+ "widget.room.chat.hide_balloon": "Hide speech bubble",
+ "widget.room.chat.show_balloon": "Speech bubble",
+ "widget.room.chat.clear_history": "clear history",
+ "widget.room.youtube.shared": "YouTube is being shared",
+ "widget.room.youtube.open_video": "Open the video",
+ "wiredfurni.tooltip.select.tile": "Select tile",
+ "wiredfurni.tooltip.remove.tile": "Deselect tile",
+ "wiredfurni.tooltip.remove.5x5_tile": "select 5x5 tiles",
+ "wiredfurni.tooltip.remove.clear_tile": "Clear all selections",
+ "wiredfurni.params.furni_neighborhood.group.user": "Players",
+ "wiredfurni.params.furni_neighborhood.group.furni": "Furniture",
+ "wiredfurni.params.selector_option.bot": "No bots",
+ "wiredfurni.params.selector_option.pet": "No pets",
+ "catalog.title": "Catalog",
+ "catalog.favorites": "Favorites",
+ "catalog.favorites.pages": "Pages",
+ "catalog.favorites.furni": "Furni",
+ "catalog.favorites.empty": "No favorites",
+ "catalog.favorites.empty.hint": "Click the heart on furni or the star on pages to add them.",
+ "catalog.admin": "Admin",
+ "catalog.admin.new": "New",
+ "catalog.admin.root": "Root",
+ "catalog.admin.new.root.category": "New root category",
+ "catalog.admin.edit.root": "Edit Root",
+ "catalog.admin.edit": "Edit:",
+ "catalog.admin.edit.page": "Edit Page",
+ "catalog.admin.hidden": "hidden",
+ "catalog.admin.edit.title": "Edit \"%name%\"",
+ "catalog.admin.show": "Show",
+ "catalog.admin.hide": "Hide",
+ "catalog.admin.delete": "Delete",
+ "catalog.admin.delete.title": "Delete \"%name%\"",
+ "catalog.admin.delete.category.confirm": "Delete category \"%name%\" and all its content?",
+ "catalog.admin.delete.page": "Delete page",
+ "catalog.admin.delete.page.confirm": "Delete page \"%name%\"?",
+ "catalog.admin.delete.offer.confirm": "Are you sure you want to delete this offer?",
+ "catalog.admin.create": "Create",
+ "catalog.admin.save": "Save",
+ "catalog.admin.create.subpage": "Create sub-page",
+ "catalog.admin.order": "Order",
+ "catalog.admin.visible": "Visible",
+ "catalog.admin.enabled": "Enabled",
+ "catalog.admin.offer.new": "New Offer",
+ "catalog.admin.offer.edit": "Edit Offer",
+ "catalog.admin.offer.name": "Catalog Name",
+ "catalog.admin.offer.general": "General",
+ "catalog.admin.offer.quantity": "Quantity",
+ "catalog.admin.offer.prices": "Prices",
+ "catalog.admin.offer.credits": "Credits",
+ "catalog.admin.offer.points": "Points",
+ "catalog.admin.offer.points.type": "Points Type",
+ "catalog.admin.offer.options": "Options",
+ "catalog.admin.offer.club.only": "Club Only",
+ "catalog.admin.offer.extradata": "Extra Data (optional)....",
+ "catalog.admin.offer.have.offer": "Multi-discount (have_offer)",
+ "catalog.trophies.title": "Trophies",
+ "catalog.trophies.write.hint": "Write a text for the trophy before purchasing",
+ "catalog.trophies.inscription": "Trophy Inscription",
+ "catalog.trophies.inscription.placeholder": "Write the text that will appear on the trophy...",
+ "catalog.pets.show.colors": "Show colors",
+ "catalog.pets.choose.color": "Choose color",
+ "catalog.pets.choose.breed": "Choose breed",
+ "catalog.pets.back.breeds": "? Breeds",
+ "catalog.prefix.text": "Text",
+ "catalog.prefix.text.placeholder": "Enter text...",
+ "catalog.prefix.icon": "Icon",
+ "catalog.prefix.icon.remove": "Remove icon",
+ "catalog.prefix.effect": "Effect",
+ "catalog.prefix.color": "Color",
+ "catalog.prefix.color.single": "?? Single",
+ "catalog.prefix.color.per.letter": "?? Per Letter",
+ "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": "Apply to all",
+ "catalog.prefix.color.selected": "Selected letter:",
+ "catalog.prefix.price": "Price:",
+ "catalog.prefix.price.amount": "5 Credits",
+ "catalog.prefix.purchased": "? Purchased!",
+ "catalog.prefix.purchase": "Purchase",
+ "modtools.userinfo.title": "User Info: %username%",
+ "modtools.userinfo.userName": "Name",
+ "modtools.userinfo.cfhCount": "CFHs",
+ "modtools.userinfo.abusiveCfhCount": "Abusive CFHs",
+ "modtools.userinfo.cautionCount": "Cautions",
+ "modtools.userinfo.banCount": "Bans",
+ "modtools.userinfo.lastSanctionTime": "Last Sanction",
+ "modtools.userinfo.tradingLockCount": "Trade Locks",
+ "modtools.userinfo.tradingExpiryDate": "Lock Expires",
+ "modtools.userinfo.minutesSinceLastLogin": "Last Login",
+ "modtools.userinfo.lastPurchaseDate": "Last Purchase",
+ "modtools.userinfo.primaryEmailAddress": "Email",
+ "modtools.userinfo.identityRelatedBanCount": "Banned Accs",
+ "modtools.userinfo.registrationAgeInMinutes": "Registered",
+ "modtools.userinfo.userClassification": "Rank",
+ "modtools.window.title": "Mod Tools",
+ "modtools.window.tools.room": "Room Tool",
+ "modtools.window.tools.chatlog": "Chatlog Tool",
+ "modtools.window.tools.report": "Report Tool",
+ "modtools.window.select.user": "Select a user",
+ "modtools.window.no.room": "Enter a room first",
+ "modtools.window.user.in_room": "Still in this room",
+ "modtools.window.user.left_room": "No longer in this room",
+ "modtools.window.user.clear": "Clear selection",
+ "modtools.window.tickets.open": "%count% open ticket",
+ "modtools.window.tickets.open.many": "%count% open tickets",
+ "modtools.window.section.room": "Room",
+ "modtools.window.section.user": "User",
+ "modtools.window.section.reports": "Reports",
+ "modtools.window.user.open_info": "Open Info",
+ "modtools.userinfo.refresh": "Refresh user info",
+ "modtools.userinfo.presence.in_room": "In room",
+ "modtools.userinfo.presence.in_room.title": "In the room you are observing",
+ "modtools.userinfo.presence.online": "Online",
+ "modtools.userinfo.presence.online.title": "Online on the hotel",
+ "modtools.userinfo.presence.offline": "Offline",
+ "modtools.userinfo.presence.offline.title": "Offline at panel open",
+ "modtools.userinfo.section.account": "Account",
+ "modtools.userinfo.section.activity": "Activity",
+ "modtools.userinfo.section.sanctions": "Sanctions",
+ "modtools.userinfo.section.trading": "Trading",
+ "modtools.userinfo.button.room.chat": "Room Chat",
+ "modtools.userinfo.button.send.message": "Send Message",
+ "modtools.userinfo.button.room.visits": "Room Visits",
+ "modtools.userinfo.button.mod.action": "Mod Action",
+ "modtools.userinfo.stat.cfh": "CFH",
+ "modtools.userinfo.stat.cautions": "Cautions",
+ "modtools.userinfo.stat.bans": "Bans",
+ "modtools.userinfo.stat.trade.locks": "Trade locks",
+ "modtools.roominfo.title": "Room Info",
+ "modtools.roominfo.refresh": "Refresh room info",
+ "modtools.roominfo.loading": "Loading…",
+ "modtools.roominfo.owner.here": "Owner here",
+ "modtools.roominfo.owner.away": "Owner away",
+ "modtools.roominfo.owner.title.here": "The room owner is currently inside",
+ "modtools.roominfo.owner.title.away": "The room owner is NOT inside",
+ "modtools.roominfo.stat.users": "Users",
+ "modtools.roominfo.stat.owner": "Owner",
+ "modtools.roominfo.owner.open": "Open %username%'s info",
+ "modtools.roominfo.button.visit": "Visit Room",
+ "modtools.roominfo.button.chatlog": "Chatlog",
+ "modtools.roominfo.moderate.title": "Moderate room",
+ "modtools.roominfo.moderate.kick": "Kick everyone out",
+ "modtools.roominfo.moderate.doorbell": "Enable the doorbell",
+ "modtools.roominfo.moderate.rename": "Change room name",
+ "modtools.roominfo.moderate.message.placeholder": "Mandatory message to deliver with the action…",
+ "modtools.roominfo.moderate.send.caution": "Send Caution",
+ "modtools.roominfo.moderate.send.alert": "Send Alert",
+ "modtools.user.message.title": "Send Message",
+ "modtools.user.message.recipient": "Message to",
+ "modtools.user.message.label": "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.chars": "%count% chars",
+ "modtools.user.message.send": "Send Message",
+ "modtools.user.modaction.title": "Mod Action: %username%",
+ "modtools.user.modaction.sanctioning": "Sanctioning",
+ "modtools.user.modaction.step.topic": "1. CFH Topic",
+ "modtools.user.modaction.step.topic.placeholder": "Select a topic…",
+ "modtools.user.modaction.step.sanction": "2. Sanction",
+ "modtools.user.modaction.step.sanction.placeholder": "Select a sanction…",
+ "modtools.user.modaction.step.message": "3. Custom message",
+ "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.preview": "Preview",
+ "modtools.user.modaction.button.default": "Default 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.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.message": "Please write a message to user",
+ "modtools.user.modaction.error.no.permission.alert": "You have insufficient permissions",
+ "modtools.user.visits.title": "User Visits",
+ "modtools.user.visits.recent": "Recent visited rooms",
+ "modtools.user.visits.entries.one": "%count% entry",
+ "modtools.user.visits.entries.many": "%count% entries",
+ "modtools.user.visits.empty": "No recent visits",
+ "modtools.user.visits.time": "Time",
+ "modtools.user.visits.room": "Room name",
+ "modtools.user.visits.action": "Action",
+ "modtools.user.visits.visit": "Visit",
+ "modtools.user.visits.visit.title": "Visit room",
+ "modtools.user.chatlog.title": "User Chatlog",
+ "modtools.user.chatlog.title.with": "User Chatlog: %username%",
+ "modtools.user.chatlog.loading": "Loading chatlog…",
+ "modtools.room.chatlog.title": "Room Chatlog",
+ "modtools.chatlog.column.time": "Time",
+ "modtools.chatlog.column.user": "User",
+ "modtools.chatlog.column.message": "Message",
+ "modtools.chatlog.empty": "No messages",
+ "modtools.chatlog.visit": "Visit",
+ "modtools.chatlog.tools": "Tools",
+ "modtools.tickets.title": "Tickets",
+ "modtools.tickets.tab.open": "Open",
+ "modtools.tickets.tab.mine": "Mine",
+ "modtools.tickets.tab.picked": "All picked",
+ "modtools.tickets.column.type": "Type",
+ "modtools.tickets.column.reported": "Reported",
+ "modtools.tickets.column.opened": "Opened",
+ "modtools.tickets.column.picker": "Picker",
+ "modtools.tickets.empty.open": "No open issues",
+ "modtools.tickets.empty.mine": "No issues picked by you",
+ "modtools.tickets.empty.picked": "No picked issues",
+ "modtools.tickets.action.pick": "Pick",
+ "modtools.tickets.action.handle": "Handle",
+ "modtools.tickets.action.release": "Release",
+ "modtools.tickets.issue.title": "Resolving issue #%issueId%",
+ "modtools.tickets.issue.label": "Issue #%issueId%",
+ "modtools.tickets.issue.details": "Details",
+ "modtools.tickets.issue.field.source": "Source",
+ "modtools.tickets.issue.field.category": "Category",
+ "modtools.tickets.issue.field.description": "Description",
+ "modtools.tickets.issue.field.caller": "Caller",
+ "modtools.tickets.issue.field.reported": "Reported",
+ "modtools.tickets.issue.chatlog.view": "View chatlog",
+ "modtools.tickets.issue.chatlog.close": "Close chatlog",
+ "modtools.tickets.issue.resolve.heading": "Resolve as",
+ "modtools.tickets.issue.resolve.resolved": "Resolved",
+ "modtools.tickets.issue.resolve.useless": "Useless",
+ "modtools.tickets.issue.resolve.abusive": "Abusive",
+ "modtools.tickets.issue.release": "Release back to queue",
+ "modtools.tickets.cfh.chatlog.title": "Issue #%issueId% Chatlog",
+ "groupforum.list.tab.most_active": "Most active threads",
+ "groupforum.list.tab.my_forums": "My group forums",
+ "groupforum.list.no_forums": "There are no forums",
+ "groupforum.view.threads": "Number of threads",
+ "groupforum.thread.pin": "Pin thread",
+ "groupforum.thread.unpin": "Unpin thread",
+ "groupforum.thread.lock": "Lock thread",
+ "groupforum.thread.unlock": "Unlock thread",
+ "groupforum.thread.hide": "Hide thread",
+ "groupforum.thread.restore": "Restore thread",
+ "groupforum.thread.delete": "Delete thread + posts",
+ "groupforum.message.hide": "Hide message",
+ "group.forum.enable.caption": "Enable / Disable group forum",
+ "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",
+ "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...",
}
diff --git a/public/configuration/asset-loader.js b/public/configuration/asset-loader.js
index 7483733..8a4a916 100644
--- a/public/configuration/asset-loader.js
+++ b/public/configuration/asset-loader.js
@@ -57,7 +57,10 @@
const renderShell = () => {
const root = document.getElementById("root");
if(!root || root.firstChild) return;
- root.innerHTML = '