Merge remote-tracking branch 'origin/Dev' into feat/messenger-groups-receipts
# Conflicts: # public/configuration/UITexts.example # src/css/friends/FriendsView.css
@@ -4,16 +4,17 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- Dev
|
||||
- 'feat/**'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
renderer_repo:
|
||||
description: 'Renderer repo (owner/name). Empty = auto from client branch.'
|
||||
description: 'Renderer repo (owner/name). Empty = vars.RENDERER_REPO or upstream default.'
|
||||
required: false
|
||||
default: ''
|
||||
renderer_ref:
|
||||
description: 'Renderer git ref. Empty = auto from client branch.'
|
||||
description: 'Renderer git ref. Empty = vars.RENDERER_REF or auto (main on client main, else Dev).'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
@@ -24,6 +25,11 @@ on:
|
||||
# it on every run.
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
||||
# Upstream renderer used as the fallback when nothing else is
|
||||
# configured. Override per-fork via the RENDERER_REPO / RENDERER_REF
|
||||
# repository variables (Settings → Secrets and variables → Actions →
|
||||
# Variables) or, for one-off runs, via the workflow_dispatch inputs.
|
||||
UPSTREAM_RENDERER_REPO: 'duckietm/Nitro_Render_V3'
|
||||
|
||||
jobs:
|
||||
check:
|
||||
@@ -39,33 +45,32 @@ jobs:
|
||||
with:
|
||||
path: Nitro-V3
|
||||
|
||||
# Pick the renderer ref dynamically based on the client context.
|
||||
# Resolve the renderer pairing with a clear precedence, from most
|
||||
# specific to most generic — no fork names or feature branches are
|
||||
# hardcoded in this workflow:
|
||||
#
|
||||
# 1. workflow_dispatch inputs (renderer_repo / renderer_ref)
|
||||
# → explicit manual override, wins outright.
|
||||
# 2. repository variables (vars.RENDERER_REPO / vars.RENDERER_REF)
|
||||
# → per-fork config set under Settings → Variables, applies
|
||||
# to push and pull_request runs without editing this file.
|
||||
# 3. upstream default
|
||||
# → UPSTREAM_RENDERER_REPO, ref `main` when the client build
|
||||
# context is `main`, otherwise `Dev`.
|
||||
#
|
||||
# The two repos must stay wire-aligned (composer/parser
|
||||
# signatures); pairing `main` with a stale branch is what
|
||||
# signatures); pairing the client with a stale renderer is what
|
||||
# produced the "Expected 14-15 arguments, but got 16" failure on
|
||||
# the catalog edit composer.
|
||||
#
|
||||
# This branch (`feat/housekeeping-panel`) references HK composers
|
||||
# /events that live on the renderer PR branch
|
||||
# (simoleo89/Nitro_Render_V3 @ feat/housekeeping-packets) — they
|
||||
# haven't been merged upstream yet. Pair against the fork branch
|
||||
# for this PR so the typecheck step can resolve the imports;
|
||||
# once the renderer PR lands on duckietm:Dev this whole
|
||||
# special-case block can be dropped.
|
||||
#
|
||||
# Mapping:
|
||||
# client `main` → duckietm/Nitro_Render_V3 @ main
|
||||
# client `feat/housekeeping-panel` → simoleo89/Nitro_Render_V3 @ feat/housekeeping-packets
|
||||
# client `feat/**` (other) → duckietm/Nitro_Render_V3 @ Dev
|
||||
# PR base `main` → duckietm/Nitro_Render_V3 @ main
|
||||
# PR head `feat/housekeeping-panel` → simoleo89/Nitro_Render_V3 @ feat/housekeeping-packets
|
||||
# PR base `Dev` (upstream) → duckietm/Nitro_Render_V3 @ Dev
|
||||
# PR base `feat/**` → duckietm/Nitro_Render_V3 @ Dev
|
||||
#
|
||||
# Override via workflow_dispatch inputs when you need an ad-hoc
|
||||
# pairing.
|
||||
# the catalog edit composer. When a feature touches both repos,
|
||||
# point RENDERER_REPO/RENDERER_REF (or the dispatch inputs) at the
|
||||
# companion renderer branch.
|
||||
- name: Resolve renderer ref
|
||||
id: renderer
|
||||
env:
|
||||
IN_REPO: ${{ github.event.inputs.renderer_repo }}
|
||||
IN_REF: ${{ github.event.inputs.renderer_ref }}
|
||||
VAR_REPO: ${{ vars.RENDERER_REPO }}
|
||||
VAR_REF: ${{ vars.RENDERER_REF }}
|
||||
run: |
|
||||
REPO="${{ github.event.inputs.renderer_repo }}"
|
||||
REF="${{ github.event.inputs.renderer_ref }}"
|
||||
@@ -93,6 +98,13 @@ jobs:
|
||||
AUTO_REPO="duckietm/Nitro_Render_V3"
|
||||
AUTO_REF="main"
|
||||
;;
|
||||
Dev)
|
||||
# The client `Dev` branch carries the custom features
|
||||
# (rare values, fortune wheel, soundboard); they live on
|
||||
# the matching renderer fork branch, not upstream.
|
||||
AUTO_REPO="medievalshell/Nitro_Render_V3"
|
||||
AUTO_REF="dev"
|
||||
;;
|
||||
feat/housekeeping-panel)
|
||||
AUTO_REPO="simoleo89/Nitro_Render_V3"
|
||||
AUTO_REF="feat/housekeeping-packets"
|
||||
@@ -107,9 +119,18 @@ jobs:
|
||||
[ -z "$REF" ] && REF="$AUTO_REF"
|
||||
fi
|
||||
|
||||
# Precedence: dispatch input → repo variable → upstream default.
|
||||
REPO="$IN_REPO"
|
||||
[ -z "$REPO" ] && REPO="$VAR_REPO"
|
||||
[ -z "$REPO" ] && REPO="$UPSTREAM_RENDERER_REPO"
|
||||
|
||||
REF="$IN_REF"
|
||||
[ -z "$REF" ] && REF="$VAR_REF"
|
||||
[ -z "$REF" ] && REF="$DEFAULT_REF"
|
||||
|
||||
echo "repo=$REPO" >> "$GITHUB_OUTPUT"
|
||||
echo "ref=$REF" >> "$GITHUB_OUTPUT"
|
||||
echo "Resolved renderer pairing: $REPO @ $REF (client ctx: ${GITHUB_BASE_REF:-$GITHUB_REF_NAME}, event: ${GITHUB_EVENT_NAME})"
|
||||
echo "Resolved renderer pairing: $REPO @ $REF (client ctx: $CTX, event: ${GITHUB_EVENT_NAME})"
|
||||
|
||||
- name: Checkout Nitro_Render_V3 (sibling)
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -44,3 +44,4 @@ Thumbs.db
|
||||
# the dev server takes minutes to start with 100k+ files under public/.
|
||||
/public/nitro-assets
|
||||
/public/swf
|
||||
.superpowers/
|
||||
|
||||
@@ -6,19 +6,27 @@ the ground running.
|
||||
|
||||
## TL;DR
|
||||
|
||||
This branch — **`feat/react19-modernization`** — is a long-running modernization
|
||||
of the Nitro V3 client: bump to React 19.2 idioms, add the supporting
|
||||
infrastructure (TanStack Query, Zustand, Vitest, React Compiler, error
|
||||
boundaries), split a few god-hooks, and audit logic bugs along the way.
|
||||
PR is **#2** on `simoleo89/Nitro-V3`.
|
||||
This client carries a long-running React 19.2 modernization: React 19
|
||||
idioms + supporting infrastructure (TanStack Query, Zustand, Vitest,
|
||||
React Compiler, error boundaries), god-hook splits, and logic-bug audits.
|
||||
|
||||
Upstream `duckietm/Nitro-V3` (`origin/Dev`) is merged in through
|
||||
`b2318b9` as of 2026-05-18 (merge commit `779a98c`). That brings in
|
||||
JSON5 config support, user-settings (reset password / email / change
|
||||
username), wear-badge popup fix, login screen fix, About update, and
|
||||
the offer-selection refactor. When syncing the next batch of upstream
|
||||
commits, expect conflicts in `App.tsx` / `bootstrap.ts` / `LoginView.tsx`
|
||||
on React 19 imports — always keep the modernized local version.
|
||||
**Working base is now `main`** (tracking `duckietm/Nitro-V3`). The earlier
|
||||
`feat/react19-modernization` long-running branch was superseded — feature
|
||||
work now ships as small focused PRs against `duckietm:Dev`, staged through
|
||||
Dev then merged to main. (`feat/react19-modernization` still exists on the
|
||||
fork as backup; do not force-push it.)
|
||||
|
||||
**Navigator modernization landed** (merged to main 2026-05-28, PRs
|
||||
#168/#169/#170): the 492-line `useNavigator` god-hook was split into
|
||||
`useNavigatorStore` + `useNavigatorData`/`useNavigatorUiState`/
|
||||
`useNavigatorSearch` filters (wired-tools layout), door lifecycle extracted
|
||||
to `src/hooks/rooms/widgets/useDoorState.ts`, 9 UI flags moved to a Zustand
|
||||
`navigatorUiStore`, search migrated to a query hook, and 5 sub-views wrapped
|
||||
in `WidgetErrorBoundary`. **Caveat**: duckietm patched `useNavigatorSearch`
|
||||
post-merge (`05d71dd1`) — see the `useNitroQuery` fragility note below.
|
||||
|
||||
When syncing upstream, expect conflicts in `App.tsx` / `bootstrap.ts` /
|
||||
`LoginView.tsx` on React 19 imports — always keep the modernized version.
|
||||
|
||||
Local-dev game assets are served by a small Vite plugin (`sirv` middleware
|
||||
mounted on `/nitro-assets` and `/swf`, reading from
|
||||
@@ -236,6 +244,20 @@ and invalidates the query slot on every push, so server-driven
|
||||
refresh paths work the same as the initial request/response (e.g.
|
||||
ClubGiftInfoEvent firing again after the user claims a gift).
|
||||
|
||||
**⚠️ Fragility — do NOT use `useNitroQuery` for primary visible data.**
|
||||
The one-shot listener inside `awaitNitroResponse` (register listener →
|
||||
await one matching response → remove itself) is fragile against
|
||||
renderer-bundle quirks: for some parsers the event fires but the listener
|
||||
never matches, so the promise never resolves and `query.data` stays
|
||||
`undefined` forever — the UI shows the server's response arriving in logs
|
||||
but renders blank. This bit **ModTools Room/CFH chatlog** (reverted to
|
||||
`useMessageEvent + useEffect`) and then **Navigator search** (P2 shipped
|
||||
with `useNitroQuery`, duckietm reverted it in `05d71dd1` to the god-hook
|
||||
pattern). **Rule: reserve `useNitroQuery` for config / secondary fetches
|
||||
where a brief blank is tolerable. For anything that is the primary visible
|
||||
content of a panel, use `useMessageEvent + useState/useEffect`** — that's
|
||||
what the rest of the codebase does and it's robust.
|
||||
|
||||
### Singleton-filter split for `useBetween`-based hooks
|
||||
|
||||
When a hook backs many consumers but most only need either state OR
|
||||
@@ -339,6 +361,7 @@ into `configurePreviewServer` so `yarn preview` keeps working.
|
||||
| Zustand | `NavigatorRoomCreatorView` (`useRoomCreatorStore`), `WiredCreatorToolsView` (`useWiredCreatorToolsUiStore` — every panel-lifecycle-relevant flag, snapshot, selection, highlight, inline editor, picker chain hoisted; what's left in the component as `useState` is genuinely transient: keepSelected, globalClock, roomEnteredAt, selectedMonitorErrorType, selectedMonitorLogDetails) |
|
||||
| God-hook split (state + actions + shim) | `doorbell`, `poll`, `furni-chooser`, `user-chooser`, `friend-request`, `chat-input` |
|
||||
| God-hook split (`useBetween` singleton + state filter + actions filter + shim) | `wired-tools`, `translation`, `notification`, `friends`, `catalog` (three-way: `useCatalogData` / `useCatalogUiState` / `useCatalogActions` — all 48 consumers migrated, deprecated `useCatalog` shim removed) |
|
||||
| Navigator modernization (merged to main 2026-05-28, PRs #168/#169/#170) | 492-line `useNavigator` god-hook split into `useNavigatorStore` (internal `useBetween` closure) + flat filters `useNavigatorData` / `useNavigatorUiState` / `useNavigatorSearch`; door bell/password lifecycle extracted to `src/hooks/rooms/widgets/useDoorState.ts` (dual-subscribes `GetGuestRoomResultEvent` + `GenericErrorEvent` alongside the nav store, each filtering by branch/errorCode); 9 UI flags + `currentTabCode`/`currentFilter` in Zustand `navigatorUiStore` (`src/hooks/navigator/navigatorUiStore.ts`); all 5 Navigator sub-views wrapped in `WidgetErrorBoundary`; old shim deleted. **`useNavigatorSearch` was reverted by duckietm (`05d71dd1`) from `useNitroQuery` to `useMessageEvent + useEffect`** — see the useNitroQuery fragility note. Specs/plans under `docs/superpowers/`. |
|
||||
| `WidgetErrorBoundary` | `RoomWidgetsView` umbrella + per-widget wrap on all 13 room widgets and all 20 furniture widgets (so a crash in one widget no longer takes down its siblings) |
|
||||
| Vitest | 207/207 cases — pure helpers (incl. 4 new on `getPetPackageNameError`) + 2 Zustand store suites (`navigatorRoomCreatorStore`, `wiredCreatorToolsUiStore` with 45 cases including the picker-chain hoists) + 2 component-/hook-level pilots (WidgetErrorBoundary, useDoorbellState) on top of the renderer-SDK mock at `src/nitro-renderer.mock.ts`, 34 cases on the catalog pure helpers, 4 contract cases on the catalog filters. **Tests are co-located** under `src/`, alongside their subject. |
|
||||
| Form Actions | Login / Register / Forgot (LoginView.tsx) |
|
||||
@@ -412,6 +435,11 @@ See `docs/ARCHITECTURE.md` "Recently fixed" for fix shapes.
|
||||
`useCatalogUiState` / `useCatalogActions` in
|
||||
`src/hooks/catalog/useCatalog.ts` (all 48 consumers migrated;
|
||||
deprecated `useCatalog` shim removed)
|
||||
- Navigator hooks: `src/hooks/navigator/` — `useNavigatorStore.ts`
|
||||
(internal closure), `useNavigatorData.ts` / `useNavigatorUiState.ts` /
|
||||
`useNavigatorSearch.ts` (filters), `navigatorUiStore.ts` (Zustand UI
|
||||
flags + `setTab`/`setFilter`). Door lifecycle: `src/hooks/rooms/widgets/useDoorState.ts`.
|
||||
Specs/plans: `docs/superpowers/specs/2026-05-2*-navigator-*.md`
|
||||
- Renderer-SDK mock for Vitest: `src/nitro-renderer.mock.ts`
|
||||
(aliased over `@nitrots/nitro-renderer` via `vitest.config.mts`).
|
||||
Hosts the explicit `NitroLogger` mock, the `mockEventDispatcher` /
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Custom themes (graphics-only)
|
||||
|
||||
Ecosistema temi caricati a **runtime** (niente rebuild del client). Un tema =
|
||||
una cartella con un manifest + "pezzi" CSS. Ogni pezzo è attivabile/disattivabile
|
||||
dall'utente da **Impostazioni → Temi** (checkbox). Se un pezzo è rotto/404 →
|
||||
fallback automatico al default (solo quel pezzo).
|
||||
|
||||
## Dove vivono
|
||||
- **Questa cartella (`custom-themes/`) è solo il TEMPLATE di riferimento**, versionata su git.
|
||||
- I temi **veri** stanno sul server in `public/nitro/custom-themes/` (serviti via
|
||||
l'url configurato in ui-config `theme.base.url`, es. `/client/nitro/custom-themes`).
|
||||
NON vanno su git → vedi `.gitignore` (`public/custom-themes/`).
|
||||
|
||||
## Struttura
|
||||
```
|
||||
custom-themes/
|
||||
index.json # { "themes": [ { "id", "name", "author?" } ] }
|
||||
<id>/
|
||||
theme.json # { "name", "pieces": [ { "id", "name", "file" } ] }
|
||||
cards.css chat.css ... # un file per "pezzo"
|
||||
assets/... # immagini referenziate dai CSS (url assoluti)
|
||||
```
|
||||
|
||||
## Creare un tema
|
||||
1. Copia `neon-viola/` in una nuova cartella `<id>/`.
|
||||
2. Modifica `theme.json` (nome + elenco pezzi).
|
||||
3. Scrivi i CSS dei pezzi (override con `!important`, caricati dopo il base).
|
||||
4. Aggiungi `{ "id": "<id>", "name": "..." }` a `index.json`.
|
||||
5. Carica la cartella in `public/nitro/custom-themes/` sul server. **Nessun rebuild.**
|
||||
|
||||
## Default hotel-wide (admin)
|
||||
In `ui-config.json`:
|
||||
- `theme.base.url` → dove sono serviti i temi
|
||||
- `theme.default` → id del tema attivo di default (vuoto = nessuno)
|
||||
- `theme.default.pieces` → array di id pezzi attivi di default
|
||||
|
||||
Ogni utente può comunque sovrascrivere da Impostazioni → Temi (salvato in localStorage).
|
||||
|
||||
> Nota: i temi ri-skinnano solo la **grafica** (CSS). Non cambiano la struttura
|
||||
> dei componenti né il comportamento.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"themes": [
|
||||
{ "id": "neon-viola", "name": "Neon Viola", "author": "infinityhotel" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/* Tema Neon Viola — pezzo "cards" (finestre / NitroCard).
|
||||
Ricolora header + cornice delle finestre. Caricato DOPO il CSS base, quindi
|
||||
usa !important per vincere. Tocca solo la cornice/header (non lo sfondo del
|
||||
contenuto) per non rovinare la leggibilita' del testo. */
|
||||
|
||||
.nitro-card-shell:not(.nitro-wired) {
|
||||
border-color: #7c3aed !important;
|
||||
box-shadow: 0 0 14px rgba(124, 58, 237, .55), 0 8px 22px rgba(0, 0, 0, .4) !important;
|
||||
}
|
||||
|
||||
.nitro-card-shell:not(.nitro-wired) .nitro-card-header-shell {
|
||||
background: linear-gradient(180deg, #9333ea 0%, #6d28d9 100%) !important;
|
||||
border-color: #a855f7 !important;
|
||||
border-bottom-color: #2a0a4a !important;
|
||||
}
|
||||
|
||||
.nitro-card-shell:not(.nitro-wired) .nitro-card-title {
|
||||
color: #fff !important;
|
||||
text-shadow: 0 0 6px #c084fc, 0 1px 0 #3b0764 !important;
|
||||
}
|
||||
|
||||
.nitro-card-shell:not(.nitro-wired) .nitro-card-tabs-shell .nitro-card-tab-item-active {
|
||||
box-shadow: inset 0 -2px 0 #a855f7 !important;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/* Tema Neon Viola — pezzo "catalog" (catalogo Hippiehotel, .nitro-catalog). */
|
||||
|
||||
.nitro-catalog .nitro-card-header-shell {
|
||||
background: linear-gradient(180deg, #9333ea 0%, #6d28d9 100%) !important;
|
||||
}
|
||||
|
||||
.nitro-catalog .group\/rail {
|
||||
background: #1a1030 !important;
|
||||
border-right-color: #7c3aed !important;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/* Tema Neon Viola — pezzo "chat".
|
||||
Accento viola sulla bubble di default (bubble-0) e sull'input chat.
|
||||
(Le bubble custom hanno la loro grafica; qui tocchiamo solo l'accento base.) */
|
||||
|
||||
.chat-bubble.bubble-0 {
|
||||
filter: drop-shadow(0 0 5px rgba(168, 85, 247, .8));
|
||||
}
|
||||
|
||||
.nitro-chat-input-container,
|
||||
.chat-input-container {
|
||||
box-shadow: inset 0 0 0 1px #7c3aed !important;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "Neon Viola",
|
||||
"author": "infinityhotel",
|
||||
"pieces": [
|
||||
{ "id": "cards", "name": "Finestre / Card", "file": "cards.css" },
|
||||
{ "id": "chat", "name": "Chat", "file": "chat.css" },
|
||||
{ "id": "toolbar", "name": "Toolbar", "file": "toolbar.css" },
|
||||
{ "id": "catalog", "name": "Catalogo", "file": "catalog.css" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/* Tema Neon Viola — pezzo "toolbar".
|
||||
Best-effort: ricolora la barra strumenti in basso. Se i selettori non
|
||||
matchano nella tua build, il pezzo non ha effetto (fallback sicuro). */
|
||||
|
||||
.nitro-toolbar,
|
||||
[class*="toolbar-container"] {
|
||||
background: linear-gradient(180deg, #2a0a4a 0%, #1a0730 100%) !important;
|
||||
box-shadow: 0 -2px 10px rgba(124, 58, 237, .4) !important;
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
# Navigator Modernization — P1: Hook Split + UI Store
|
||||
|
||||
**Branch:** `feat/navigator-modernization` (forked from `origin/Dev` @ `d5d5ca59`)
|
||||
**Date:** 2026-05-26
|
||||
**Scope:** P1 of a 4-phase Navigator modernization sweep (P1 → P2 → P3 → P4).
|
||||
**This spec covers ONLY P1.** P2 (TanStack Query), P3 (reactive snapshots),
|
||||
and P4 (visual rework + virtualization + persistence) will each get their
|
||||
own spec when P1 lands.
|
||||
|
||||
## 1. Context
|
||||
|
||||
The Nitro-V3 client has established patterns for god-hook
|
||||
modernization, all visible on the current `origin/Dev` tip:
|
||||
|
||||
- **God-hook split into filters over a `useBetween` singleton.** Two
|
||||
precedents:
|
||||
- `useWiredTools` — 4 files (`useWiredToolsStore` + `useWiredToolsState`
|
||||
+ `useWiredToolsActions` + `useWiredTools` shim). 630-line store.
|
||||
- `useCatalog` — single 1055-line file holding store + three filters
|
||||
(`useCatalogData` / `useCatalogUiState` / `useCatalogActions`).
|
||||
- **Zustand UI stores** via `createNitroStore` (`src/state/createNitroStore.ts`)
|
||||
for cross-feature UI flags.
|
||||
- **Renderer snapshot consumer hooks** (`useSyncExternalStore`) — out of
|
||||
scope for P1, used in P3.
|
||||
- **`useNitroQuery`** for composer/parser request-response — out of
|
||||
scope for P1, used in P2.
|
||||
- **Co-located Vitest suites** under `src/`, sharing the renderer-SDK
|
||||
stub at `src/nitro-renderer.mock.ts`.
|
||||
|
||||
`src/hooks/navigator/useNavigator.ts` is the largest remaining god-hook
|
||||
on this branch: 492 lines, 21 event listeners, 9 internal `useState`,
|
||||
consumed by 13 files (10 inside `src/components/navigator/` + 3
|
||||
outside in `room-tools`, `room-filter-words`, and `catalog` views). It
|
||||
mixes three logically separate concerns:
|
||||
|
||||
1. **Navigator data** — search results, categories, top-level
|
||||
contexts, favourites, metadata.
|
||||
2. **Door state** — doorbell, password prompt, accepted / no-answer /
|
||||
wrong-password lifecycle.
|
||||
3. **Local UI flags** — 9 `useState` in `NavigatorView.tsx` controlling
|
||||
panel visibility and search lifecycle.
|
||||
|
||||
P1 separates these three and migrates all consumers.
|
||||
|
||||
## 2. Decisions
|
||||
|
||||
| Topic | Decision |
|
||||
|---|---|
|
||||
| Door state | **Extract** to `src/hooks/rooms/widgets/useDoorState.ts` |
|
||||
| UI store scope | **All 9 flags** into `navigatorUiStore` Zustand |
|
||||
| Shim retention | **Remove** `useNavigator` after all 13 consumers migrated |
|
||||
| Filter shape | **Flat objects**, mirroring `useCatalog` and `useWiredTools` |
|
||||
| File layout | **4 separate files**, mirroring `wired-tools` (not the monolithic `useCatalog.ts`) |
|
||||
| Scope of P1 | **Pure refactor** — zero user-visible change |
|
||||
| Branch | `feat/navigator-modernization` (forked from `origin/Dev`, not a sub-branch of any other modernization branch) |
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
Mirrors the `wired-tools` layout exactly — 4 hook files in
|
||||
`src/hooks/navigator/`, plus a sibling `navigatorUiStore.ts` for the
|
||||
Zustand UI flags, plus `useDoorState.ts` extracted to
|
||||
`src/hooks/rooms/widgets/`:
|
||||
|
||||
```
|
||||
src/hooks/navigator/
|
||||
├── useNavigatorStore.ts ← NEW: internal useBetween closure
|
||||
│ (data state + non-door listeners + actions)
|
||||
├── useNavigatorData.ts ← NEW: public filter — read-only data
|
||||
├── useNavigatorUiState.ts ← NEW: public filter — read-only UI flags
|
||||
├── useNavigatorActions.ts ← NEW: public filter — imperative actions
|
||||
├── navigatorUiStore.ts ← NEW: Zustand UI store (9 flags + actions)
|
||||
├── index.ts ← REWRITTEN: barrel exports the 3 filters,
|
||||
│ useNavigatorUiStore, and re-exports useDoorState
|
||||
└── useNavigator.ts ← DELETED at end of P1 (god-hook shim removed)
|
||||
|
||||
src/hooks/rooms/widgets/
|
||||
└── useDoorState.ts ← NEW: extracted door lifecycle
|
||||
```
|
||||
|
||||
### 3.1 Internal `useNavigatorStore` closure (in `useNavigatorStore.ts`)
|
||||
|
||||
The single `useBetween` singleton's internal function. Holds:
|
||||
|
||||
- All non-door state currently in `useNavigatorState` of the old
|
||||
`useNavigator.ts`: `categories`, `eventCategories`,
|
||||
`favouriteRoomIds`, `topLevelContext`, `topLevelContexts`,
|
||||
`searchResult`, `navigatorSearches`, `navigatorData`.
|
||||
- All non-door event listeners (16 of them): `FavouritesEvent`,
|
||||
`FavouriteChangedEvent`, `RoomSettingsUpdatedEvent`,
|
||||
`CanCreateRoomEventEvent`, `UserInfoEvent`, `UserPermissionsEvent`,
|
||||
`RoomForwardEvent`, `RoomEntryInfoMessageEvent`,
|
||||
`NavigatorMetadataEvent`, `NavigatorSearchEvent`,
|
||||
`UserFlatCatsEvent`, `UserEventCatsEvent`, `FlatCreatedEvent`,
|
||||
`NavigatorHomeRoomEvent`, `RoomEnterErrorEvent`,
|
||||
`NavigatorOpenRoomCreatorEvent`, `NavigatorSearchesEvent`,
|
||||
plus `NitroEventType.SOCKET_RECONNECTING`.
|
||||
- `GetGuestRoomResultEvent` — dual-subscribed (see §5.2).
|
||||
- `GenericErrorEvent` — dual-subscribed (see §5.3).
|
||||
- New imperative actions `sendSearch` and `reloadCurrentSearch`,
|
||||
extracted from the current `NavigatorView.tsx` locals (today defined
|
||||
on lines 42-79 of `src/components/navigator/NavigatorView.tsx`).
|
||||
|
||||
### 3.2 The three filters (flat shape, wired-tools layout)
|
||||
|
||||
```ts
|
||||
// useNavigatorData.ts
|
||||
import { useBetween } from 'use-between';
|
||||
import { useNavigatorStore } from './useNavigatorStore';
|
||||
|
||||
export const useNavigatorData = () => {
|
||||
const {
|
||||
categories, eventCategories, favouriteRoomIds,
|
||||
topLevelContext, topLevelContexts,
|
||||
searchResult, navigatorSearches, navigatorData,
|
||||
} = useBetween(useNavigatorStore);
|
||||
return {
|
||||
categories, eventCategories, favouriteRoomIds,
|
||||
topLevelContext, topLevelContexts,
|
||||
searchResult, navigatorSearches, navigatorData,
|
||||
};
|
||||
};
|
||||
|
||||
// useNavigatorUiState.ts
|
||||
import { useNavigatorUiStore } from './navigatorUiStore';
|
||||
|
||||
export const useNavigatorUiState = () => {
|
||||
const isVisible = useNavigatorUiStore(s => s.isVisible);
|
||||
const isReady = useNavigatorUiStore(s => s.isReady);
|
||||
const isCreatorOpen = useNavigatorUiStore(s => s.isCreatorOpen);
|
||||
const isRoomInfoOpen = useNavigatorUiStore(s => s.isRoomInfoOpen);
|
||||
const isRoomLinkOpen = useNavigatorUiStore(s => s.isRoomLinkOpen);
|
||||
const isOpenSavesSearches = useNavigatorUiStore(s => s.isOpenSavesSearches);
|
||||
const isLoading = useNavigatorUiStore(s => s.isLoading);
|
||||
const needsInit = useNavigatorUiStore(s => s.needsInit);
|
||||
const needsSearch = useNavigatorUiStore(s => s.needsSearch);
|
||||
return {
|
||||
isVisible, isReady, isCreatorOpen, isRoomInfoOpen, isRoomLinkOpen,
|
||||
isOpenSavesSearches, isLoading, needsInit, needsSearch,
|
||||
};
|
||||
};
|
||||
|
||||
// useNavigatorActions.ts
|
||||
import { useBetween } from 'use-between';
|
||||
import { useNavigatorStore } from './useNavigatorStore';
|
||||
|
||||
export const useNavigatorActions = () => {
|
||||
const { sendSearch, reloadCurrentSearch } = useBetween(useNavigatorStore);
|
||||
return { sendSearch, reloadCurrentSearch };
|
||||
};
|
||||
```
|
||||
|
||||
`useNavigatorActions` is intentionally small in P1 — favourite
|
||||
toggles, room visits, and door responses keep flowing through their
|
||||
existing direct composer calls in consumer components. We only hoist
|
||||
the two functions that are currently prop-drilled into
|
||||
`NavigatorSearchView` and the tab `onClick` handlers.
|
||||
|
||||
`useNavigatorUiState` uses per-key Zustand selectors (one selector
|
||||
per flag) so a component re-renders only when a flag it actually
|
||||
reads changes. The flat object it returns preserves the API shape
|
||||
consumers expect.
|
||||
|
||||
### 3.3 `navigatorUiStore` (Zustand)
|
||||
|
||||
```ts
|
||||
// src/hooks/navigator/navigatorUiStore.ts
|
||||
import { createNitroStore } from '../../state/createNitroStore';
|
||||
|
||||
type NavigatorUiState = {
|
||||
isVisible: boolean;
|
||||
isReady: boolean;
|
||||
isCreatorOpen: boolean;
|
||||
isRoomInfoOpen: boolean;
|
||||
isRoomLinkOpen: boolean;
|
||||
isOpenSavesSearches: boolean;
|
||||
isLoading: boolean;
|
||||
needsInit: boolean;
|
||||
needsSearch: boolean;
|
||||
};
|
||||
|
||||
type NavigatorUiActions = {
|
||||
show(): void;
|
||||
hide(): void;
|
||||
toggle(): void;
|
||||
openCreator(): void;
|
||||
closeCreator(): void;
|
||||
setRoomInfoOpen(open: boolean): void;
|
||||
toggleRoomInfo(): void;
|
||||
setRoomLinkOpen(open: boolean): void;
|
||||
toggleRoomLink(): void;
|
||||
toggleSavesSearches(): void;
|
||||
setLoading(loading: boolean): void;
|
||||
markReady(): void;
|
||||
markInitDone(): void;
|
||||
requestSearch(): void; // sets needsSearch = true
|
||||
consumeSearchRequest(): void; // sets needsSearch = false
|
||||
};
|
||||
|
||||
const INITIAL: NavigatorUiState = {
|
||||
isVisible: false,
|
||||
isReady: false,
|
||||
isCreatorOpen: false,
|
||||
isRoomInfoOpen: false,
|
||||
isRoomLinkOpen: false,
|
||||
isOpenSavesSearches: false,
|
||||
isLoading: false,
|
||||
needsInit: true,
|
||||
needsSearch: false,
|
||||
};
|
||||
|
||||
export const useNavigatorUiStore = createNitroStore<NavigatorUiState & NavigatorUiActions>()((set) => ({
|
||||
...INITIAL,
|
||||
show: () => set({ isVisible: true, needsSearch: true }),
|
||||
hide: () => set({ isVisible: false }),
|
||||
toggle: () => set((s) => s.isVisible
|
||||
? { isVisible: false }
|
||||
: { isVisible: true, needsSearch: true }),
|
||||
openCreator: () => set({ isVisible: true, isCreatorOpen: true }),
|
||||
closeCreator: () => set({ isCreatorOpen: false }),
|
||||
setRoomInfoOpen: (open) => set({ isRoomInfoOpen: open }),
|
||||
toggleRoomInfo: () => set((s) => ({ isRoomInfoOpen: !s.isRoomInfoOpen })),
|
||||
setRoomLinkOpen: (open) => set({ isRoomLinkOpen: open }),
|
||||
toggleRoomLink: () => set((s) => ({ isRoomLinkOpen: !s.isRoomLinkOpen })),
|
||||
toggleSavesSearches: () => set((s) => ({ isOpenSavesSearches: !s.isOpenSavesSearches })),
|
||||
setLoading: (loading) => set({ isLoading: loading }),
|
||||
markReady: () => set({ isReady: true }),
|
||||
markInitDone: () => set({ needsInit: false }),
|
||||
requestSearch: () => set({ needsSearch: true }),
|
||||
consumeSearchRequest: () => set({ needsSearch: false }),
|
||||
}));
|
||||
```
|
||||
|
||||
The `linkTracker` in `NavigatorView.tsx` calls these actions directly
|
||||
on `useNavigatorUiStore.getState()` instead of mutating local
|
||||
`useState`. That collapses the switch statement from 30+ lines to a
|
||||
clean dispatch table and eliminates the closure-over-stale-state hazard
|
||||
where the tracker re-registers on every `isVisible` change (today at
|
||||
`src/components/navigator/NavigatorView.tsx:162`).
|
||||
|
||||
### 3.4 `useDoorState` (extracted to `src/hooks/rooms/widgets/`)
|
||||
|
||||
```ts
|
||||
// src/hooks/rooms/widgets/useDoorState.ts
|
||||
import { DoorbellMessageEvent, FlatAccessDeniedMessageEvent,
|
||||
GenericErrorEvent, GetGuestRoomResultEvent,
|
||||
GetSessionDataManager, RoomDataParser,
|
||||
RoomDoorbellAcceptedEvent } from '@nitrots/nitro-renderer';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useBetween } from 'use-between';
|
||||
import { DoorStateType } from '../../../api';
|
||||
import { useMessageEvent } from '../../events';
|
||||
|
||||
export type DoorStateSnapshot = {
|
||||
roomInfo: RoomDataParser | null;
|
||||
state: number; // DoorStateType.*
|
||||
};
|
||||
|
||||
const INITIAL: DoorStateSnapshot = { roomInfo: null, state: DoorStateType.NONE };
|
||||
|
||||
const useDoorStateStore = () => {
|
||||
const [snapshot, setSnapshot] = useState<DoorStateSnapshot>(INITIAL);
|
||||
|
||||
useMessageEvent<DoorbellMessageEvent>(DoorbellMessageEvent, event => {
|
||||
const parser = event.getParser();
|
||||
if (parser.userName && parser.userName.length > 0) return;
|
||||
setSnapshot(prev => ({ ...prev, state: DoorStateType.STATE_WAITING }));
|
||||
});
|
||||
|
||||
useMessageEvent<RoomDoorbellAcceptedEvent>(RoomDoorbellAcceptedEvent, event => {
|
||||
const parser = event.getParser();
|
||||
if (parser.userName && parser.userName.length > 0) return;
|
||||
setSnapshot(prev => ({ ...prev, state: DoorStateType.STATE_ACCEPTED }));
|
||||
});
|
||||
|
||||
useMessageEvent<FlatAccessDeniedMessageEvent>(FlatAccessDeniedMessageEvent, event => {
|
||||
const parser = event.getParser();
|
||||
if (parser.userName && parser.userName.length > 0) return;
|
||||
setSnapshot(prev => ({ ...prev, state: DoorStateType.STATE_NO_ANSWER }));
|
||||
});
|
||||
|
||||
useMessageEvent<GenericErrorEvent>(GenericErrorEvent, event => {
|
||||
const parser = event.getParser();
|
||||
if (parser.errorCode !== -100002) return; // door-only error code
|
||||
setSnapshot(prev => ({ ...prev, state: DoorStateType.STATE_WRONG_PASSWORD }));
|
||||
});
|
||||
|
||||
useMessageEvent<GetGuestRoomResultEvent>(GetGuestRoomResultEvent, event => {
|
||||
const parser = event.getParser();
|
||||
// ONLY handle the roomForward branch with door modes
|
||||
if (!parser.roomForward) return;
|
||||
if (parser.data.ownerName === GetSessionDataManager().userName) return;
|
||||
if (parser.isGroupMember) return;
|
||||
if (parser.data.doorMode === RoomDataParser.DOORBELL_STATE) {
|
||||
setSnapshot({ roomInfo: parser.data, state: DoorStateType.START_DOORBELL });
|
||||
} else if (parser.data.doorMode === RoomDataParser.PASSWORD_STATE) {
|
||||
setSnapshot({ roomInfo: parser.data, state: DoorStateType.START_PASSWORD });
|
||||
}
|
||||
});
|
||||
|
||||
const reset = useCallback(() => setSnapshot(INITIAL), []);
|
||||
|
||||
return { snapshot, setSnapshot, reset };
|
||||
};
|
||||
|
||||
export const useDoorState = () => useBetween(useDoorStateStore);
|
||||
```
|
||||
|
||||
The current `NavigatorDoorStateView.tsx` does
|
||||
`setDoorData({ roomInfo: null, state: DoorStateType.NONE })` to reset
|
||||
— after P1 it calls `reset()`.
|
||||
|
||||
## 4. Consumer migration map (13 files)
|
||||
|
||||
| File | Reads today | Reads after P1 |
|
||||
|---|---|---|
|
||||
| `NavigatorView.tsx` | full `useNavigator()` + 9 local useState | `useNavigatorData` + `useNavigatorActions` + `useNavigatorUiStore` (one selector per flag) |
|
||||
| `NavigatorDoorStateView.tsx` | `doorData`, `setDoorData` | `useDoorState` (`snapshot`, `setSnapshot`, `reset`) |
|
||||
| `NavigatorRoomCreatorView.tsx` | `categories` | `useNavigatorData` |
|
||||
| `NavigatorRoomInfoView.tsx` | `navigatorData`, `favouriteRoomIds` | `useNavigatorData` |
|
||||
| `NavigatorRoomLinkView.tsx` | `navigatorData.enteredGuestRoom` | `useNavigatorData` |
|
||||
| `NavigatorRoomSettingsBasicTabView.tsx` | `categories` | `useNavigatorData` |
|
||||
| `NavigatorSearchResultItemView.tsx` | `favouriteRoomIds`, `navigatorData` | `useNavigatorData` |
|
||||
| `NavigatorSearchResultItemInfoView.tsx` | `navigatorData` | `useNavigatorData` |
|
||||
| `NavigatorSearchResultView.tsx` | `topLevelContext` | `useNavigatorData` |
|
||||
| `NavigatorSearchView.tsx` | `topLevelContext` + `sendSearch` prop | `useNavigatorData` + `useNavigatorActions` |
|
||||
| `CatalogLayoutRoomAdsView.tsx` | `navigatorData.currentRoomId` | `useNavigatorData` |
|
||||
| `RoomFilterWordsWidgetView.tsx` | `navigatorData.currentRoomId` | `useNavigatorData` |
|
||||
| `RoomToolsWidgetView.tsx` | `navigatorData` | `useNavigatorData` |
|
||||
|
||||
All 13 consumers get a one-line import swap (plus `NavigatorView`
|
||||
which is more involved since it owns the 9 useState + linkTracker
|
||||
dispatch + `sendSearch` prop drilling that all go away). No
|
||||
behavioural change.
|
||||
|
||||
## 5. Dual-subscription edge cases
|
||||
|
||||
### 5.1 `useBetween` guarantee
|
||||
|
||||
`useDoorState` uses `useBetween(useDoorStateStore)`, so multiple
|
||||
consumers (currently only `NavigatorDoorStateView`) share a single
|
||||
listener registration — same as how `useNavigatorStore` works.
|
||||
|
||||
### 5.2 `GetGuestRoomResultEvent` — dual subscription
|
||||
|
||||
Today this event is handled in one place (current `useNavigator.ts`
|
||||
lines 130-209) with three branches: `roomEnter`, `roomForward`, else.
|
||||
After P1:
|
||||
|
||||
- `useDoorStateStore` subscribes and acts ONLY on the `roomForward`
|
||||
branch when `doorMode` is `DOORBELL_STATE` or `PASSWORD_STATE` AND
|
||||
the user is not the owner / not a group member.
|
||||
- `useNavigatorStore` subscribes and handles `roomEnter`, the
|
||||
`roomForward` branch WITHOUT door modes (direct `CreateRoomSession`
|
||||
call), and the `else` branch.
|
||||
|
||||
Multiple subscribers to the same event is an accepted pattern (see
|
||||
`FlatCreatedEvent` listened in `useNavigator` and elsewhere). Both
|
||||
listeners register through `useMessageEvent` so the renderer event
|
||||
bus dispatches to both.
|
||||
|
||||
### 5.3 `GenericErrorEvent` — dual subscription
|
||||
|
||||
- `useDoorStateStore` acts ONLY on `errorCode === -100002` (wrong
|
||||
password).
|
||||
- `useNavigatorStore` acts on `4009`, `4010`, `4011`, `4013` (room
|
||||
management alerts via `simpleAlert`).
|
||||
|
||||
Each side filters by `errorCode` immediately — no cross-effects.
|
||||
|
||||
## 6. Visual direction (anchor for P4 — informational only)
|
||||
|
||||
P1 ships zero visual change. This section documents the visual
|
||||
target that P4's spec will detail, so the architecture choices in
|
||||
P1 align with where we are heading.
|
||||
|
||||
### 6.1 Current pain points (from user screenshots, 2026-05-26)
|
||||
|
||||
- **Tab "Pubbliche":** empty state is bare text "No rooms found".
|
||||
- **Tab "Tutte le stanze":** popular rooms shown as a small thumbnail
|
||||
grid; the "Party" category uses a compact list mode with no
|
||||
visual hierarchy or live signal.
|
||||
- **Tab "Eventi":** empty state is bare text "No rooms found".
|
||||
- **Tab "Il mio mondo":** sparse list, no per-room preview.
|
||||
- **Saved searches:** today a 600px-wide sidebar that resizes the
|
||||
card and pushes content right.
|
||||
- **Filter dropdown "Qualsiasi":** opaque about what filters exist.
|
||||
|
||||
### 6.2 Target shape (P4 spec will detail)
|
||||
|
||||
**Empty states with illustration + contextual CTA:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Navigator @ Habbo [×] │
|
||||
│ [⚡][Pubbliche][Tutte][Eventi][Mio] │
|
||||
├─────────────────────────────────────┤
|
||||
│ [🔓 Aperte] [🚪 Campanello] [🔒] │
|
||||
│ [filtra stanze...] 🔍 │
|
||||
│ [🔖 staff] [🔖 party] [🔖 chill] + │
|
||||
├─────────────────────────────────────┤
|
||||
│ ╭──────────╮ │
|
||||
│ │ 🏠 ✨ │ │
|
||||
│ ╰──────────╯ │
|
||||
│ Nessuna stanza pubblica │
|
||||
│ ancora attiva │
|
||||
│ │
|
||||
│ [ Esplora stanze popolari → ] │
|
||||
├─────────────────────────────────────┤
|
||||
│ [+ Crea stanza] [Da qualche parte] │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Card list with row-level hover-reveal:**
|
||||
|
||||
```
|
||||
▼ Stanze più popolari [▦ ☰] [⚡]
|
||||
┌─────────┐ Big Party Room
|
||||
│ 🏠 🎵 │ 👤 22 · 🔓 Aperta · ★ 4.7
|
||||
│ (img) │ by @Cocco
|
||||
└─────────┘ [Entra] [ⓘ] [☆ favori] ← shown on row hover
|
||||
─────────────────────────────────────────
|
||||
▼ Party [▦ ☰] [⚡]
|
||||
🟢 fcfcvcvcv 👤2 🔓 [ⓘ]
|
||||
🔒 aaaaa 👤1 🚪 [ⓘ]
|
||||
```
|
||||
|
||||
**Saved searches as horizontal chip row** above the filter input
|
||||
(replaces the 600px sidebar — no layout shift on toggle).
|
||||
|
||||
**Filter intent as visible chips** instead of "Qualsiasi" dropdown:
|
||||
`🔓 Aperte` `🚪 Campanello` `🔒 Con password` `👥 Solo amici`.
|
||||
|
||||
**Sticky section headers** when scrolling long lists.
|
||||
|
||||
**Skeleton loaders** during fetch (post-P2 when query state lands).
|
||||
|
||||
**Per-card actions on hover**: favourite ☆, info ⓘ, room link 🔗.
|
||||
|
||||
### 6.3 Why P1 architecture supports this
|
||||
|
||||
- `useNavigatorUiStore` makes future flags (`viewMode: 'compact' | 'expanded'`,
|
||||
`lastTab`, `lastScrollTop`) trivial to add — they're new state on
|
||||
the store; persistence can be added with a Zustand `persist`
|
||||
middleware on a single line.
|
||||
- Splitting `useDoorState` out means the visual rework of the door
|
||||
prompt (a separate panel, possibly modal) can evolve independently
|
||||
of Navigator search UI.
|
||||
- Three flat filters mean a new card variant (compact-vs-expanded
|
||||
list) reads `useNavigatorData` only — no risk of re-rendering the
|
||||
whole Navigator when card-mode toggles.
|
||||
|
||||
## 7. Testing strategy
|
||||
|
||||
Coherent with `CLAUDE.md` "`yarn test` must stay green on every
|
||||
commit":
|
||||
|
||||
| Suite | New / changed | Cases (target) |
|
||||
|---|---|---|
|
||||
| `navigatorUiStore.test.ts` | NEW | ~30: each action idempotent on no-op, transitions valid, `requestSearch`/`consumeSearchRequest` symmetric |
|
||||
| `useDoorState.test.tsx` | NEW | ~12: each event listener happy path + filter-by-userName + filter-by-errorCode + reset() |
|
||||
| `useNavigatorStore.test.tsx` | NEW (smoke) | ~5: 3 filters return expected shape, dispatch updates propagate to `useNavigatorData`, GenericError 4010 does NOT touch door state, GenericError -100002 DOES touch door state |
|
||||
| Existing Vitest suites | Stay green | — |
|
||||
|
||||
All tests co-located under `src/`, alongside their subject. Reuse
|
||||
`src/nitro-renderer.mock.ts` for event dispatching (the
|
||||
`mockEventDispatcher` / `clearMockEventDispatcher` helpers).
|
||||
|
||||
CI gates that must stay green: `yarn typecheck` (TS 7 native),
|
||||
`yarn test`, `yarn lint:hooks` (`react-hooks/rules-of-hooks: error`).
|
||||
|
||||
## 8. Compatibility with project conventions
|
||||
|
||||
`feat/navigator-modernization` is forked from `origin/Dev` @ `d5d5ca59`,
|
||||
so it carries everything upstream has shipped through the floorplan
|
||||
editor work + classic catalog view + emustats + housekeeping panel.
|
||||
The design respects every constraint of this base:
|
||||
|
||||
- **No new dependencies.** Uses `zustand` (present), `use-between`
|
||||
(present), `vitest` (present), `createNitroStore` (present at
|
||||
`src/state/createNitroStore.ts`).
|
||||
- **React 19 idioms** identical to the rest of the codebase. No
|
||||
manual `useMemo`/`useCallback` unless the React Compiler asks for
|
||||
them.
|
||||
- **TypeScript strict** consistent with the rest of the project.
|
||||
- **Co-located tests** under `src/` per the layout convention.
|
||||
- **No conflicts with adopted patterns**: `useNitroEvent`,
|
||||
`useMessageEvent`, `useBetween`, `createNitroStore`. The new
|
||||
filters expose plain data — they don't call snapshot hooks
|
||||
(`useSyncExternalStore`) inside `useBetween` scopes, so the
|
||||
documented "snapshot-outside-useBetween" constraint never
|
||||
triggers here.
|
||||
- **Commit author** per house rules: `simoleo89
|
||||
<simoleo89@users.noreply.github.com>` via per-command `-c`
|
||||
overrides. **No Co-Authored-By trailer.**
|
||||
- **Branch policy**: fresh branch off `origin/Dev`, pushable
|
||||
fast-forward to `simoleo/feat/navigator-modernization` (which
|
||||
doesn't yet exist on the fork — first push creates it). No
|
||||
force-push required.
|
||||
|
||||
## 9. Out of scope (explicit)
|
||||
|
||||
- TanStack Query migration of search (P2).
|
||||
- Reactive favourite icons via snapshot (P3).
|
||||
- Live user counts via snapshot (P3).
|
||||
- Virtualization of result list (P4).
|
||||
- Empty-state component (P4).
|
||||
- Saved-search chip row (P4).
|
||||
- Persistence of tab/scroll/filter (P4).
|
||||
- `useActionState` on search input (P6).
|
||||
- `WidgetErrorBoundary` wrapping of Navigator sub-views (P5 —
|
||||
independent, can land in parallel).
|
||||
- Any visual change. P1 ships byte-identical UI.
|
||||
- Any change to `NavigatorRoomSettings*` subtree (self-contained,
|
||||
only reads `categories` in one tab).
|
||||
|
||||
## 10. Acceptance criteria
|
||||
|
||||
P1 is complete when:
|
||||
|
||||
1. `src/hooks/navigator/useNavigator.ts` does NOT exist (god-hook
|
||||
removed).
|
||||
2. `src/hooks/navigator/` contains `useNavigatorStore.ts`,
|
||||
`useNavigatorData.ts`, `useNavigatorUiState.ts`,
|
||||
`useNavigatorActions.ts`, `navigatorUiStore.ts`, and an updated
|
||||
`index.ts`.
|
||||
3. `src/hooks/rooms/widgets/useDoorState.ts` exists.
|
||||
4. All 13 active consumers compile after their import swap.
|
||||
5. `yarn typecheck` clean.
|
||||
6. `yarn lint:hooks` clean.
|
||||
7. `yarn test --run` green, with at least 3 new suites
|
||||
(`navigatorUiStore`, `useDoorState`, `useNavigatorStore` smoke).
|
||||
8. Manual smoke test: open Navigator, switch each top-level tab, run
|
||||
a search, open a room with a doorbell, get rejected, open a room
|
||||
with a password, enter the right password, enter wrong password,
|
||||
open a room you own, click a favourite ☆, open RoomInfo, open
|
||||
RoomLink. Each path renders identically to pre-P1 behaviour.
|
||||
9. Branch `feat/navigator-modernization` pushed (fast-forward only)
|
||||
to `simoleo/feat/navigator-modernization` on the user's fork.
|
||||
|
||||
## 11. Risk register
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|---|---|---|
|
||||
| A consumer reads a field we forgot to expose on a filter | medium | Type-checker catches it — all 13 consumers re-typecheck on swap |
|
||||
| Dual-subscription on `GetGuestRoomResultEvent` causes double `CreateRoomSession` | low | `useDoorStateStore` only acts on doorMode bell/password; `useNavigatorStore` only acts on the other branches. Explicit `if` guards on both sides |
|
||||
| `linkTracker` re-registration leaks because deps changed | low | New tracker reads `useNavigatorUiStore.getState()` instead of closure-captured state, so its `useEffect` deps shrink |
|
||||
| `useDoorState` consumer in `NavigatorDoorStateView` regresses on `reset()` semantics | low | Smoke test in §10 covers this |
|
||||
| Per-key Zustand selectors in `useNavigatorUiState` cause stale-closure issues | low | Each selector is one-shot, no derived values; identical pattern to existing Zustand stores in the codebase |
|
||||
| Renderer SDK mismatch on local dev (e.g. floorplan-live-preview not in renderer's main) | medium | Already exists today regardless of this PR; surface in plan as a `yarn typecheck` caveat, not introduced by P1 |
|
||||
@@ -0,0 +1,243 @@
|
||||
# Navigator Modernization — P2: TanStack Query for Search
|
||||
|
||||
**Branch**: `feat/navigator-p2-query` (forked from `feat/navigator-modernization` @ `1148c0a6`)
|
||||
**Date**: 2026-05-27
|
||||
**Depends on**: P1 (hook split) — merged or pending merge
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Migrate Navigator's search request/response from event-driven imperative state to TanStack Query. The user gets:
|
||||
- **Instant tab switching** when the same tab/filter was visited before in the session (cache hit, no round-trip)
|
||||
- **Stale-while-revalidate** on revisit (shows cached results while refetching in background)
|
||||
- **Server-driven refresh** via `useNitroEventInvalidator` on `FlatCreatedEvent` and `RoomSettingsUpdatedEvent` (and possibly `FavouriteChangedEvent` if the active tab is `favorites_view`)
|
||||
- **Single source of truth** for `isFetching` — no separate `isLoading` flag to manage
|
||||
|
||||
## 2. Architecture changes
|
||||
|
||||
### 2.1 New file: `src/hooks/navigator/useNavigatorSearch.ts`
|
||||
|
||||
The query hook. Reads `currentTabCode` + `currentFilter` from `navigatorUiStore`, fires `NavigatorSearchComposer`, waits for `NavigatorSearchEvent`, returns the parsed `NavigatorSearchResultSet`.
|
||||
|
||||
```ts
|
||||
import { NavigatorSearchComposer, NavigatorSearchEvent, NavigatorSearchResultSet } from '@nitrots/nitro-renderer';
|
||||
import { useNitroEventInvalidator, useNitroQuery } from '../../api/nitro-query';
|
||||
import { useNavigatorUiStore } from './navigatorUiStore';
|
||||
|
||||
export const useNavigatorSearch = () =>
|
||||
{
|
||||
const tabCode = useNavigatorUiStore(s => s.currentTabCode);
|
||||
const filter = useNavigatorUiStore(s => s.currentFilter);
|
||||
|
||||
const query = useNitroQuery<typeof NavigatorSearchEvent, NavigatorSearchResultSet>({
|
||||
key: [ 'navigator', 'search', tabCode, filter ],
|
||||
request: () => new NavigatorSearchComposer(tabCode, filter),
|
||||
parser: NavigatorSearchEvent,
|
||||
select: e => e.getParser()?.result ?? null,
|
||||
accept: e => {
|
||||
const result = e.getParser()?.result;
|
||||
// accept-filter: only this query's matching tab code
|
||||
return !!result && result.code === tabCode;
|
||||
},
|
||||
enabled: !!tabCode,
|
||||
staleTime: 30_000 // re-fetch after 30s of staleness on revisit
|
||||
});
|
||||
|
||||
useNitroEventInvalidator(FlatCreatedEvent, [ 'navigator', 'search' ]);
|
||||
useNitroEventInvalidator(RoomSettingsUpdatedEvent, [ 'navigator', 'search' ]);
|
||||
|
||||
return {
|
||||
searchResult: query.data,
|
||||
isFetching: query.isFetching,
|
||||
refetch: query.refetch
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 2.2 `navigatorUiStore.ts` additions
|
||||
|
||||
Add 2 new state fields + 2 new actions:
|
||||
|
||||
```ts
|
||||
type NavigatorUiState = {
|
||||
// ...existing 9 flags...
|
||||
currentTabCode: string; // '' until NavigatorMetadataEvent arrives, then first top-level context code
|
||||
currentFilter: string; // '' by default
|
||||
};
|
||||
|
||||
type NavigatorUiActions = {
|
||||
// ...existing 15 actions...
|
||||
setTab(code: string): void; // also clears currentFilter
|
||||
setFilter(value: string): void;
|
||||
};
|
||||
```
|
||||
|
||||
`setTab(code)` resets `currentFilter` to `''` because switching tabs starts a fresh search. `setFilter` updates only the filter — the user is typing in the same tab.
|
||||
|
||||
### 2.3 `useNavigatorStore.ts` — remove search state ownership
|
||||
|
||||
Remove:
|
||||
- `useState<NavigatorSearchResultSet>(null)` for `searchResult`
|
||||
- `useMessageEvent<NavigatorSearchEvent>` listener
|
||||
- `sendSearch` and `reloadCurrentSearch` actions
|
||||
- The `useNavigatorUiStore.getState().setLoading(...)` calls (no longer needed)
|
||||
- The `topLevelContextRef` and `searchResultRef` (only consumed inside `reloadCurrentSearch`)
|
||||
|
||||
Keep:
|
||||
- `topLevelContext` + `topLevelContexts` (these still come from `NavigatorMetadataEvent` and drive the tab list)
|
||||
- The `NavigatorMetadataEvent` listener — but it now ALSO calls `useNavigatorUiStore.getState().setTab(parser.topLevelContexts[0]?.code ?? '')` on first arrival, to seed the initial tab. The query then activates because `currentTabCode` becomes non-empty (`enabled: !!tabCode`).
|
||||
|
||||
### 2.4 `useNavigatorData.ts` — remove `searchResult` from return shape
|
||||
|
||||
`useNavigatorData()` no longer returns `searchResult`. Consumers that need it call `useNavigatorSearch()` instead.
|
||||
|
||||
### 2.5 `useNavigatorActions.ts` — empty or removed
|
||||
|
||||
Both `sendSearch` and `reloadCurrentSearch` are gone. Either:
|
||||
- Remove the file + the export — consumers use `useNavigatorUiStore.getState().setTab(...)` / `setFilter(...)` directly
|
||||
- Or keep the file as an empty re-export for forward compat. (Decision: REMOVE — minimize dead API).
|
||||
|
||||
### 2.6 `useNavigatorUiState.ts` — add the 2 new flags
|
||||
|
||||
Add `currentTabCode` and `currentFilter` to the per-key selector list and return shape.
|
||||
|
||||
### 2.7 `useNavigatorSearch.test.tsx` — new
|
||||
|
||||
Test cases:
|
||||
- Initial mount with empty tabCode → query is disabled, no request fired
|
||||
- After `setTab('public')` → query fires NavigatorSearchComposer('public', '')
|
||||
- After `setFilter('cocco')` → query fires NavigatorSearchComposer('public', 'cocco')
|
||||
- After `setTab('events')` → currentFilter resets to '', query fires NavigatorSearchComposer('events', '')
|
||||
- `FlatCreatedEvent` invalidates the cache → refetch
|
||||
- `RoomSettingsUpdatedEvent` invalidates the cache → refetch
|
||||
- `NavigatorSearchEvent` with WRONG tabCode (e.g. server pushes an unsolicited result) is REJECTED by `accept` filter — does NOT update query data
|
||||
|
||||
### 2.8 `NavigatorView.tsx` — major rewrite
|
||||
|
||||
Replace:
|
||||
- `useNavigatorActions` import → gone
|
||||
- `useNavigatorData` no longer destructures `searchResult` — get it from `useNavigatorSearch` instead
|
||||
- 4 `useEffect` blocks driving the imperative search flow (`needsSearch`, `needsInit` lifecycle, `reloadCurrentSearch` orchestration) → gone
|
||||
- Tab `onClick={ () => sendSearch('', context.code) }` → `onClick={ () => useNavigatorUiStore.getState().setTab(context.code) }`
|
||||
- `isLoading` from `useNavigatorUiState()` → `isFetching` from `useNavigatorSearch()` query
|
||||
- `NavigatorInitComposer` initial dispatch on first `isVisible` — KEEP (still need it to get `topLevelContexts` populated)
|
||||
- `pendingSearch` ref — gone (linkTracker `case 'search'` directly does `setTab(code); setFilter(value)`)
|
||||
|
||||
Major simplification: the file shrinks ~30 lines.
|
||||
|
||||
### 2.9 `NavigatorSearchView.tsx` — drive setFilter
|
||||
|
||||
Read the file. The component currently exposes a search input that, on enter or button click, calls `sendSearch(value, currentTabCode)`. After P2 it:
|
||||
- Reads `currentFilter` from `useNavigatorUiState`
|
||||
- onChange → `useNavigatorUiStore.getState().setFilter(value)` (debounced 300ms)
|
||||
- No more `sendSearch` reference
|
||||
|
||||
Debounce: use a local `useState` for the input text + a `useEffect` that calls `setFilter(text)` 300ms after the last keystroke. Standard pattern.
|
||||
|
||||
## 3. Backward-compat considerations
|
||||
|
||||
- `useNavigatorActions.sendSearch` and `useNavigatorActions.reloadCurrentSearch` are REMOVED. No consumer outside Navigator depends on them — verified by grepping the previous P1 consumer migration.
|
||||
- `useNavigatorData.searchResult` is REMOVED. Only `NavigatorView` reads it currently — easy to migrate.
|
||||
- The `useNavigatorActions` filter itself becomes empty — consider whether to delete the file entirely. **Decision: delete the file** to minimize the API surface. Tasks 5-8 of P1 migrated `NavigatorSearchView` to use `useNavigatorActions` — that's the only consumer; it migrates to `useNavigatorUiStore` directly.
|
||||
|
||||
## 4. Out of scope (each gets its own future spec)
|
||||
|
||||
- Reactive favourite stars on cards (P3)
|
||||
- Visual rework: empty states, virtualization, chip-based UI (P4)
|
||||
- Form Action on search input (P6)
|
||||
|
||||
## 5. Acceptance criteria
|
||||
|
||||
P2 is complete when:
|
||||
|
||||
1. `src/hooks/navigator/useNavigatorSearch.ts` exists and exports `useNavigatorSearch`
|
||||
2. `useNavigatorStore.ts` no longer owns `searchResult`, no longer subscribes to `NavigatorSearchEvent`, no longer exposes `sendSearch` or `reloadCurrentSearch`
|
||||
3. `navigatorUiStore.ts` has `currentTabCode` + `currentFilter` state and `setTab` + `setFilter` actions
|
||||
4. `useNavigatorActions.ts` is deleted; barrel no longer exports `useNavigatorActions`
|
||||
5. `useNavigatorData.ts` no longer returns `searchResult`
|
||||
6. `useNavigatorUiState.ts` returns `currentTabCode` + `currentFilter`
|
||||
7. `NavigatorView.tsx` reads `searchResult` from `useNavigatorSearch()`, uses `isFetching` for the loading flag, calls `setTab` on tab clicks
|
||||
8. `NavigatorSearchView.tsx` debounces `setFilter` calls
|
||||
9. `yarn typecheck` clean (same pre-existing floorplan errors)
|
||||
10. `yarn test --run` green; smoke test updated; new `useNavigatorSearch.test.tsx` with 7 cases
|
||||
11. `yarn lint:hooks` clean
|
||||
12. Manual smoke: switch tabs rapidly → results cached, no flicker. Type filter → debounced refetch. Create a room → list refreshes.
|
||||
|
||||
## 6. Risk register
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| `NavigatorSearchEvent` arrives unsolicited (server-side push) — query wouldn't update | The `accept` filter checks the result's code matches the current tabCode, so only matching events update the query. Unsolicited results to a non-active tab are ignored (acceptable — when the user switches to that tab, the cache is empty and a fresh query fires). |
|
||||
| Removing `useNavigatorActions` breaks an import we missed | Type-checker catches it. The P1 grep showed only Navigator-internal consumers use it. |
|
||||
| Removing the `isLoading`/`isReady`/`needsInit`/`needsSearch` flags from `navigatorUiStore` (they're now derivable from query state) — too aggressive? | KEEP them in P2. Only `searchResult` ownership moves. Future cleanup can remove the obsolete lifecycle flags once we're sure nothing reads them. |
|
||||
| Debounce timing on search input | 300ms is standard; if it feels laggy the user can lower it later — pure UX tune |
|
||||
|
||||
## 7. Plan (executable)
|
||||
|
||||
### Task 1: Add UI store state + actions (TDD)
|
||||
|
||||
**Files**: `src/hooks/navigator/navigatorUiStore.ts`, `src/hooks/navigator/navigatorUiStore.test.ts`
|
||||
|
||||
- [ ] Add `currentTabCode: string` (initial `''`) and `currentFilter: string` (initial `''`) to `NavigatorUiState`
|
||||
- [ ] Add `setTab(code: string): void` and `setFilter(value: string): void` to `NavigatorUiActions`
|
||||
- [ ] `setTab(code)` sets `{ currentTabCode: code, currentFilter: '' }` (atomic reset on tab change)
|
||||
- [ ] `setFilter(value)` sets `{ currentFilter: value }` (no tab side-effect)
|
||||
- [ ] Update test file: 3 new cases — `setTab` updates tab and resets filter; `setFilter` updates filter without touching tab; idempotent `setTab` on same code resets filter to '' regardless
|
||||
- [ ] `yarn test --run src/hooks/navigator/navigatorUiStore.test.ts` → green
|
||||
- [ ] Commit: `feat(navigator): add currentTabCode + currentFilter to UI store (P2 prep)`
|
||||
|
||||
### Task 2: Create `useNavigatorSearch` query hook (TDD)
|
||||
|
||||
**Files**: `src/hooks/navigator/useNavigatorSearch.ts`, `src/hooks/navigator/useNavigatorSearch.test.tsx`
|
||||
|
||||
Implement per §2.1 + §2.7 above. 7 test cases.
|
||||
|
||||
The test will need: `QueryClientProvider` wrapper, mock for `NavigatorSearchComposer` (probably already in mock), `NavigatorSearchEvent` dispatch with parser.result.code matching/non-matching.
|
||||
|
||||
- [ ] Commit: `feat(navigator): useNavigatorSearch query hook (P2 core)`
|
||||
|
||||
### Task 3: Strip search ownership from `useNavigatorStore` + `useNavigatorData` + remove `useNavigatorActions`
|
||||
|
||||
**Files**: `useNavigatorStore.ts`, `useNavigatorData.ts`, `useNavigatorActions.ts` (DELETE), `useNavigatorUiState.ts`, `index.ts`
|
||||
|
||||
- [ ] Remove `searchResult` state + `setSearchResult` from `useNavigatorStore`
|
||||
- [ ] Remove `NavigatorSearchEvent` listener from `useNavigatorStore`
|
||||
- [ ] Remove `sendSearch` and `reloadCurrentSearch` from `useNavigatorStore` return
|
||||
- [ ] Remove `setLoading` calls inside `useNavigatorStore`
|
||||
- [ ] Remove `topLevelContextRef` and `searchResultRef` (no longer used after sendSearch/reload removal)
|
||||
- [ ] In `NavigatorMetadataEvent` handler, add `useNavigatorUiStore.getState().setTab(parser.topLevelContexts[0]?.code ?? '')` after `setTopLevelContext(...)` — seeds the query when contexts arrive
|
||||
- [ ] Remove `searchResult` from `useNavigatorData` destructure + return
|
||||
- [ ] DELETE `src/hooks/navigator/useNavigatorActions.ts`
|
||||
- [ ] Update `useNavigatorUiState.ts` to expose `currentTabCode` + `currentFilter` per-key selectors
|
||||
- [ ] Update `src/hooks/navigator/index.ts` to remove `useNavigatorActions` export, add `useNavigatorSearch` export
|
||||
- [ ] Update `useNavigatorStore.test.tsx` smoke test: 2 cases that expected `searchResult` in data shape or `sendSearch/reloadCurrentSearch` in actions shape — update accordingly (or just remove the "useNavigatorActions returns ..." test entirely)
|
||||
- [ ] Verify typecheck: ONLY consumer-side errors expected (NavigatorView still references the old API). Hook files clean.
|
||||
- [ ] Commit: `refactor(navigator): remove search ownership from useNavigatorStore`
|
||||
|
||||
### Task 4: Migrate `NavigatorView.tsx` + `NavigatorSearchView.tsx`
|
||||
|
||||
**Files**: `src/components/navigator/NavigatorView.tsx`, `src/components/navigator/views/search/NavigatorSearchView.tsx`
|
||||
|
||||
- [ ] In `NavigatorView`:
|
||||
- Import `useNavigatorSearch`
|
||||
- Replace `useNavigatorData` destructure of `searchResult` with `useNavigatorSearch()` call returning `{ searchResult, isFetching }`
|
||||
- Drop `useNavigatorActions` import + destructure (it's gone)
|
||||
- Drop the 4 lifecycle `useEffect` blocks (needsSearch / needsInit-init / markReady / reloadCurrentSearch); the new flow:
|
||||
- Keep the `NavigatorInitComposer` on first `isVisible` — still needed for metadata
|
||||
- Tab clicks call `useNavigatorUiStore.getState().setTab(context.code)`
|
||||
- linkTracker `case 'search'`: `store.setTab(parts[2]); store.setFilter(parts[3] ?? ''); store.show();` (no more `pendingSearch` ref)
|
||||
- Replace `<NitroCard.Content isLoading={ isLoading }>` with `isFetching` from the query
|
||||
- Drop the `pendingSearch` ref
|
||||
- [ ] In `NavigatorSearchView`:
|
||||
- Read `currentFilter` from `useNavigatorUiState` for the initial input value
|
||||
- Local `useState` for the text being typed (mirrors the store value)
|
||||
- Debounce: `useEffect` with 300ms timer calling `useNavigatorUiStore.getState().setFilter(text)`
|
||||
- Remove all `useNavigatorActions` references — the search submit happens via store, query refires automatically
|
||||
- [ ] `yarn typecheck` clean
|
||||
- [ ] `yarn test --run` green
|
||||
- [ ] `yarn lint:hooks` clean
|
||||
- [ ] Commit: `feat(navigator): drive search via TanStack Query + setTab/setFilter UI store actions`
|
||||
|
||||
### Task 5: PR
|
||||
|
||||
- [ ] Push branch
|
||||
- [ ] Open PR against `duckietm:Dev`: `feat(navigator): TanStack Query for search (P2)`
|
||||
@@ -0,0 +1,71 @@
|
||||
# Navigator — Room Settings "Base" tab: stacked-label layout
|
||||
|
||||
**Date:** 2026-05-31
|
||||
**Component:** Nitro-V3 client
|
||||
**File:** `src/components/navigator/views/room-settings/NavigatorRoomSettingsBasicTabView.tsx`
|
||||
**Type:** Layout-only refactor (no logic / data-flow change)
|
||||
|
||||
## Problem
|
||||
|
||||
The Base tab uses a horizontal two-column row layout: a fixed-width label on the
|
||||
left, the control on the right. In the narrow room-settings panel the label column
|
||||
is too tight, so multi-word Italian labels ("Visitatori massimi", "Impostazioni
|
||||
scambio") wrap onto two lines and look broken. An earlier fix replaced dead
|
||||
Bootstrap `col-3` classes with `w-1/4 shrink-0`, which stopped the crushing but
|
||||
still leaves the labels cramped and occasionally wrapping.
|
||||
|
||||
The other five room-settings tabs (Access, Rights, VIP/Chat, Mod, Misc) already use
|
||||
idiomatic vertical/grouped layouts. Base is the outlier.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt the **stacked-label** pattern (chosen from three mockup options — A stacked,
|
||||
B sectioned cards, C wider label column). Each field becomes a vertical block: bold
|
||||
label on top, full-width control below, validation message underneath. This mirrors
|
||||
the sibling **Access** tab's existing `<Column gap={1}>` + `<Text bold>` shape, so
|
||||
the two tabs become visually consistent and labels can never wrap.
|
||||
|
||||
## Layout
|
||||
|
||||
Every field → its own `<Column gap={1}>` block:
|
||||
|
||||
```tsx
|
||||
<Column gap={ 1 }>
|
||||
<Text bold>{ LocalizeText('navigator.roomname') }</Text>
|
||||
<input className="form-control form-control-sm" value={ roomName } … onBlur={ saveRoomName } />
|
||||
{ (roomName.length < ROOM_NAME_MIN_LENGTH) &&
|
||||
<Text bold small variant="danger">{ LocalizeText('navigator.roomsettings.roomnameismandatory') }</Text> }
|
||||
</Column>
|
||||
```
|
||||
|
||||
Field-by-field:
|
||||
|
||||
- **Nome stanza** — stacked block, mandatory-name validation preserved.
|
||||
- **Descrizione** — stacked block, `<textarea>` full width.
|
||||
- **Categoria** — stacked block, `<select>` from `categories`.
|
||||
- **Visitatori massimi** — stacked block, `<select>` from `GetMaxVisitorsList`.
|
||||
- **Impostazioni scambio** — stacked block, 3-option `<select>`.
|
||||
- **Tag** — one "Tag" label, then the two tag inputs side-by-side in a
|
||||
`<Flex gap={1}>`, each `fullWidth`, each keeping its own length/type validation.
|
||||
- **allow_walkthrough / allow_underpass** — remain inline `checkbox + label` rows;
|
||||
remove the empty `<Base className="w-1/4 shrink-0" />` spacers that only existed
|
||||
to align with the old label column.
|
||||
- **Delete link** — unchanged at the bottom.
|
||||
|
||||
## Explicit non-goals
|
||||
|
||||
- No change to `handleChange` field names or values.
|
||||
- No change to validation thresholds (`ROOM_NAME_MIN_LENGTH=3`,
|
||||
`ROOM_NAME_MAX_LENGTH=60`, `DESC_MAX_LENGTH=255`, `TAGS_MAX_LENGTH=15`).
|
||||
- No change to save-on-blur handlers (`saveRoomName`, `saveRoomDescription`,
|
||||
`saveTags`), the `RoomSettingsSaveErrorEvent` subscription, or `deleteRoom`.
|
||||
- No change to field order or any localization key.
|
||||
- No change to the other five tabs.
|
||||
- The `w-1/4 shrink-0` utility classes added in the prior fix are removed (labels
|
||||
are full-width now).
|
||||
|
||||
## Risk
|
||||
|
||||
Single-file, JSX-only diff. No test covers this view, so no test impact. Manual
|
||||
check: open Room Settings → Base, confirm no label wraps, all controls full width,
|
||||
validation still appears, save-on-blur still fires.
|
||||
@@ -1,271 +0,0 @@
|
||||
{
|
||||
"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...",
|
||||
"catalog.gift_wrapping.gift_sent": "Done!",
|
||||
"messenger.offline.delivered": "Sent while you were offline",
|
||||
"messenger.typing": "%FRIEND_NAME% is typing..."
|
||||
}
|
||||
@@ -0,0 +1,781 @@
|
||||
{
|
||||
// ------------------------------------------------------------------------
|
||||
// Friendlist
|
||||
// ------------------------------------------------------------------------
|
||||
'friendlist.search': 'Search friends',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Purse / Currency
|
||||
// ------------------------------------------------------------------------
|
||||
'purse.seasonal.currency.101': 'doekoes',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Widget: furni chooser
|
||||
// ------------------------------------------------------------------------
|
||||
'widget.chooser.checkall': 'Select furni',
|
||||
'widget.chooser.btn.pickall': 'pick up the selected items!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Game center
|
||||
// ------------------------------------------------------------------------
|
||||
'gamecenter.players': 'Players',
|
||||
'gamecenter.players.2to6': '2 to 6 players',
|
||||
'gamecenter.players.2to8': '2 to 8 players',
|
||||
'gamecenter.players.4to12': '4 to 12 players',
|
||||
'gamecenter.players.single': 'Single player',
|
||||
'gamecenter.players.score': 'Score:',
|
||||
'gamecenter.players.theme': 'Theme:',
|
||||
'gamecenter.players.winner': 'Winner!',
|
||||
|
||||
// Game descriptions
|
||||
'gamecenter.battleball.description': 'BattleBall is a colorful game in which you must color more surfaces than your opponent. Items appear randomly and give you unique powers to boost your chances. Tactics, skill and quick decisions are the key to victory. Will you become the champion of BattleBall?',
|
||||
'gamecenter.tombrunner.description': 'This treasure hunter is determined to find as many old coins as possible while running through ancient corridors and leaping over enormous cracks. On your journey through this endless 3D running game you will also encounter unstable and fragile bridges. Find out how long you can survive.',
|
||||
'gamecenter.flappybirds.description': 'Flappy Bird is an arcade-style game in which we control the Faby bird, which moves to the right. It is your task to guide Faby through pipes that have equal gaps placed at random heights.',
|
||||
'gamecenter.bargame.description': 'Show off your skills by working in the best bar of the hotel and serving the best drinks to the most demanding customers. Try to be the waiter with the best skills, delivering glasses to win the game and demonstrate your abilities at working with cocktails.',
|
||||
'gamecenter.roombuildergame.description': 'Are you good at building rooms? Do you have enough imagination? Take on the challenge and build a themed room in under 6 minutes. The nicest room wins!',
|
||||
|
||||
// Game center: voting
|
||||
'gamecenter.vote.description': 'Vote on the rooms',
|
||||
'gamecenter.vote.room.made.by': 'Room made by',
|
||||
'gamecenter.vote.room.bestihaveseen': 'This is the nicest room I have ever seen!',
|
||||
'gamecenter.vote.room.nice': 'Fine room, nicely done.',
|
||||
'gamecenter.vote.room.normal': 'An OK room, not bad and not super cool.',
|
||||
'gamecenter.vote.room.couldbebetter': 'This could have been a lot better',
|
||||
'gamecenter.vote.room.bad': 'Help, where is the exit, my eyes hurt!',
|
||||
'gamecenter.vote.room.wait': 'The other players are now voting on your room, please wait!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Wired furniture
|
||||
// ------------------------------------------------------------------------
|
||||
'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.tooltip.select.tile': 'Select tile',
|
||||
'wiredfurni.tooltip.remove.tile': 'Deselect tile',
|
||||
'wiredfurni.tooltip.remove.5x5_tile': 'select 5x5 tiles',
|
||||
'wiredfurni.tooltip.remove.clear_tile': 'Remove all selections',
|
||||
'wiredfurni.params.furni_neighborhood.group.user': 'Players',
|
||||
'wiredfurni.params.furni_neighborhood.group.furni': 'Furni',
|
||||
'wiredfurni.params.selector_option.bot': 'No BOTs',
|
||||
'wiredfurni.params.selector_option.pet': 'No Pets',
|
||||
|
||||
// Wired furniture: badge received
|
||||
'wiredfurni.badgereceived.title': 'Badge received!',
|
||||
'wiredfurni.badgereceived.body': 'You just received a new badge! Check it out in your inventory!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Notifications
|
||||
// ------------------------------------------------------------------------
|
||||
'notification.badge.received': 'New badge!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Settings widget
|
||||
// ------------------------------------------------------------------------
|
||||
'widget.settings.general': 'Default',
|
||||
'widget.settings.general.title': 'Adjust the default nitro settings',
|
||||
'widget.settings.volume': 'Volume',
|
||||
'widget.settings.interface': 'Interface',
|
||||
'widget.settings.interface.title': 'Adjust the settings for the interface',
|
||||
'widget.settings.interface.fps.automatic': 'Set FPS to unlimited',
|
||||
'widget.settings.interface.fps.warning': 'Setting FPS to unlimited can cause performance problems!',
|
||||
'widget.settings.interface.secondary': 'Change the window header color',
|
||||
'widget.settings.interface.reset': 'Reset header color to default',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Room widgets: chat + youtube
|
||||
// ------------------------------------------------------------------------
|
||||
'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',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Catalog
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
// Catalog: general
|
||||
'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
|
||||
'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 contents?',
|
||||
'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 subpage',
|
||||
'catalog.admin.order': 'Order',
|
||||
'catalog.admin.visible': 'Visible',
|
||||
'catalog.admin.enabled': 'Enabled',
|
||||
|
||||
// Catalog admin: offer editor
|
||||
'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
|
||||
'catalog.trophies.title': 'Trophies',
|
||||
'catalog.trophies.write.hint': 'Write a text for the trophy before buying',
|
||||
'catalog.trophies.inscription': 'Trophy inscription',
|
||||
'catalog.trophies.inscription.placeholder': 'Write the text that will appear on the trophy...',
|
||||
|
||||
// Catalog: pets
|
||||
'catalog.pets.show.colors': 'Show colors',
|
||||
'catalog.pets.choose.color': 'Choose color',
|
||||
'catalog.pets.choose.breed': 'Choose breed',
|
||||
'catalog.pets.back.breeds': '← Breeds',
|
||||
|
||||
// Catalog: name prefix editor
|
||||
'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 and then choose the color. Advances automatically.',
|
||||
'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': 'Buy',
|
||||
|
||||
// Catalog: gift wrapping
|
||||
'catalog.gift_wrapping.gift_sent': 'Done!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Group forum
|
||||
// ------------------------------------------------------------------------
|
||||
'groupforum.list.tab.most_active': 'Most active topics',
|
||||
'groupforum.list.tab.my_forums': 'My group forums',
|
||||
'groupforum.list.no_forums': 'There are no forums',
|
||||
'groupforum.view.threads': 'Number of topics',
|
||||
'groupforum.thread.pin': 'Pin topic',
|
||||
'groupforum.thread.unpin': 'Unpin topic',
|
||||
'groupforum.thread.lock': 'Lock topic',
|
||||
'groupforum.thread.unlock': 'Unlock topic',
|
||||
'groupforum.thread.hide': 'Hide topic',
|
||||
'groupforum.thread.restore': 'Make topic visible again',
|
||||
'groupforum.thread.delete': 'Delete topic + 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 be deleted too!',
|
||||
'groupforum.view.no_threads': 'There are currently no active topics',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: window
|
||||
// ------------------------------------------------------------------------
|
||||
'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',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: user info
|
||||
// ------------------------------------------------------------------------
|
||||
'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 accounts',
|
||||
'modtools.userinfo.registrationAgeInMinutes': 'Registered',
|
||||
'modtools.userinfo.userClassification': 'Rank',
|
||||
'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 in the hotel',
|
||||
'modtools.userinfo.presence.offline': 'Offline',
|
||||
'modtools.userinfo.presence.offline.title': 'Offline when panel opened',
|
||||
'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',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: room info
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.roominfo.title': 'Room info',
|
||||
'modtools.roominfo.refresh': 'Refresh room info',
|
||||
'modtools.roominfo.loading': 'Loading…',
|
||||
'modtools.roominfo.owner.here': 'Owner present',
|
||||
'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 info of %username%',
|
||||
'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 doorbell',
|
||||
'modtools.roominfo.moderate.rename': 'Change room name',
|
||||
'modtools.roominfo.moderate.message.placeholder': 'Required message sent along with the action…',
|
||||
'modtools.roominfo.moderate.send.caution': 'Send caution',
|
||||
'modtools.roominfo.moderate.send.alert': 'Send alert',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: user message
|
||||
// ------------------------------------------------------------------------
|
||||
'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 sees it as a moderator message.',
|
||||
'modtools.user.message.empty': 'Empty',
|
||||
'modtools.user.message.chars': '%count% characters',
|
||||
'modtools.user.message.send': 'Send message',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: mod action
|
||||
// ------------------------------------------------------------------------
|
||||
'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': 'Write a message to the user',
|
||||
'modtools.user.modaction.error.no.permission.alert': 'You have insufficient rights',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: user visits
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.user.visits.title': 'User visits',
|
||||
'modtools.user.visits.recent': 'Recently 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',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: chatlog
|
||||
// ------------------------------------------------------------------------
|
||||
'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',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: tickets
|
||||
// ------------------------------------------------------------------------
|
||||
'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': 'Picked up by',
|
||||
'modtools.tickets.empty.open': 'No open reports',
|
||||
'modtools.tickets.empty.mine': 'No reports picked up by you',
|
||||
'modtools.tickets.empty.picked': 'No picked-up reports',
|
||||
'modtools.tickets.action.pick': 'Pick up',
|
||||
'modtools.tickets.action.handle': 'Handle',
|
||||
'modtools.tickets.action.release': 'Release',
|
||||
'modtools.tickets.issue.title': 'Resolve report #%issueId%',
|
||||
'modtools.tickets.issue.label': 'Report #%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': 'Reporter',
|
||||
'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': 'Put back in queue',
|
||||
'modtools.tickets.cfh.chatlog.title': 'Report #%issueId% chatlog',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Login
|
||||
// ------------------------------------------------------------------------
|
||||
'login.username': 'What is your habbo name',
|
||||
'login.forgot_password': 'Forgot password?',
|
||||
|
||||
// First-time visitors card
|
||||
'nitro.login.firsttime.title': 'First time here?',
|
||||
'nitro.login.firsttime.text': 'Don\'t have a habbo account yet?',
|
||||
'nitro.login.firsttime.link': 'You can create one here',
|
||||
'nitro.login.card.title': 'Sign in to habbo',
|
||||
|
||||
// Server status checks
|
||||
'nitro.login.server.offline.short': 'The game server is not running right now. Try again in a moment.',
|
||||
'nitro.login.server.offline.long': 'The game server is not running right now, so no new accounts can be created. Try again in a moment.',
|
||||
'nitro.login.server.checking': 'Checking…',
|
||||
'nitro.login.server.retry': 'Try again',
|
||||
|
||||
// Registration flow
|
||||
'nitro.login.register.title': 'habbo details',
|
||||
'nitro.login.register.next': 'Next',
|
||||
'nitro.login.register.finish': 'Finish',
|
||||
'nitro.login.register.creating': 'Creating…',
|
||||
'nitro.login.register.intro.credentials': 'Let\'s create your account. Enter your email address and choose a password — we\'ll check that this email is not already in use.',
|
||||
'nitro.login.register.intro.avatar': 'Now it\'s time to create your own habbo character! Start by choosing your habbo name.',
|
||||
'nitro.login.register.intro.room': 'Last step — choose a starter room, or skip this and make your own room later.',
|
||||
'nitro.login.register.confirm.label': 'Confirm password',
|
||||
'nitro.login.register.username.placeholder': 'HabboName',
|
||||
'nitro.login.register.hotlooks.count': '%count% looks available',
|
||||
'nitro.login.register.hotlooks.none': 'No looks loaded',
|
||||
'nitro.login.register.room.skip.title': 'Fine — I\'ll make my own rooms',
|
||||
'nitro.login.register.room.skip.description': 'Skip this and start with an empty hotel inventory.',
|
||||
'nitro.login.register.room.loading': 'Loading rooms…',
|
||||
'nitro.login.register.room.error': 'Could not load room options. You can still skip this step.',
|
||||
'nitro.login.register.success': 'Welcome aboard, %username%! Your account is ready — log in below with the password you just chose.',
|
||||
|
||||
// Forgot password
|
||||
'nitro.login.forgot.title': 'Reset password',
|
||||
'nitro.login.forgot.email.label': 'Email address',
|
||||
'nitro.login.forgot.send': 'Send email',
|
||||
'nitro.login.forgot.success': 'Email sent! If an account is linked to this address, you\'ll find a reset link in your inbox shortly (check your spam if you see nothing within a minute).',
|
||||
|
||||
// Login errors (validation + transport)
|
||||
'nitro.login.error.missing_credentials': 'Enter both your habbo name and password.',
|
||||
'nitro.login.error.invalid_credentials': 'Invalid habbo name or password.',
|
||||
'nitro.login.error.too_many_attempts': 'Too many attempts. Try again in %seconds%s.',
|
||||
'nitro.login.error.turnstile': 'Complete the security check.',
|
||||
'nitro.login.error.server_offline': 'The game server is not running. Try again later.',
|
||||
'nitro.login.error.login_unreachable': 'Cannot reach the login service. Try again.',
|
||||
'nitro.login.error.register_failed': 'Cannot create your account.',
|
||||
'nitro.login.error.register_unreachable': 'Cannot reach the registration service.',
|
||||
'nitro.login.error.forgot_failed': 'Cannot send a reset email right now.',
|
||||
'nitro.login.error.forgot_unreachable': 'Cannot reach the password reset service.',
|
||||
'nitro.login.error.missing_fields': 'Fill in all fields.',
|
||||
'nitro.login.error.invalid_email': 'Enter a valid email address.',
|
||||
'nitro.login.error.password_too_short': 'Your password must be at least 8 characters long.',
|
||||
'nitro.login.error.password_mismatch': 'Passwords do not match.',
|
||||
'nitro.login.error.email_taken': 'This email address is already in use.',
|
||||
'nitro.login.error.missing_username': 'Choose a habbo name.',
|
||||
'nitro.login.error.username_length': 'The habbo name must be 3–16 characters.',
|
||||
'nitro.login.error.username_taken': 'This habbo name is already in use.',
|
||||
'nitro.login.error.missing_email': 'Enter your email address.',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Inventory
|
||||
// ------------------------------------------------------------------------
|
||||
'inventory.effects.activate': 'Use effect',
|
||||
'inventory.effects.remove': 'remove effect',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Loading screen — boot-stage labels read by App.tsx (taskLabel)
|
||||
// ------------------------------------------------------------------------
|
||||
'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...',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Housekeeping
|
||||
// ------------------------------------------------------------------------
|
||||
'housekeeping.title': 'Housekeeping',
|
||||
'housekeeping.mode.light': 'Light',
|
||||
|
||||
// Housekeeping: tabs
|
||||
'housekeeping.tab.dashboard': 'Dashboard',
|
||||
'housekeeping.tab.users': 'Users',
|
||||
'housekeeping.tab.rooms': 'Rooms',
|
||||
'housekeeping.tab.economy': 'Economy',
|
||||
'housekeeping.tab.audit': 'Audit log',
|
||||
|
||||
// Housekeeping: confirm + status
|
||||
'housekeeping.confirm.title': 'Confirm action',
|
||||
'housekeeping.confirm.proceed': 'Proceed',
|
||||
'housekeeping.confirm.cancel': 'Cancel',
|
||||
'housekeeping.status.dismiss': 'Dismiss',
|
||||
|
||||
// Housekeeping: action status
|
||||
'housekeeping.action.pending': 'Action in progress…',
|
||||
'housekeeping.action.success': 'Action completed',
|
||||
'housekeeping.action.error': 'Action failed',
|
||||
'housekeeping.action.reset_password.done': 'Password reset — new password below.',
|
||||
|
||||
// Housekeeping: generated password card
|
||||
'housekeeping.password.title': '%username% (#%id%) · new password',
|
||||
'housekeeping.password.value_label': 'Generated password',
|
||||
'housekeeping.password.copy': 'Copy',
|
||||
'housekeeping.password.copied': 'Copied',
|
||||
'housekeeping.password.copy_failed': 'Copy failed',
|
||||
'housekeeping.password.dismiss': 'Dismiss',
|
||||
'housekeeping.password.hint': 'Share this with the user outside the hotel. It is shown once — close this card when you\'re done; the password will never be displayed again.',
|
||||
|
||||
// Housekeeping: errors
|
||||
'housekeeping.error.invalid_input': 'Invalid input — check the user ID and the entered value.',
|
||||
'housekeeping.error.user_not_found': 'User not found.',
|
||||
'housekeeping.error.user_offline': 'User is offline — this action only works on online users.',
|
||||
'housekeeping.error.target_unkickable': 'This user cannot be kicked.',
|
||||
'housekeeping.error.ban_failed': 'Ban could not be applied — the server refused the request.',
|
||||
'housekeeping.error.no_active_ban': 'No active ban to lift for this user.',
|
||||
'housekeeping.error.rank_not_found': 'Rank not found — choose a rank that exists in permission_ranks.',
|
||||
'housekeeping.error.db_failed': 'Database error — see the emulator log for the SQL exception.',
|
||||
'housekeeping.error.hash_failed': 'Could not hash the new password — SHA-256 not available on this JVM.',
|
||||
'housekeeping.error.room_not_found': 'Room not found.',
|
||||
'housekeeping.error.room_action_failed': 'Room action could not be applied.',
|
||||
'housekeeping.error.new_owner_not_found': 'New owner not found.',
|
||||
'housekeeping.error.economy_failed': 'Economy action could not be applied — check the user ID and the amount.',
|
||||
'housekeeping.error.alert_empty': 'Hotel alert may not be empty.',
|
||||
|
||||
// Housekeeping: actions
|
||||
'housekeeping.action.ban_h': 'Ban %h%h',
|
||||
'housekeeping.action.mute_min': 'Mute %m%m',
|
||||
'housekeeping.action.trade_lock_h': 'Trade lock %h%h',
|
||||
'housekeeping.action.kick': 'Kick',
|
||||
'housekeeping.action.unban': 'Lift ban',
|
||||
'housekeeping.action.force_disconnect': 'Disconnect',
|
||||
'housekeeping.action.set_rank': 'Set rank',
|
||||
'housekeeping.action.reset_password': 'Reset password',
|
||||
|
||||
// Housekeeping: user panel
|
||||
'housekeeping.user.search.placeholder': 'Search by username…',
|
||||
'housekeeping.user.search.button': 'Search',
|
||||
'housekeeping.user.clear': 'Clear selection',
|
||||
'housekeeping.user.none': 'No user selected — search above to pick one.',
|
||||
'housekeeping.user.not_found': 'User not found.',
|
||||
'housekeeping.user.credits': 'Credits',
|
||||
'housekeeping.user.duckets': 'Duckets / pixels',
|
||||
'housekeeping.user.diamonds': 'Diamonds',
|
||||
'housekeeping.user.audit_hint': 'All actions are recorded in the audit log tab.',
|
||||
'housekeeping.user.live.label': 'Live (in current room)',
|
||||
'housekeeping.user.live.kick': 'Kick',
|
||||
'housekeeping.user.live.mute_2m': 'Mute 2m',
|
||||
'housekeeping.user.live.mute_10m': 'Mute 10m',
|
||||
'housekeeping.user.live.ban_h': 'Ban 1h',
|
||||
'housekeeping.user.live.ban_d': 'Ban 1d',
|
||||
|
||||
// Housekeeping: room panel
|
||||
'housekeeping.room.search.placeholder': 'Room ID…',
|
||||
'housekeeping.room.search.button': 'Search',
|
||||
'housekeeping.room.clear': 'Clear selection',
|
||||
'housekeeping.room.none': 'No room selected — enter an ID above.',
|
||||
'housekeeping.room.not_found': 'Room not found.',
|
||||
'housekeeping.room.open': 'Open',
|
||||
'housekeeping.room.close': 'Close',
|
||||
'housekeeping.room.mute_min': 'Mute %m%m',
|
||||
'housekeeping.room.kick_all': 'Kick everyone',
|
||||
'housekeeping.room.kick_all.confirm': 'Kick every user currently in the room?',
|
||||
'housekeeping.room.delete': 'Delete room',
|
||||
'housekeeping.room.delete.confirm': 'Permanently delete this room and all its furni?',
|
||||
'housekeeping.room.transfer': 'Transfer',
|
||||
'housekeeping.room.transfer.label': 'Transfer ownership',
|
||||
'housekeeping.room.transfer.new_owner': 'New owner ID',
|
||||
|
||||
// Housekeeping: economy
|
||||
'housekeeping.economy.select_user': 'Pick a user in the Users tab first.',
|
||||
'housekeeping.economy.target': 'Target: %username% (#%id%)',
|
||||
'housekeeping.economy.give_credits': 'Give credits',
|
||||
'housekeeping.economy.give_duckets': 'Give duckets',
|
||||
'housekeeping.economy.give_diamonds': 'Give diamonds',
|
||||
'housekeeping.economy.grant_item': 'Grant item',
|
||||
'housekeeping.economy.grant_item.label': 'Grant catalog item',
|
||||
'housekeeping.economy.item_id': 'Item ID',
|
||||
'housekeeping.economy.item_quantity': 'Quantity',
|
||||
'housekeeping.economy.set_hc_days': 'Set HC days',
|
||||
|
||||
// Housekeeping: hotel-wide alert
|
||||
'housekeeping.hotel.alert.label': 'Hotel-wide alert',
|
||||
'housekeeping.hotel.alert.placeholder': 'Message broadcast to every connected user…',
|
||||
'housekeeping.hotel.alert.send': 'Send to hotel',
|
||||
'housekeeping.hotel.alert.confirm': 'Broadcast a %count%-character alert to every connected user?',
|
||||
|
||||
// Housekeeping: dashboard
|
||||
'housekeeping.dashboard.title': 'Overview',
|
||||
'housekeeping.dashboard.refresh': 'Refresh',
|
||||
'housekeeping.dashboard.loading': 'Loading dashboard…',
|
||||
'housekeeping.dashboard.unavailable': 'Dashboard unavailable — check the admin endpoint.',
|
||||
'housekeeping.dashboard.online': 'Online',
|
||||
'housekeeping.dashboard.total_users': '%count% total',
|
||||
'housekeeping.dashboard.rooms_active': 'Active rooms',
|
||||
'housekeeping.dashboard.total_rooms': '%count% total',
|
||||
'housekeeping.dashboard.peak_today': 'Peak today',
|
||||
'housekeeping.dashboard.peak_alltime': 'All-time peak %count%',
|
||||
'housekeeping.dashboard.pending_tickets': 'Tickets',
|
||||
'housekeeping.dashboard.sanctions_24h': '%count% sanctions / 24h',
|
||||
'housekeeping.dashboard.server': 'Server',
|
||||
'housekeeping.dashboard.recent_sanctions': 'Recent sanctions',
|
||||
'housekeeping.dashboard.recent_lookups': 'Recent lookups',
|
||||
|
||||
// Housekeeping: audit log
|
||||
'housekeeping.audit.title': 'Audit log',
|
||||
'housekeeping.audit.refresh': 'Refresh',
|
||||
'housekeeping.audit.filter.all': 'All',
|
||||
'housekeeping.audit.filter.users': 'Users',
|
||||
'housekeeping.audit.filter.rooms': 'Rooms',
|
||||
'housekeeping.audit.filter.hotel': 'Hotel',
|
||||
'housekeeping.audit.search.placeholder': 'Search actor / target / action…',
|
||||
'housekeeping.audit.empty': 'No audit entries yet.',
|
||||
'housekeeping.audit.no_match': 'No entries match the current filters.',
|
||||
|
||||
// Housekeeping: shared fields
|
||||
'housekeeping.field.reason': 'Reason',
|
||||
'housekeeping.field.reason.placeholder': 'Free-text reason (optional)',
|
||||
'housekeeping.field.duration': 'Duration',
|
||||
'housekeeping.reason.default': 'No reason given.',
|
||||
|
||||
// Housekeeping: context menu
|
||||
'housekeeping.menu.send_to_hk': 'Send to housekeeping',
|
||||
|
||||
// Housekeeping: bulk actions
|
||||
'housekeeping.bulk.done': 'Bulk done',
|
||||
'housekeeping.bulk.success': 'All bulk actions succeeded.',
|
||||
'housekeeping.bulk.partial': 'Bulk completed with some failures.',
|
||||
'housekeeping.bulk.failed': 'Every bulk action failed.',
|
||||
'housekeeping.bulk.confirm': 'Apply %action% to %count% selected users?',
|
||||
'housekeeping.bulk.label': '%count% selected',
|
||||
'housekeeping.bulk.clear': 'Clear selection',
|
||||
'housekeeping.bulk.apply': 'Apply to selection',
|
||||
|
||||
// Housekeeping: telemetry
|
||||
'housekeeping.telemetry.title': 'Telemetry',
|
||||
'housekeeping.telemetry.empty': 'No actions observed yet.',
|
||||
'housekeeping.telemetry.reset': 'Reset statistics',
|
||||
|
||||
// Housekeeping: live room session
|
||||
'housekeeping.live.no_room': 'No active room session.',
|
||||
'housekeeping.live.kicked': 'Kicked from the room.',
|
||||
'housekeeping.live.banned': 'Banned from the room.',
|
||||
'housekeeping.live.muted': 'Muted in the room.',
|
||||
|
||||
// Housekeeping: validation
|
||||
'housekeeping.validation.empty_username': 'Username may not be empty.',
|
||||
'housekeeping.validation.invalid_user_id': 'Invalid user ID.',
|
||||
'housekeeping.validation.invalid_room_id': 'Invalid room ID.',
|
||||
'housekeeping.validation.invalid_amount': 'Invalid amount.',
|
||||
'housekeeping.validation.amount_too_large': 'Amount exceeds the safety limit.',
|
||||
'housekeeping.validation.empty_reason': 'Reason may not be empty.',
|
||||
'housekeeping.validation.invalid_hours': 'Invalid duration in hours.',
|
||||
'housekeeping.validation.invalid_rank': 'Invalid rank — must be between 1 and 12.',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Fortune Wheel
|
||||
// ------------------------------------------------------------------------
|
||||
'wheel.title': 'Fortune Wheel',
|
||||
'wheel.free.today': 'You have %count% free spins today!',
|
||||
'wheel.extra': 'Extra spins: %count%',
|
||||
'wheel.spin': 'SPIN',
|
||||
'wheel.buy': 'Buy spin',
|
||||
'wheel.winners': 'Latest winners',
|
||||
'wheel.winners.empty': 'No winners yet',
|
||||
'wheel.win.title': 'You won!',
|
||||
'wheel.win.jackpot': '★ Jackpot ★',
|
||||
'wheel.win.nothing': 'Better luck next time!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Soundboard
|
||||
// ------------------------------------------------------------------------
|
||||
'soundboard.title': 'Soundboard',
|
||||
'soundboard.empty': 'No sounds available',
|
||||
'soundboard.lastplayed': 'Played by %user%',
|
||||
'soundboard.room.setting.desc': 'Let people in this room play sound effects',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Radio
|
||||
// ------------------------------------------------------------------------
|
||||
'radio.title': 'Radio',
|
||||
'radio.empty': 'No stations',
|
||||
'radio.error': 'Couldn\'t load stations',
|
||||
'radio.stop': 'Stop',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Rare Values
|
||||
// ------------------------------------------------------------------------
|
||||
'rarevalues.title': 'Rare Values',
|
||||
'rarevalues.loading': 'Loading values…',
|
||||
'rarevalues.empty': 'No rares found',
|
||||
'rarevalues.infostand.label': 'Value:',
|
||||
|
||||
// Rare Values: editor
|
||||
'rarevalues.editor.tab': 'Edit',
|
||||
'rarevalues.editor.type': 'Type',
|
||||
'rarevalues.editor.value': 'Value',
|
||||
'rarevalues.editor.weight': 'Chance',
|
||||
'rarevalues.editor.label': 'Label',
|
||||
'rarevalues.editor.save': 'Save',
|
||||
'rarevalues.editor.add': '+ Add prize',
|
||||
'rarevalues.editor.remove': 'Remove prize',
|
||||
'rarevalues.editor.cat.item': 'Furni (ID)',
|
||||
'rarevalues.editor.cat.spin': 'Extra spins',
|
||||
'rarevalues.editor.cat.nothing': 'Nothing',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Chat commands: client
|
||||
// ------------------------------------------------------------------------
|
||||
'chatcmd.client.shake': 'Shake the room',
|
||||
'chatcmd.client.rotate': 'Rotate the room',
|
||||
'chatcmd.client.zoom': 'Zoom in/out',
|
||||
'chatcmd.client.flip': 'Reset zoom',
|
||||
'chatcmd.client.iddqd': 'Turn the room upside down',
|
||||
'chatcmd.client.screenshot': 'Screenshot of the room',
|
||||
'chatcmd.client.togglefps': 'Toggle FPS',
|
||||
'chatcmd.client.laugh': 'Laugh (VIP)',
|
||||
'chatcmd.client.kiss': 'Blow a kiss (VIP)',
|
||||
'chatcmd.client.jump': 'Jump (VIP)',
|
||||
'chatcmd.client.idle': 'Go idle',
|
||||
'chatcmd.client.sign': 'Show sign',
|
||||
'chatcmd.client.furni': 'Furni chooser',
|
||||
'chatcmd.client.chooser': 'User chooser',
|
||||
'chatcmd.client.floor': 'Floor editor',
|
||||
'chatcmd.client.pickall': 'Pick up all furni',
|
||||
'chatcmd.client.ejectall': 'Eject all furni',
|
||||
'chatcmd.client.settings': 'Room settings',
|
||||
'chatcmd.client.info': 'Client info',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Me-menu settings + User account settings window
|
||||
// ------------------------------------------------------------------------
|
||||
'usersettings.tab.general': "General",
|
||||
'usersettings.tab.themes': "Themes",
|
||||
'memenu.settings.other.place.multiple.objects': "Place multiple objects",
|
||||
'memenu.settings.other.skip.purchase.confirmation': "Skip purchase confirmation",
|
||||
'memenu.settings.other.enable.chat.window': "Enable chat window",
|
||||
'memenu.settings.other.catalog.classic.style': "Catalog: classic style",
|
||||
'usersettings.open.title': "User settings",
|
||||
'usersettings.open.subtitle': "Password & account",
|
||||
'usersettings.themes.custom': "Custom theme",
|
||||
'usersettings.themes.default_option': "Default (no theme)",
|
||||
'usersettings.themes.active_pieces': "Active pieces",
|
||||
'usersettings.themes.invalid': "Theme invalid or unreachable — using the default.",
|
||||
'usersettings.themes.none': "No themes available. Add a folder in custom-themes/ on the server.",
|
||||
'usersettings.title': "User Settings",
|
||||
'usersettings.account.label': "My account",
|
||||
'usersettings.guest': "Guest",
|
||||
'usersettings.subtitle': "Manage your account and security",
|
||||
'usersettings.menu.section': "Account",
|
||||
'usersettings.menu.password.title': "Reset password",
|
||||
'usersettings.menu.password.desc': "Change the password used to log in.",
|
||||
'usersettings.menu.email.title': "Change email",
|
||||
'usersettings.menu.email.desc': "Update the email address on your account.",
|
||||
'usersettings.menu.username.title': "Change username",
|
||||
'usersettings.menu.username.desc': "Pick a new name. You'll need to log in again.",
|
||||
'usersettings.menu.soon.title': "More coming soon",
|
||||
'usersettings.menu.soon.desc': "Two-factor authentication and more.",
|
||||
'usersettings.password.hint': "Use at least %count% characters. Mix upper & lowercase, numbers and symbols for a stronger password.",
|
||||
'usersettings.email.hint': "For security we ask you to confirm your current password before changing the email on your account.",
|
||||
'usersettings.username.hint': "Renaming will log you out and you can only rename again after 30 days. Make sure your friends know your new name!",
|
||||
'usersettings.field.current_password': "Current password",
|
||||
'usersettings.field.new_password': "New password",
|
||||
'usersettings.field.retype_password': "Retype new password",
|
||||
'usersettings.field.new_email': "New email address",
|
||||
'usersettings.field.new_username': "New username",
|
||||
'usersettings.username.rules': "%min%-%max% characters. Letters, numbers, dot, underscore and dash only.",
|
||||
'usersettings.strength.weak': "Weak",
|
||||
'usersettings.strength.fair': "Fair",
|
||||
'usersettings.strength.good': "Good",
|
||||
'usersettings.strength.strong': "Strong",
|
||||
'usersettings.aria.show_password': "Show password",
|
||||
'usersettings.aria.hide_password': "Hide password",
|
||||
'usersettings.btn.cancel': "Cancel",
|
||||
'usersettings.btn.saving': "Saving…",
|
||||
'usersettings.btn.save_password': "Save password",
|
||||
'usersettings.btn.save_email': "Save email",
|
||||
'usersettings.btn.renaming': "Renaming…",
|
||||
'usersettings.btn.rename': "Rename me",
|
||||
'usersettings.error.fields_required': "All fields are required.",
|
||||
'usersettings.error.password_min': "Password must be at least %count% characters.",
|
||||
'usersettings.error.password_long': "Password is too long.",
|
||||
'usersettings.error.password_mismatch': "New passwords do not match.",
|
||||
'usersettings.error.password_same': "New password must be different from the current password.",
|
||||
'usersettings.error.not_authenticated': "You are not authenticated. Please log in again.",
|
||||
'usersettings.error.network': "Could not reach the server. Please try again.",
|
||||
'usersettings.error.request_failed': "Request failed (%status%).",
|
||||
'usersettings.error.email_long': "Email address is too long.",
|
||||
'usersettings.error.email_invalid': "Please enter a valid email address.",
|
||||
'usersettings.error.username_length': "Username must be between %min% and %max% characters.",
|
||||
'usersettings.error.username_invalid': "Username may only contain letters, numbers, dot, underscore and dash.",
|
||||
'usersettings.error.username_same': "New username must be different from the current one.",
|
||||
'usersettings.success.password': "Password updated successfully.",
|
||||
'usersettings.success.email': "Email updated successfully.",
|
||||
'usersettings.success.username': "Username updated. Please log in again with your new name.",
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Messenger (offline delivery + typing)
|
||||
// ------------------------------------------------------------------------
|
||||
'messenger.offline.delivered': 'Sent while you were offline',
|
||||
'messenger.typing': '%FRIEND_NAME% is typing...',
|
||||
}
|
||||
@@ -0,0 +1,781 @@
|
||||
{
|
||||
// ------------------------------------------------------------------------
|
||||
// Friendlist
|
||||
// ------------------------------------------------------------------------
|
||||
'friendlist.search': 'Cerca amici',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Purse / Currency
|
||||
// ------------------------------------------------------------------------
|
||||
'purse.seasonal.currency.101': 'doekoes',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Widget: furni chooser
|
||||
// ------------------------------------------------------------------------
|
||||
'widget.chooser.checkall': 'Seleziona arredi',
|
||||
'widget.chooser.btn.pickall': 'raccogli gli oggetti selezionati!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Game center
|
||||
// ------------------------------------------------------------------------
|
||||
'gamecenter.players': 'Giocatori',
|
||||
'gamecenter.players.2to6': 'Da 2 a 6 giocatori',
|
||||
'gamecenter.players.2to8': 'Da 2 a 8 giocatori',
|
||||
'gamecenter.players.4to12': 'Da 4 a 12 giocatori',
|
||||
'gamecenter.players.single': 'Giocatore singolo',
|
||||
'gamecenter.players.score': 'Punteggio:',
|
||||
'gamecenter.players.theme': 'Tema:',
|
||||
'gamecenter.players.winner': 'Vincitore!',
|
||||
|
||||
// Game descriptions
|
||||
'gamecenter.battleball.description': 'BattleBall è un gioco colorato in cui devi colorare più superfici del tuo avversario. Gli oggetti compaiono in modo casuale e ti danno poteri unici per aumentare le tue possibilità. Tattica, abilità e decisioni rapide sono la chiave della vittoria. Diventerai il campione di BattleBall?',
|
||||
'gamecenter.tombrunner.description': 'Questo cacciatore di tesori è determinato a trovare quante più monete antiche possibile mentre corre attraverso corridoi millenari e salta enormi crepe. Nel tuo viaggio attraverso questo infinito gioco di corsa in 3D incontrerai anche ponti instabili e fragili. Scopri quanto a lungo riesci a sopravvivere.',
|
||||
'gamecenter.flappybirds.description': 'Flappy Bird è un gioco in stile arcade in cui controlliamo l\'uccellino Faby, che si muove verso destra. Il tuo compito è guidare Faby attraverso i tubi che hanno aperture uguali poste ad altezze casuali.',
|
||||
'gamecenter.bargame.description': 'Mostra le tue abilità lavorando nel miglior bar dell\'hotel e servendo i migliori drink ai clienti più esigenti. Cerca di essere il cameriere con le migliori abilità, consegnando i bicchieri per vincere la partita e dimostrare la tua bravura con i cocktail.',
|
||||
'gamecenter.roombuildergame.description': 'Sei bravo a costruire stanze? Hai abbastanza fantasia? Accetta la sfida e costruisci una stanza a tema in meno di 6 minuti. La stanza più bella vince!',
|
||||
|
||||
// Game center: voting
|
||||
'gamecenter.vote.description': 'Vota le stanze',
|
||||
'gamecenter.vote.room.made.by': 'Stanza creata da',
|
||||
'gamecenter.vote.room.bestihaveseen': 'Questa è la stanza più bella che abbia mai visto!',
|
||||
'gamecenter.vote.room.nice': 'Bella stanza, ben fatto.',
|
||||
'gamecenter.vote.room.normal': 'Una stanza OK, né male né eccezionale.',
|
||||
'gamecenter.vote.room.couldbebetter': 'Si poteva fare molto meglio',
|
||||
'gamecenter.vote.room.bad': 'Aiuto, dov\'è l\'uscita, mi fanno male gli occhi!',
|
||||
'gamecenter.vote.room.wait': 'Gli altri giocatori stanno votando la tua stanza, attendi!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Wired furniture
|
||||
// ------------------------------------------------------------------------
|
||||
'wiredfurni.params.requireall.2': 'Se uno degli arredi selezionati ha un avatar',
|
||||
'wiredfurni.params.requireall.3': 'Se tutti gli arredi selezionati hanno avatar sopra di essi',
|
||||
'wiredfurni.tooltip.select.tile': 'Seleziona riquadro',
|
||||
'wiredfurni.tooltip.remove.tile': 'Deseleziona riquadro',
|
||||
'wiredfurni.tooltip.remove.5x5_tile': 'seleziona riquadri 5x5',
|
||||
'wiredfurni.tooltip.remove.clear_tile': 'Rimuovi tutte le selezioni',
|
||||
'wiredfurni.params.furni_neighborhood.group.user': 'Giocatori',
|
||||
'wiredfurni.params.furni_neighborhood.group.furni': 'Arredi',
|
||||
'wiredfurni.params.selector_option.bot': 'Nessun BOT',
|
||||
'wiredfurni.params.selector_option.pet': 'Nessun animale',
|
||||
|
||||
// Wired furniture: badge received
|
||||
'wiredfurni.badgereceived.title': 'Distintivo ricevuto!',
|
||||
'wiredfurni.badgereceived.body': 'Hai appena ricevuto un nuovo distintivo! Guardalo nel tuo inventario!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Notifications
|
||||
// ------------------------------------------------------------------------
|
||||
'notification.badge.received': 'Nuovo distintivo!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Settings widget
|
||||
// ------------------------------------------------------------------------
|
||||
'widget.settings.general': 'Predefinito',
|
||||
'widget.settings.general.title': 'Modifica le impostazioni predefinite di nitro',
|
||||
'widget.settings.volume': 'Volume',
|
||||
'widget.settings.interface': 'Interfaccia',
|
||||
'widget.settings.interface.title': 'Modifica le impostazioni dell\'interfaccia',
|
||||
'widget.settings.interface.fps.automatic': 'Imposta FPS su illimitato',
|
||||
'widget.settings.interface.fps.warning': 'Impostare gli FPS su illimitato può causare problemi di prestazioni!',
|
||||
'widget.settings.interface.secondary': 'Cambia il colore dell\'intestazione della finestra',
|
||||
'widget.settings.interface.reset': 'Ripristina il colore dell\'intestazione predefinito',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Room widgets: chat + youtube
|
||||
// ------------------------------------------------------------------------
|
||||
'widget.room.chat.hide_pets': 'Nascondi animali',
|
||||
'widget.room.chat.hide_avatars': 'Nascondi avatar',
|
||||
'widget.room.chat.hide_balloon': 'Nascondi fumetto',
|
||||
'widget.room.chat.show_balloon': 'Fumetto',
|
||||
'widget.room.chat.clear_history': 'cancella cronologia',
|
||||
'widget.room.youtube.shared': 'YouTube è condiviso',
|
||||
'widget.room.youtube.open_video': 'Apri il video',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Catalog
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
// Catalog: general
|
||||
'catalog.title': 'Catalogo',
|
||||
'catalog.favorites': 'Preferiti',
|
||||
'catalog.favorites.pages': 'Pagine',
|
||||
'catalog.favorites.furni': 'Arredi',
|
||||
'catalog.favorites.empty': 'Nessun preferito',
|
||||
'catalog.favorites.empty.hint': 'Clicca sul cuore sugli arredi o sulla stella sulle pagine per aggiungerli.',
|
||||
|
||||
// Catalog: admin
|
||||
'catalog.admin': 'Gestione',
|
||||
'catalog.admin.new': 'Nuovo',
|
||||
'catalog.admin.root': 'Radice',
|
||||
'catalog.admin.new.root.category': 'Nuova categoria radice',
|
||||
'catalog.admin.edit.root': 'Modifica radice',
|
||||
'catalog.admin.edit': 'Modifica:',
|
||||
'catalog.admin.edit.page': 'Modifica pagina',
|
||||
'catalog.admin.hidden': 'nascosto',
|
||||
'catalog.admin.edit.title': 'Modifica "%name%"',
|
||||
'catalog.admin.show': 'Mostra',
|
||||
'catalog.admin.hide': 'Nascondi',
|
||||
'catalog.admin.delete': 'Elimina',
|
||||
'catalog.admin.delete.title': 'Elimina "%name%"',
|
||||
'catalog.admin.delete.category.confirm': 'Eliminare la categoria "%name%" e tutto il suo contenuto?',
|
||||
'catalog.admin.delete.page': 'Elimina pagina',
|
||||
'catalog.admin.delete.page.confirm': 'Eliminare la pagina "%name%"?',
|
||||
'catalog.admin.delete.offer.confirm': 'Sei sicuro di voler eliminare questa offerta?',
|
||||
'catalog.admin.create': 'Crea',
|
||||
'catalog.admin.save': 'Salva',
|
||||
'catalog.admin.create.subpage': 'Crea sottopagina',
|
||||
'catalog.admin.order': 'Ordine',
|
||||
'catalog.admin.visible': 'Visibile',
|
||||
'catalog.admin.enabled': 'Abilitato',
|
||||
|
||||
// Catalog admin: offer editor
|
||||
'catalog.admin.offer.new': 'Nuova offerta',
|
||||
'catalog.admin.offer.edit': 'Modifica offerta',
|
||||
'catalog.admin.offer.name': 'Nome catalogo',
|
||||
'catalog.admin.offer.general': 'Generale',
|
||||
'catalog.admin.offer.quantity': 'Quantità',
|
||||
'catalog.admin.offer.prices': 'Prezzi',
|
||||
'catalog.admin.offer.credits': 'Crediti',
|
||||
'catalog.admin.offer.points': 'Punti',
|
||||
'catalog.admin.offer.points.type': 'Tipo di punti',
|
||||
'catalog.admin.offer.options': 'Opzioni',
|
||||
'catalog.admin.offer.club.only': 'Solo Club',
|
||||
'catalog.admin.offer.extradata': 'Dati extra (opzionale)....',
|
||||
'catalog.admin.offer.have.offer': 'Multi-sconto (have_offer)',
|
||||
|
||||
// Catalog: trophies
|
||||
'catalog.trophies.title': 'Trofei',
|
||||
'catalog.trophies.write.hint': 'Scrivi un testo per il trofeo prima di acquistare',
|
||||
'catalog.trophies.inscription': 'Iscrizione del trofeo',
|
||||
'catalog.trophies.inscription.placeholder': 'Scrivi il testo che apparirà sul trofeo...',
|
||||
|
||||
// Catalog: pets
|
||||
'catalog.pets.show.colors': 'Mostra colori',
|
||||
'catalog.pets.choose.color': 'Scegli colore',
|
||||
'catalog.pets.choose.breed': 'Scegli razza',
|
||||
'catalog.pets.back.breeds': '← Razze',
|
||||
|
||||
// Catalog: name prefix editor
|
||||
'catalog.prefix.text': 'Testo',
|
||||
'catalog.prefix.text.placeholder': 'Inserisci testo...',
|
||||
'catalog.prefix.icon': 'Icona',
|
||||
'catalog.prefix.icon.remove': 'Rimuovi icona',
|
||||
'catalog.prefix.effect': 'Effetto',
|
||||
'catalog.prefix.color': 'Colore',
|
||||
'catalog.prefix.color.single': '🎨 Singolo',
|
||||
'catalog.prefix.color.per.letter': '🌈 Per lettera',
|
||||
'catalog.prefix.color.hint': 'Seleziona una lettera e poi scegli il colore. Avanza automaticamente.',
|
||||
'catalog.prefix.color.apply.all.title': 'Applica il colore corrente a tutte le lettere',
|
||||
'catalog.prefix.color.apply.all': 'Applica a tutte',
|
||||
'catalog.prefix.color.selected': 'Lettera selezionata:',
|
||||
'catalog.prefix.price': 'Prezzo:',
|
||||
'catalog.prefix.price.amount': '5 Crediti',
|
||||
'catalog.prefix.purchased': '✓ Acquistato!',
|
||||
'catalog.prefix.purchase': 'Acquista',
|
||||
|
||||
// Catalog: gift wrapping
|
||||
'catalog.gift_wrapping.gift_sent': 'Fatto!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Group forum
|
||||
// ------------------------------------------------------------------------
|
||||
'groupforum.list.tab.most_active': 'Argomenti più attivi',
|
||||
'groupforum.list.tab.my_forums': 'I miei forum di gruppo',
|
||||
'groupforum.list.no_forums': 'Non ci sono forum',
|
||||
'groupforum.view.threads': 'Numero di argomenti',
|
||||
'groupforum.thread.pin': 'Fissa argomento',
|
||||
'groupforum.thread.unpin': 'Sblocca argomento',
|
||||
'groupforum.thread.lock': 'Blocca argomento',
|
||||
'groupforum.thread.unlock': 'Sblocca argomento',
|
||||
'groupforum.thread.hide': 'Nascondi argomento',
|
||||
'groupforum.thread.restore': 'Rendi di nuovo visibile l\'argomento',
|
||||
'groupforum.thread.delete': 'Elimina argomento + messaggi',
|
||||
'groupforum.message.hide': 'Nascondi messaggio',
|
||||
'group.forum.enable.caption': 'Abilita/disabilita forum di gruppo',
|
||||
'group.forum.enable.help': 'Se disabiliti il forum di gruppo, verranno eliminati anche tutti i messaggi!',
|
||||
'groupforum.view.no_threads': 'Al momento non ci sono argomenti attivi',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: window
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.window.title': 'Strumenti Mod',
|
||||
'modtools.window.tools.room': 'Strumento stanza',
|
||||
'modtools.window.tools.chatlog': 'Strumento chatlog',
|
||||
'modtools.window.tools.report': 'Strumento segnalazioni',
|
||||
'modtools.window.select.user': 'Seleziona un utente',
|
||||
'modtools.window.no.room': 'Entra prima in una stanza',
|
||||
'modtools.window.user.in_room': 'Ancora in questa stanza',
|
||||
'modtools.window.user.left_room': 'Non più in questa stanza',
|
||||
'modtools.window.user.clear': 'Cancella selezione',
|
||||
'modtools.window.tickets.open': '%count% ticket aperto',
|
||||
'modtools.window.tickets.open.many': '%count% ticket aperti',
|
||||
'modtools.window.section.room': 'Stanza',
|
||||
'modtools.window.section.user': 'Utente',
|
||||
'modtools.window.section.reports': 'Segnalazioni',
|
||||
'modtools.window.user.open_info': 'Apri info',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: user info
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.userinfo.title': 'Info utente: %username%',
|
||||
'modtools.userinfo.userName': 'Nome',
|
||||
'modtools.userinfo.cfhCount': 'CFH',
|
||||
'modtools.userinfo.abusiveCfhCount': 'CFH abusivi',
|
||||
'modtools.userinfo.cautionCount': 'Avvertimenti',
|
||||
'modtools.userinfo.banCount': 'Ban',
|
||||
'modtools.userinfo.lastSanctionTime': 'Ultima sanzione',
|
||||
'modtools.userinfo.tradingLockCount': 'Blocchi scambio',
|
||||
'modtools.userinfo.tradingExpiryDate': 'Blocco scade',
|
||||
'modtools.userinfo.minutesSinceLastLogin': 'Ultimo accesso',
|
||||
'modtools.userinfo.lastPurchaseDate': 'Ultimo acquisto',
|
||||
'modtools.userinfo.primaryEmailAddress': 'Email',
|
||||
'modtools.userinfo.identityRelatedBanCount': 'Account bannati',
|
||||
'modtools.userinfo.registrationAgeInMinutes': 'Registrato',
|
||||
'modtools.userinfo.userClassification': 'Grado',
|
||||
'modtools.userinfo.refresh': 'Aggiorna info utente',
|
||||
'modtools.userinfo.presence.in_room': 'In stanza',
|
||||
'modtools.userinfo.presence.in_room.title': 'Nella stanza che stai osservando',
|
||||
'modtools.userinfo.presence.online': 'Online',
|
||||
'modtools.userinfo.presence.online.title': 'Online nell\'hotel',
|
||||
'modtools.userinfo.presence.offline': 'Offline',
|
||||
'modtools.userinfo.presence.offline.title': 'Offline all\'apertura del pannello',
|
||||
'modtools.userinfo.section.account': 'Account',
|
||||
'modtools.userinfo.section.activity': 'Attività',
|
||||
'modtools.userinfo.section.sanctions': 'Sanzioni',
|
||||
'modtools.userinfo.section.trading': 'Scambi',
|
||||
'modtools.userinfo.button.room.chat': 'Chat stanza',
|
||||
'modtools.userinfo.button.send.message': 'Invia messaggio',
|
||||
'modtools.userinfo.button.room.visits': 'Visite stanze',
|
||||
'modtools.userinfo.button.mod.action': 'Azione mod',
|
||||
'modtools.userinfo.stat.cfh': 'CFH',
|
||||
'modtools.userinfo.stat.cautions': 'Avvertimenti',
|
||||
'modtools.userinfo.stat.bans': 'Ban',
|
||||
'modtools.userinfo.stat.trade.locks': 'Blocchi scambio',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: room info
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.roominfo.title': 'Info stanza',
|
||||
'modtools.roominfo.refresh': 'Aggiorna info stanza',
|
||||
'modtools.roominfo.loading': 'Caricamento…',
|
||||
'modtools.roominfo.owner.here': 'Proprietario presente',
|
||||
'modtools.roominfo.owner.away': 'Proprietario assente',
|
||||
'modtools.roominfo.owner.title.here': 'Il proprietario della stanza è attualmente all\'interno',
|
||||
'modtools.roominfo.owner.title.away': 'Il proprietario della stanza NON è all\'interno',
|
||||
'modtools.roominfo.stat.users': 'Utenti',
|
||||
'modtools.roominfo.stat.owner': 'Proprietario',
|
||||
'modtools.roominfo.owner.open': 'Apri le info di %username%',
|
||||
'modtools.roominfo.button.visit': 'Visita stanza',
|
||||
'modtools.roominfo.button.chatlog': 'Chatlog',
|
||||
'modtools.roominfo.moderate.title': 'Modera stanza',
|
||||
'modtools.roominfo.moderate.kick': 'Caccia tutti',
|
||||
'modtools.roominfo.moderate.doorbell': 'Abilita campanello',
|
||||
'modtools.roominfo.moderate.rename': 'Cambia nome stanza',
|
||||
'modtools.roominfo.moderate.message.placeholder': 'Messaggio obbligatorio inviato insieme all\'azione…',
|
||||
'modtools.roominfo.moderate.send.caution': 'Invia avvertimento',
|
||||
'modtools.roominfo.moderate.send.alert': 'Invia avviso',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: user message
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.user.message.title': 'Invia messaggio',
|
||||
'modtools.user.message.recipient': 'Messaggio a',
|
||||
'modtools.user.message.label': 'Messaggio',
|
||||
'modtools.user.message.placeholder': 'Scrivi qualcosa di utile — l\'utente lo vede come un messaggio del moderatore.',
|
||||
'modtools.user.message.empty': 'Vuoto',
|
||||
'modtools.user.message.chars': '%count% caratteri',
|
||||
'modtools.user.message.send': 'Invia messaggio',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: mod action
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.user.modaction.title': 'Azione mod: %username%',
|
||||
'modtools.user.modaction.sanctioning': 'Sanzionamento',
|
||||
'modtools.user.modaction.step.topic': '1. Argomento CFH',
|
||||
'modtools.user.modaction.step.topic.placeholder': 'Seleziona un argomento…',
|
||||
'modtools.user.modaction.step.sanction': '2. Sanzione',
|
||||
'modtools.user.modaction.step.sanction.placeholder': 'Seleziona una sanzione…',
|
||||
'modtools.user.modaction.step.message': '3. Messaggio personalizzato',
|
||||
'modtools.user.modaction.step.message.optional': '(opzionale — sostituisce il predefinito)',
|
||||
'modtools.user.modaction.message.placeholder': 'Lascia vuoto per usare il messaggio predefinito dell\'argomento',
|
||||
'modtools.user.modaction.preview': 'Anteprima',
|
||||
'modtools.user.modaction.button.default': 'Sanzione predefinita',
|
||||
'modtools.user.modaction.button.apply': 'Applica sanzione',
|
||||
'modtools.user.modaction.error.no.topic': 'Devi selezionare un argomento CFH',
|
||||
'modtools.user.modaction.error.no.action': 'Devi selezionare un argomento CFH e una sanzione',
|
||||
'modtools.user.modaction.error.no.permission': 'Non hai il permesso di farlo',
|
||||
'modtools.user.modaction.error.no.message': 'Scrivi un messaggio all\'utente',
|
||||
'modtools.user.modaction.error.no.permission.alert': 'Non hai diritti sufficienti',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: user visits
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.user.visits.title': 'Visite utente',
|
||||
'modtools.user.visits.recent': 'Stanze visitate di recente',
|
||||
'modtools.user.visits.entries.one': '%count% voce',
|
||||
'modtools.user.visits.entries.many': '%count% voci',
|
||||
'modtools.user.visits.empty': 'Nessuna visita recente',
|
||||
'modtools.user.visits.time': 'Ora',
|
||||
'modtools.user.visits.room': 'Nome stanza',
|
||||
'modtools.user.visits.action': 'Azione',
|
||||
'modtools.user.visits.visit': 'Visita',
|
||||
'modtools.user.visits.visit.title': 'Visita stanza',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: chatlog
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.user.chatlog.title': 'Chatlog utente',
|
||||
'modtools.user.chatlog.title.with': 'Chatlog utente: %username%',
|
||||
'modtools.user.chatlog.loading': 'Caricamento chatlog…',
|
||||
'modtools.room.chatlog.title': 'Chatlog stanza',
|
||||
'modtools.chatlog.column.time': 'Ora',
|
||||
'modtools.chatlog.column.user': 'Utente',
|
||||
'modtools.chatlog.column.message': 'Messaggio',
|
||||
'modtools.chatlog.empty': 'Nessun messaggio',
|
||||
'modtools.chatlog.visit': 'Visita',
|
||||
'modtools.chatlog.tools': 'Strumenti',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: tickets
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.tickets.title': 'Ticket',
|
||||
'modtools.tickets.tab.open': 'Aperti',
|
||||
'modtools.tickets.tab.mine': 'Miei',
|
||||
'modtools.tickets.tab.picked': 'Tutti presi',
|
||||
'modtools.tickets.column.type': 'Tipo',
|
||||
'modtools.tickets.column.reported': 'Segnalato',
|
||||
'modtools.tickets.column.opened': 'Aperto',
|
||||
'modtools.tickets.column.picker': 'Preso in carico da',
|
||||
'modtools.tickets.empty.open': 'Nessuna segnalazione aperta',
|
||||
'modtools.tickets.empty.mine': 'Nessuna segnalazione presa in carico da te',
|
||||
'modtools.tickets.empty.picked': 'Nessuna segnalazione presa in carico',
|
||||
'modtools.tickets.action.pick': 'Prendi in carico',
|
||||
'modtools.tickets.action.handle': 'Gestisci',
|
||||
'modtools.tickets.action.release': 'Rilascia',
|
||||
'modtools.tickets.issue.title': 'Risolvi segnalazione #%issueId%',
|
||||
'modtools.tickets.issue.label': 'Segnalazione #%issueId%',
|
||||
'modtools.tickets.issue.details': 'Dettagli',
|
||||
'modtools.tickets.issue.field.source': 'Origine',
|
||||
'modtools.tickets.issue.field.category': 'Categoria',
|
||||
'modtools.tickets.issue.field.description': 'Descrizione',
|
||||
'modtools.tickets.issue.field.caller': 'Segnalatore',
|
||||
'modtools.tickets.issue.field.reported': 'Segnalato',
|
||||
'modtools.tickets.issue.chatlog.view': 'Visualizza chatlog',
|
||||
'modtools.tickets.issue.chatlog.close': 'Chiudi chatlog',
|
||||
'modtools.tickets.issue.resolve.heading': 'Risolvi come',
|
||||
'modtools.tickets.issue.resolve.resolved': 'Risolto',
|
||||
'modtools.tickets.issue.resolve.useless': 'Inutile',
|
||||
'modtools.tickets.issue.resolve.abusive': 'Abusivo',
|
||||
'modtools.tickets.issue.release': 'Rimetti in coda',
|
||||
'modtools.tickets.cfh.chatlog.title': 'Chatlog segnalazione #%issueId%',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Login
|
||||
// ------------------------------------------------------------------------
|
||||
'login.username': 'Qual è il tuo nome habbo',
|
||||
'login.forgot_password': 'Password dimenticata?',
|
||||
|
||||
// First-time visitors card
|
||||
'nitro.login.firsttime.title': 'È la prima volta qui?',
|
||||
'nitro.login.firsttime.text': 'Non hai ancora un account habbo?',
|
||||
'nitro.login.firsttime.link': 'Puoi crearne uno qui',
|
||||
'nitro.login.card.title': 'Accedi a habbo',
|
||||
|
||||
// Server status checks
|
||||
'nitro.login.server.offline.short': 'Il server di gioco al momento non è attivo. Riprova tra poco.',
|
||||
'nitro.login.server.offline.long': 'Il server di gioco al momento non è attivo, quindi non è possibile creare nuovi account. Riprova tra poco.',
|
||||
'nitro.login.server.checking': 'Verifica in corso…',
|
||||
'nitro.login.server.retry': 'Riprova',
|
||||
|
||||
// Registration flow
|
||||
'nitro.login.register.title': 'Dati habbo',
|
||||
'nitro.login.register.next': 'Avanti',
|
||||
'nitro.login.register.finish': 'Completa',
|
||||
'nitro.login.register.creating': 'Creazione in corso…',
|
||||
'nitro.login.register.intro.credentials': 'Creiamo il tuo account. Inserisci il tuo indirizzo email e scegli una password — verificheremo che questa email non sia già in uso.',
|
||||
'nitro.login.register.intro.avatar': 'Ora è il momento di creare il tuo personaggio habbo! Inizia scegliendo il tuo nome habbo.',
|
||||
'nitro.login.register.intro.room': 'Ultimo passaggio — scegli una stanza iniziale, oppure salta e crea la tua stanza più tardi.',
|
||||
'nitro.login.register.confirm.label': 'Conferma password',
|
||||
'nitro.login.register.username.placeholder': 'NomeHabbo',
|
||||
'nitro.login.register.hotlooks.count': '%count% look disponibili',
|
||||
'nitro.login.register.hotlooks.none': 'Nessun look caricato',
|
||||
'nitro.login.register.room.skip.title': 'Va bene — creerò le mie stanze',
|
||||
'nitro.login.register.room.skip.description': 'Salta questo passaggio e inizia con un inventario hotel vuoto.',
|
||||
'nitro.login.register.room.loading': 'Caricamento stanze…',
|
||||
'nitro.login.register.room.error': 'Impossibile caricare le opzioni delle stanze. Puoi comunque saltare questo passaggio.',
|
||||
'nitro.login.register.success': 'Benvenuto a bordo, %username%! Il tuo account è pronto — accedi qui sotto con la password che hai appena scelto.',
|
||||
|
||||
// Forgot password
|
||||
'nitro.login.forgot.title': 'Reimposta password',
|
||||
'nitro.login.forgot.email.label': 'Indirizzo email',
|
||||
'nitro.login.forgot.send': 'Invia email',
|
||||
'nitro.login.forgot.success': 'Email inviata! Se a questo indirizzo è associato un account, troverai presto un link per il reset nella tua casella di posta (controlla lo spam se non vedi nulla entro un minuto).',
|
||||
|
||||
// Login errors (validation + transport)
|
||||
'nitro.login.error.missing_credentials': 'Inserisci sia il tuo nome habbo che la password.',
|
||||
'nitro.login.error.invalid_credentials': 'Nome habbo o password non validi.',
|
||||
'nitro.login.error.too_many_attempts': 'Troppi tentativi. Riprova tra %seconds%s.',
|
||||
'nitro.login.error.turnstile': 'Completa il controllo di sicurezza.',
|
||||
'nitro.login.error.server_offline': 'Il server di gioco non è attivo. Riprova più tardi.',
|
||||
'nitro.login.error.login_unreachable': 'Impossibile raggiungere il servizio di accesso. Riprova.',
|
||||
'nitro.login.error.register_failed': 'Impossibile creare il tuo account.',
|
||||
'nitro.login.error.register_unreachable': 'Impossibile raggiungere il servizio di registrazione.',
|
||||
'nitro.login.error.forgot_failed': 'Impossibile inviare ora un\'email di reset.',
|
||||
'nitro.login.error.forgot_unreachable': 'Impossibile raggiungere il servizio di reset password.',
|
||||
'nitro.login.error.missing_fields': 'Compila tutti i campi.',
|
||||
'nitro.login.error.invalid_email': 'Inserisci un indirizzo email valido.',
|
||||
'nitro.login.error.password_too_short': 'La tua password deve contenere almeno 8 caratteri.',
|
||||
'nitro.login.error.password_mismatch': 'Le password non corrispondono.',
|
||||
'nitro.login.error.email_taken': 'Questo indirizzo email è già in uso.',
|
||||
'nitro.login.error.missing_username': 'Scegli un nome habbo.',
|
||||
'nitro.login.error.username_length': 'Il nome habbo deve contenere 3–16 caratteri.',
|
||||
'nitro.login.error.username_taken': 'Questo nome habbo è già in uso.',
|
||||
'nitro.login.error.missing_email': 'Inserisci il tuo indirizzo email.',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Inventory
|
||||
// ------------------------------------------------------------------------
|
||||
'inventory.effects.activate': 'Usa effetto',
|
||||
'inventory.effects.remove': 'rimuovi effetto',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Loading screen — boot-stage labels read by App.tsx (taskLabel)
|
||||
// ------------------------------------------------------------------------
|
||||
'loading.task.session': 'Verifica della sessione...',
|
||||
'loading.task.renderer': 'Inizializzazione del renderer...',
|
||||
'loading.task.assets': 'Caricamento risorse di gioco...',
|
||||
'loading.task.localization': 'Caricamento traduzioni...',
|
||||
'loading.task.avatar': 'Caricamento guardaroba...',
|
||||
'loading.task.sounds': 'Caricamento suoni...',
|
||||
'loading.task.startsession': 'Avvio della sessione...',
|
||||
'loading.task.userdata': 'Caricamento dati utente...',
|
||||
'loading.task.rooms': 'Caricamento stanze...',
|
||||
'loading.task.engine': 'Caricamento motore grafico...',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Housekeeping
|
||||
// ------------------------------------------------------------------------
|
||||
'housekeeping.title': 'Gestione',
|
||||
'housekeeping.mode.light': 'Chiaro',
|
||||
|
||||
// Housekeeping: tabs
|
||||
'housekeeping.tab.dashboard': 'Dashboard',
|
||||
'housekeeping.tab.users': 'Utenti',
|
||||
'housekeeping.tab.rooms': 'Stanze',
|
||||
'housekeeping.tab.economy': 'Economia',
|
||||
'housekeeping.tab.audit': 'Registro',
|
||||
|
||||
// Housekeeping: confirm + status
|
||||
'housekeeping.confirm.title': 'Conferma azione',
|
||||
'housekeeping.confirm.proceed': 'Procedi',
|
||||
'housekeeping.confirm.cancel': 'Annulla',
|
||||
'housekeeping.status.dismiss': 'Chiudi',
|
||||
|
||||
// Housekeeping: action status
|
||||
'housekeeping.action.pending': 'Azione in corso…',
|
||||
'housekeeping.action.success': 'Azione completata',
|
||||
'housekeeping.action.error': 'Azione fallita',
|
||||
'housekeeping.action.reset_password.done': 'Password reimpostata — nuova password qui sotto.',
|
||||
|
||||
// Housekeeping: generated password card
|
||||
'housekeeping.password.title': '%username% (#%id%) · nuova password',
|
||||
'housekeeping.password.value_label': 'Password generata',
|
||||
'housekeeping.password.copy': 'Copia',
|
||||
'housekeeping.password.copied': 'Copiato',
|
||||
'housekeeping.password.copy_failed': 'Copia fallita',
|
||||
'housekeeping.password.dismiss': 'Chiudi',
|
||||
'housekeeping.password.hint': 'Condividi questa password con l\'utente al di fuori dell\'hotel. Viene mostrata una sola volta — chiudi questa scheda quando hai finito; la password non verrà mai più visualizzata.',
|
||||
|
||||
// Housekeeping: errors
|
||||
'housekeeping.error.invalid_input': 'Input non valido — controlla l\'ID utente e il valore inserito.',
|
||||
'housekeeping.error.user_not_found': 'Utente non trovato.',
|
||||
'housekeeping.error.user_offline': 'L\'utente è offline — questa azione funziona solo su utenti online.',
|
||||
'housekeeping.error.target_unkickable': 'Questo utente non può essere cacciato.',
|
||||
'housekeeping.error.ban_failed': 'Impossibile applicare il ban — il server ha rifiutato la richiesta.',
|
||||
'housekeeping.error.no_active_ban': 'Nessun ban attivo da revocare per questo utente.',
|
||||
'housekeeping.error.rank_not_found': 'Grado non trovato — scegli un grado che esiste in permission_ranks.',
|
||||
'housekeeping.error.db_failed': 'Errore del database — consulta il log dell\'emulatore per l\'eccezione SQL.',
|
||||
'housekeeping.error.hash_failed': 'Impossibile generare l\'hash della nuova password — SHA-256 non disponibile su questa JVM.',
|
||||
'housekeeping.error.room_not_found': 'Stanza non trovata.',
|
||||
'housekeeping.error.room_action_failed': 'Impossibile applicare l\'azione sulla stanza.',
|
||||
'housekeeping.error.new_owner_not_found': 'Nuovo proprietario non trovato.',
|
||||
'housekeeping.error.economy_failed': 'Impossibile applicare l\'azione economica — controlla l\'ID utente e la quantità.',
|
||||
'housekeeping.error.alert_empty': 'L\'avviso hotel non può essere vuoto.',
|
||||
|
||||
// Housekeeping: actions
|
||||
'housekeeping.action.ban_h': 'Ban %h%h',
|
||||
'housekeeping.action.mute_min': 'Muta %m%m',
|
||||
'housekeeping.action.trade_lock_h': 'Blocco scambio %h%h',
|
||||
'housekeeping.action.kick': 'Caccia',
|
||||
'housekeeping.action.unban': 'Revoca ban',
|
||||
'housekeeping.action.force_disconnect': 'Disconnetti',
|
||||
'housekeeping.action.set_rank': 'Imposta grado',
|
||||
'housekeeping.action.reset_password': 'Reimposta password',
|
||||
|
||||
// Housekeeping: user panel
|
||||
'housekeeping.user.search.placeholder': 'Cerca per nome utente…',
|
||||
'housekeeping.user.search.button': 'Cerca',
|
||||
'housekeeping.user.clear': 'Cancella selezione',
|
||||
'housekeeping.user.none': 'Nessun utente selezionato — cerca sopra per sceglierne uno.',
|
||||
'housekeeping.user.not_found': 'Utente non trovato.',
|
||||
'housekeeping.user.credits': 'Crediti',
|
||||
'housekeeping.user.duckets': 'Duckets / pixel',
|
||||
'housekeeping.user.diamonds': 'Diamanti',
|
||||
'housekeeping.user.audit_hint': 'Tutte le azioni vengono registrate nella scheda del registro.',
|
||||
'housekeeping.user.live.label': 'Live (nella stanza corrente)',
|
||||
'housekeeping.user.live.kick': 'Caccia',
|
||||
'housekeeping.user.live.mute_2m': 'Muta 2m',
|
||||
'housekeeping.user.live.mute_10m': 'Muta 10m',
|
||||
'housekeeping.user.live.ban_h': 'Ban 1h',
|
||||
'housekeeping.user.live.ban_d': 'Ban 1g',
|
||||
|
||||
// Housekeeping: room panel
|
||||
'housekeeping.room.search.placeholder': 'ID stanza…',
|
||||
'housekeeping.room.search.button': 'Cerca',
|
||||
'housekeeping.room.clear': 'Cancella selezione',
|
||||
'housekeeping.room.none': 'Nessuna stanza selezionata — inserisci un ID sopra.',
|
||||
'housekeeping.room.not_found': 'Stanza non trovata.',
|
||||
'housekeeping.room.open': 'Apri',
|
||||
'housekeeping.room.close': 'Chiudi',
|
||||
'housekeeping.room.mute_min': 'Muta %m%m',
|
||||
'housekeeping.room.kick_all': 'Caccia tutti',
|
||||
'housekeeping.room.kick_all.confirm': 'Cacciare ogni utente attualmente nella stanza?',
|
||||
'housekeeping.room.delete': 'Elimina stanza',
|
||||
'housekeeping.room.delete.confirm': 'Eliminare definitivamente questa stanza e tutti i suoi arredi?',
|
||||
'housekeeping.room.transfer': 'Trasferisci',
|
||||
'housekeeping.room.transfer.label': 'Trasferisci proprietà',
|
||||
'housekeeping.room.transfer.new_owner': 'ID nuovo proprietario',
|
||||
|
||||
// Housekeeping: economy
|
||||
'housekeeping.economy.select_user': 'Scegli prima un utente nella scheda Utenti.',
|
||||
'housekeeping.economy.target': 'Destinatario: %username% (#%id%)',
|
||||
'housekeeping.economy.give_credits': 'Dai crediti',
|
||||
'housekeeping.economy.give_duckets': 'Dai duckets',
|
||||
'housekeeping.economy.give_diamonds': 'Dai diamanti',
|
||||
'housekeeping.economy.grant_item': 'Assegna oggetto',
|
||||
'housekeeping.economy.grant_item.label': 'Assegna oggetto del catalogo',
|
||||
'housekeeping.economy.item_id': 'ID oggetto',
|
||||
'housekeeping.economy.item_quantity': 'Quantità',
|
||||
'housekeeping.economy.set_hc_days': 'Imposta giorni HC',
|
||||
|
||||
// Housekeeping: hotel-wide alert
|
||||
'housekeeping.hotel.alert.label': 'Avviso a tutto l\'hotel',
|
||||
'housekeeping.hotel.alert.placeholder': 'Messaggio trasmesso a ogni utente connesso…',
|
||||
'housekeeping.hotel.alert.send': 'Invia all\'hotel',
|
||||
'housekeeping.hotel.alert.confirm': 'Trasmettere un avviso di %count% caratteri a ogni utente connesso?',
|
||||
|
||||
// Housekeeping: dashboard
|
||||
'housekeeping.dashboard.title': 'Panoramica',
|
||||
'housekeeping.dashboard.refresh': 'Aggiorna',
|
||||
'housekeeping.dashboard.loading': 'Caricamento dashboard…',
|
||||
'housekeeping.dashboard.unavailable': 'Dashboard non disponibile — controlla l\'endpoint admin.',
|
||||
'housekeeping.dashboard.online': 'Online',
|
||||
'housekeeping.dashboard.total_users': '%count% totali',
|
||||
'housekeeping.dashboard.rooms_active': 'Stanze attive',
|
||||
'housekeeping.dashboard.total_rooms': '%count% totali',
|
||||
'housekeeping.dashboard.peak_today': 'Picco di oggi',
|
||||
'housekeeping.dashboard.peak_alltime': 'Picco di sempre %count%',
|
||||
'housekeeping.dashboard.pending_tickets': 'Ticket',
|
||||
'housekeeping.dashboard.sanctions_24h': '%count% sanzioni / 24h',
|
||||
'housekeeping.dashboard.server': 'Server',
|
||||
'housekeeping.dashboard.recent_sanctions': 'Sanzioni recenti',
|
||||
'housekeeping.dashboard.recent_lookups': 'Ricerche recenti',
|
||||
|
||||
// Housekeeping: audit log
|
||||
'housekeeping.audit.title': 'Registro',
|
||||
'housekeeping.audit.refresh': 'Aggiorna',
|
||||
'housekeeping.audit.filter.all': 'Tutti',
|
||||
'housekeeping.audit.filter.users': 'Utenti',
|
||||
'housekeeping.audit.filter.rooms': 'Stanze',
|
||||
'housekeeping.audit.filter.hotel': 'Hotel',
|
||||
'housekeeping.audit.search.placeholder': 'Cerca esecutore / destinatario / azione…',
|
||||
'housekeeping.audit.empty': 'Nessuna voce di registro ancora.',
|
||||
'housekeeping.audit.no_match': 'Nessuna voce corrisponde ai filtri attuali.',
|
||||
|
||||
// Housekeeping: shared fields
|
||||
'housekeeping.field.reason': 'Motivo',
|
||||
'housekeeping.field.reason.placeholder': 'Motivo libero (opzionale)',
|
||||
'housekeeping.field.duration': 'Durata',
|
||||
'housekeeping.reason.default': 'Nessun motivo fornito.',
|
||||
|
||||
// Housekeeping: context menu
|
||||
'housekeeping.menu.send_to_hk': 'Invia alla gestione',
|
||||
|
||||
// Housekeeping: bulk actions
|
||||
'housekeeping.bulk.done': 'Azione di massa completata',
|
||||
'housekeeping.bulk.success': 'Tutte le azioni di massa sono riuscite.',
|
||||
'housekeeping.bulk.partial': 'Azione di massa completata con alcuni errori.',
|
||||
'housekeeping.bulk.failed': 'Ogni azione di massa è fallita.',
|
||||
'housekeeping.bulk.confirm': 'Applicare %action% a %count% utenti selezionati?',
|
||||
'housekeeping.bulk.label': '%count% selezionati',
|
||||
'housekeeping.bulk.clear': 'Cancella selezione',
|
||||
'housekeeping.bulk.apply': 'Applica alla selezione',
|
||||
|
||||
// Housekeeping: telemetry
|
||||
'housekeeping.telemetry.title': 'Telemetria',
|
||||
'housekeeping.telemetry.empty': 'Nessuna azione osservata ancora.',
|
||||
'housekeeping.telemetry.reset': 'Reimposta statistiche',
|
||||
|
||||
// Housekeeping: live room session
|
||||
'housekeeping.live.no_room': 'Nessuna sessione di stanza attiva.',
|
||||
'housekeeping.live.kicked': 'Cacciato dalla stanza.',
|
||||
'housekeeping.live.banned': 'Bannato dalla stanza.',
|
||||
'housekeeping.live.muted': 'Mutato nella stanza.',
|
||||
|
||||
// Housekeeping: validation
|
||||
'housekeeping.validation.empty_username': 'Il nome utente non può essere vuoto.',
|
||||
'housekeeping.validation.invalid_user_id': 'ID utente non valido.',
|
||||
'housekeeping.validation.invalid_room_id': 'ID stanza non valido.',
|
||||
'housekeeping.validation.invalid_amount': 'Quantità non valida.',
|
||||
'housekeeping.validation.amount_too_large': 'La quantità supera il limite di sicurezza.',
|
||||
'housekeeping.validation.empty_reason': 'Il motivo non può essere vuoto.',
|
||||
'housekeeping.validation.invalid_hours': 'Durata in ore non valida.',
|
||||
'housekeeping.validation.invalid_rank': 'Grado non valido — deve essere compreso tra 1 e 12.',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Fortune Wheel
|
||||
// ------------------------------------------------------------------------
|
||||
'wheel.title': 'Ruota della Fortuna',
|
||||
'wheel.free.today': 'Hai %count% giri gratuiti oggi!',
|
||||
'wheel.extra': 'Giri extra: %count%',
|
||||
'wheel.spin': 'GIRA',
|
||||
'wheel.buy': 'Acquista giro',
|
||||
'wheel.winners': 'Ultimi vincitori',
|
||||
'wheel.winners.empty': 'Ancora nessun vincitore',
|
||||
'wheel.win.title': 'Hai vinto!',
|
||||
'wheel.win.jackpot': '★ Jackpot ★',
|
||||
'wheel.win.nothing': 'Sarà per la prossima!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Soundboard
|
||||
// ------------------------------------------------------------------------
|
||||
'soundboard.title': 'Soundboard',
|
||||
'soundboard.empty': 'Nessun suono disponibile',
|
||||
'soundboard.lastplayed': 'Riprodotto da %user%',
|
||||
'soundboard.room.setting.desc': 'Permetti alle persone in questa stanza di riprodurre effetti sonori',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Radio
|
||||
// ------------------------------------------------------------------------
|
||||
'radio.title': 'Radio',
|
||||
'radio.empty': 'Nessuna stazione',
|
||||
'radio.error': 'Impossibile caricare le stazioni',
|
||||
'radio.stop': 'Ferma',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Rare Values
|
||||
// ------------------------------------------------------------------------
|
||||
'rarevalues.title': 'Valori dei Rari',
|
||||
'rarevalues.loading': 'Caricamento valori…',
|
||||
'rarevalues.empty': 'Nessun raro trovato',
|
||||
'rarevalues.infostand.label': 'Valore:',
|
||||
|
||||
// Rare Values: editor
|
||||
'rarevalues.editor.tab': 'Modifica',
|
||||
'rarevalues.editor.type': 'Tipo',
|
||||
'rarevalues.editor.value': 'Valore',
|
||||
'rarevalues.editor.weight': 'Probabilità',
|
||||
'rarevalues.editor.label': 'Etichetta',
|
||||
'rarevalues.editor.save': 'Salva',
|
||||
'rarevalues.editor.add': '+ Aggiungi premio',
|
||||
'rarevalues.editor.remove': 'Rimuovi premio',
|
||||
'rarevalues.editor.cat.item': 'Arredo (ID)',
|
||||
'rarevalues.editor.cat.spin': 'Giri extra',
|
||||
'rarevalues.editor.cat.nothing': 'Niente',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Chat commands: client
|
||||
// ------------------------------------------------------------------------
|
||||
'chatcmd.client.shake': 'Scuoti la stanza',
|
||||
'chatcmd.client.rotate': 'Ruota la stanza',
|
||||
'chatcmd.client.zoom': 'Zoom avanti/indietro',
|
||||
'chatcmd.client.flip': 'Reimposta zoom',
|
||||
'chatcmd.client.iddqd': 'Capovolgi la stanza',
|
||||
'chatcmd.client.screenshot': 'Screenshot della stanza',
|
||||
'chatcmd.client.togglefps': 'Attiva/disattiva FPS',
|
||||
'chatcmd.client.laugh': 'Ridi (VIP)',
|
||||
'chatcmd.client.kiss': 'Manda un bacio (VIP)',
|
||||
'chatcmd.client.jump': 'Salta (VIP)',
|
||||
'chatcmd.client.idle': 'Vai inattivo',
|
||||
'chatcmd.client.sign': 'Mostra cartello',
|
||||
'chatcmd.client.furni': 'Selettore arredi',
|
||||
'chatcmd.client.chooser': 'Selettore utenti',
|
||||
'chatcmd.client.floor': 'Editor pavimento',
|
||||
'chatcmd.client.pickall': 'Raccogli tutti gli arredi',
|
||||
'chatcmd.client.ejectall': 'Rimuovi tutti gli arredi',
|
||||
'chatcmd.client.settings': 'Impostazioni stanza',
|
||||
'chatcmd.client.info': 'Info client',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Me-menu settings + User account settings window
|
||||
// ------------------------------------------------------------------------
|
||||
'usersettings.tab.general': "Generale",
|
||||
'usersettings.tab.themes': "Temi",
|
||||
'memenu.settings.other.place.multiple.objects': "Posiziona più oggetti",
|
||||
'memenu.settings.other.skip.purchase.confirmation': "Salta la conferma d'acquisto",
|
||||
'memenu.settings.other.enable.chat.window': "Abilita finestra chat",
|
||||
'memenu.settings.other.catalog.classic.style': "Catalogo: stile classico",
|
||||
'usersettings.open.title': "Impostazioni utente",
|
||||
'usersettings.open.subtitle': "Password e account",
|
||||
'usersettings.themes.custom': "Tema personalizzato",
|
||||
'usersettings.themes.default_option': "Predefinito (nessun tema)",
|
||||
'usersettings.themes.active_pieces': "Elementi attivi",
|
||||
'usersettings.themes.invalid': "Tema non valido o non raggiungibile — uso il predefinito.",
|
||||
'usersettings.themes.none': "Nessun tema disponibile. Aggiungi una cartella in custom-themes/ sul server.",
|
||||
'usersettings.title': "Impostazioni utente",
|
||||
'usersettings.account.label': "Il mio account",
|
||||
'usersettings.guest': "Ospite",
|
||||
'usersettings.subtitle': "Gestisci il tuo account e la sicurezza",
|
||||
'usersettings.menu.section': "Account",
|
||||
'usersettings.menu.password.title': "Reimposta password",
|
||||
'usersettings.menu.password.desc': "Cambia la password che usi per accedere.",
|
||||
'usersettings.menu.email.title': "Cambia email",
|
||||
'usersettings.menu.email.desc': "Aggiorna l'indirizzo email del tuo account.",
|
||||
'usersettings.menu.username.title': "Cambia nome utente",
|
||||
'usersettings.menu.username.desc': "Scegli un nuovo nome. Dovrai accedere di nuovo.",
|
||||
'usersettings.menu.soon.title': "Altro in arrivo",
|
||||
'usersettings.menu.soon.desc': "Autenticazione a due fattori e altro.",
|
||||
'usersettings.password.hint': "Usa almeno %count% caratteri. Combina maiuscole e minuscole, numeri e simboli per una password più sicura.",
|
||||
'usersettings.email.hint': "Per sicurezza ti chiediamo di confermare la password attuale prima di cambiare l'email del tuo account.",
|
||||
'usersettings.username.hint': "Cambiando nome verrai disconnesso e potrai rinominarti di nuovo solo dopo 30 giorni. Assicurati che i tuoi amici conoscano il tuo nuovo nome!",
|
||||
'usersettings.field.current_password': "Password attuale",
|
||||
'usersettings.field.new_password': "Nuova password",
|
||||
'usersettings.field.retype_password': "Ripeti la nuova password",
|
||||
'usersettings.field.new_email': "Nuovo indirizzo email",
|
||||
'usersettings.field.new_username': "Nuovo nome utente",
|
||||
'usersettings.username.rules': "%min%-%max% caratteri. Solo lettere, numeri, punto, trattino basso e trattino.",
|
||||
'usersettings.strength.weak': "Debole",
|
||||
'usersettings.strength.fair': "Discreta",
|
||||
'usersettings.strength.good': "Buona",
|
||||
'usersettings.strength.strong': "Forte",
|
||||
'usersettings.aria.show_password': "Mostra password",
|
||||
'usersettings.aria.hide_password': "Nascondi password",
|
||||
'usersettings.btn.cancel': "Annulla",
|
||||
'usersettings.btn.saving': "Salvataggio…",
|
||||
'usersettings.btn.save_password': "Salva password",
|
||||
'usersettings.btn.save_email': "Salva email",
|
||||
'usersettings.btn.renaming': "Rinomina…",
|
||||
'usersettings.btn.rename': "Rinominami",
|
||||
'usersettings.error.fields_required': "Tutti i campi sono obbligatori.",
|
||||
'usersettings.error.password_min': "La password deve contenere almeno %count% caratteri.",
|
||||
'usersettings.error.password_long': "La password è troppo lunga.",
|
||||
'usersettings.error.password_mismatch': "Le nuove password non corrispondono.",
|
||||
'usersettings.error.password_same': "La nuova password deve essere diversa da quella attuale.",
|
||||
'usersettings.error.not_authenticated': "Non sei autenticato. Effettua di nuovo l'accesso.",
|
||||
'usersettings.error.network': "Impossibile raggiungere il server. Riprova.",
|
||||
'usersettings.error.request_failed': "Richiesta non riuscita (%status%).",
|
||||
'usersettings.error.email_long': "L'indirizzo email è troppo lungo.",
|
||||
'usersettings.error.email_invalid': "Inserisci un indirizzo email valido.",
|
||||
'usersettings.error.username_length': "Il nome utente deve contenere tra %min% e %max% caratteri.",
|
||||
'usersettings.error.username_invalid': "Il nome utente può contenere solo lettere, numeri, punto, trattino basso e trattino.",
|
||||
'usersettings.error.username_same': "Il nuovo nome utente deve essere diverso da quello attuale.",
|
||||
'usersettings.success.password': "Password aggiornata con successo.",
|
||||
'usersettings.success.email': "Email aggiornata con successo.",
|
||||
'usersettings.success.username': "Nome utente aggiornato. Accedi di nuovo con il tuo nuovo nome.",
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Messenger (offline delivery + typing)
|
||||
// ------------------------------------------------------------------------
|
||||
'messenger.offline.delivered': 'Inviato mentre eri offline',
|
||||
'messenger.typing': '%FRIEND_NAME% sta scrivendo...',
|
||||
}
|
||||
@@ -0,0 +1,777 @@
|
||||
{
|
||||
// ------------------------------------------------------------------------
|
||||
// Friendlist
|
||||
// ------------------------------------------------------------------------
|
||||
'friendlist.search': 'Zoek vrienden',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Purse / Currency
|
||||
// ------------------------------------------------------------------------
|
||||
'purse.seasonal.currency.101': "doekoe's",
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Widget: furni chooser
|
||||
// ------------------------------------------------------------------------
|
||||
'widget.chooser.checkall': 'Selecteer meubels',
|
||||
'widget.chooser.btn.pickall': 'pak de geselecteerde items op!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Game center
|
||||
// ------------------------------------------------------------------------
|
||||
'gamecenter.players': 'Spelers',
|
||||
'gamecenter.players.2to6': '2 tot 6 spelers',
|
||||
'gamecenter.players.2to8': '2 tot 8 spelers',
|
||||
'gamecenter.players.4to12': '4 tot 12 spelers',
|
||||
'gamecenter.players.single': 'Één speler',
|
||||
'gamecenter.players.score': 'Score:',
|
||||
'gamecenter.players.theme': 'Thema:',
|
||||
'gamecenter.players.winner': 'Winnaar!',
|
||||
|
||||
// Game descriptions
|
||||
'gamecenter.battleball.description': 'BattleBall is een kleurrijk spel waarin je meer oppervlakken moet kleuren dan je tegenstander. Items verschijnen willekeurig en geven je unieke krachten om je kansen te vergroten. Tactiek, vaardigheid en snelle beslissingen zijn de sleutel tot de overwinning. Word jij de kampioen van BattleBall?',
|
||||
'gamecenter.tombrunner.description': 'Deze schatzoeker is vastbesloten om zoveel mogelijk oude munten te vinden terwijl hij door eeuwenoude gangen loopt en over enorme scheuren springt. Op je reis door dit eindeloze 3D-hardloopspel kom je ook onstabiele en kwetsbare bruggen tegen. Ontdek hoe lang je kunt overleven.',
|
||||
'gamecenter.flappybirds.description': 'Flappy Bird is een spel in arcadestijl waarin we de Faby-vogel besturen die naar rechts beweegt. Het is jouw taak om Faby door pijpen te loodsen die op willekeurige hoogte gelijke openingen hebben.',
|
||||
'gamecenter.bargame.description': 'Toon uw vaardigheden door in de beste bar van het hotel te werken en de beste drankjes te serveren aan de meest veeleisende klanten. Probeer de ober te zijn met de beste vaardigheden die glazen aflevert om het spel te winnen en demonstreer je vaardigheden bij het werken met cocktails.',
|
||||
'gamecenter.roombuildergame.description': 'Ben jij goed in het bouwen van kamers? Heb je voldoende fantasie? Ga de strijd aan en bouw in minder dan 6 minuten een kamer rond een thema. De mooiste kamer wint!',
|
||||
|
||||
// Game center: voting
|
||||
'gamecenter.vote.description': 'Stem op de kamers',
|
||||
'gamecenter.vote.room.made.by': 'Kamer gemaakt door',
|
||||
'gamecenter.vote.room.bestihaveseen': 'Dit is de mooiste kamer die ik ooit heb gezien!',
|
||||
'gamecenter.vote.room.nice': 'Prima kamer, leuk gedaan.',
|
||||
'gamecenter.vote.room.normal': 'Een OK kamer, niet slecht en niet super cool.',
|
||||
'gamecenter.vote.room.couldbebetter': 'Dit had veel beter gekund',
|
||||
'gamecenter.vote.room.bad': 'Help waar is de uitgang, mijn ogen doen pijn!',
|
||||
'gamecenter.vote.room.wait': 'De andere spelers zijn nu aan het stemmen op jou kamer, even geduld!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Wired furniture
|
||||
// ------------------------------------------------------------------------
|
||||
'wiredfurni.params.requireall.2': 'Als een van de geselecteerde furni een avatar heeft',
|
||||
'wiredfurni.params.requireall.3': 'Als alle geselecteerde furni avatars op hen hebben',
|
||||
'wiredfurni.tooltip.select.tile': 'Selecteer tegel',
|
||||
'wiredfurni.tooltip.remove.tile': 'Deselecteer tegel',
|
||||
'wiredfurni.tooltip.remove.5x5_tile': 'selecteer 5x5 tegels',
|
||||
'wiredfurni.tooltip.remove.clear_tile': 'Verwijder alle selecties',
|
||||
'wiredfurni.params.furni_neighborhood.group.user': 'Speelers',
|
||||
'wiredfurni.params.furni_neighborhood.group.furni': 'Meubels',
|
||||
'wiredfurni.params.selector_option.bot': "Geen BOT's",
|
||||
'wiredfurni.params.selector_option.pet': 'Geen Huisdieren',
|
||||
|
||||
// Wired furniture: badge received
|
||||
'wiredfurni.badgereceived.title': 'Badge ontvangen!',
|
||||
'wiredfurni.badgereceived.body': 'Je hebt zojuist een nieuwe badge ontvangen! Bekijk hem in je inventaris!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Notifications
|
||||
// ------------------------------------------------------------------------
|
||||
'notification.badge.received': 'Nieuwe badge!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Settings widget
|
||||
// ------------------------------------------------------------------------
|
||||
'widget.settings.general': 'Standaard',
|
||||
'widget.settings.general.title': 'Pas de standaard nitro settings aan',
|
||||
'widget.settings.volume': 'Volume',
|
||||
'widget.settings.interface': 'Interface',
|
||||
'widget.settings.interface.title': 'Pas de settings aan voor de interface',
|
||||
'widget.settings.interface.fps.automatic': 'Zet FPS naar unlimited',
|
||||
'widget.settings.interface.fps.warning': 'Het zetten van FPS naar unlimited kan prestatie problemen veroorzaken!',
|
||||
'widget.settings.interface.secondary': 'Verander de window header kleur',
|
||||
'widget.settings.interface.reset': 'Reset header kleur naar default',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Room widgets: chat + youtube
|
||||
// ------------------------------------------------------------------------
|
||||
'widget.room.chat.hide_pets': 'Verberg dieren',
|
||||
'widget.room.chat.hide_avatars': 'Verberg avatars',
|
||||
'widget.room.chat.hide_balloon': 'Verberg Spreekballon',
|
||||
'widget.room.chat.show_balloon': 'Spreekballon',
|
||||
'widget.room.chat.clear_history': 'leeg geschiedenis',
|
||||
'widget.room.youtube.shared': 'YouTube word gedeeld',
|
||||
'widget.room.youtube.open_video': 'Open de video',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Catalog
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
// Catalog: general
|
||||
'catalog.title': 'Catalogus',
|
||||
'catalog.favorites': 'Favorieten',
|
||||
'catalog.favorites.pages': 'Pagina’s',
|
||||
'catalog.favorites.furni': 'Furni',
|
||||
'catalog.favorites.empty': 'Geen favorieten',
|
||||
'catalog.favorites.empty.hint': 'Klik op het hartje bij furni of de ster bij pagina’s om ze toe te voegen.',
|
||||
|
||||
// Catalog: admin
|
||||
'catalog.admin': 'Beheer',
|
||||
'catalog.admin.new': 'Nieuw',
|
||||
'catalog.admin.root': 'Hoofdmap',
|
||||
'catalog.admin.new.root.category': 'Nieuwe hoofdcategorie',
|
||||
'catalog.admin.edit.root': 'Hoofdmap bewerken',
|
||||
'catalog.admin.edit': 'Bewerken:',
|
||||
'catalog.admin.edit.page': 'Pagina bewerken',
|
||||
'catalog.admin.hidden': 'verborgen',
|
||||
'catalog.admin.edit.title': '"%name%" bewerken',
|
||||
'catalog.admin.show': 'Tonen',
|
||||
'catalog.admin.hide': 'Verbergen',
|
||||
'catalog.admin.delete': 'Verwijderen',
|
||||
'catalog.admin.delete.title': '"%name%" verwijderen',
|
||||
'catalog.admin.delete.category.confirm': 'Categorie "%name%" en alle inhoud verwijderen?',
|
||||
'catalog.admin.delete.page': 'Pagina verwijderen',
|
||||
'catalog.admin.delete.page.confirm': 'Pagina "%name%" verwijderen?',
|
||||
'catalog.admin.delete.offer.confirm': 'Weet je zeker dat je deze aanbieding wilt verwijderen?',
|
||||
'catalog.admin.create': 'Aanmaken',
|
||||
'catalog.admin.save': 'Opslaan',
|
||||
'catalog.admin.create.subpage': 'Subpagina aanmaken',
|
||||
'catalog.admin.order': 'Volgorde',
|
||||
'catalog.admin.visible': 'Zichtbaar',
|
||||
'catalog.admin.enabled': 'Ingeschakeld',
|
||||
|
||||
// Catalog admin: offer editor
|
||||
'catalog.admin.offer.new': 'Nieuwe aanbieding',
|
||||
'catalog.admin.offer.edit': 'Aanbieding bewerken',
|
||||
'catalog.admin.offer.name': 'Catalogusnaam',
|
||||
'catalog.admin.offer.general': 'Algemeen',
|
||||
'catalog.admin.offer.quantity': 'Aantal',
|
||||
'catalog.admin.offer.prices': 'Prijzen',
|
||||
'catalog.admin.offer.credits': 'Credits',
|
||||
'catalog.admin.offer.points': 'Punten',
|
||||
'catalog.admin.offer.points.type': 'Type punten',
|
||||
'catalog.admin.offer.options': 'Opties',
|
||||
'catalog.admin.offer.club.only': 'Alleen Club',
|
||||
'catalog.admin.offer.extradata': 'Extra data (optioneel)....',
|
||||
'catalog.admin.offer.have.offer': 'Multi-korting (have_offer)',
|
||||
|
||||
// Catalog: trophies
|
||||
'catalog.trophies.title': 'Trofeeën',
|
||||
'catalog.trophies.write.hint': 'Schrijf een tekst voor de trofee voordat je koopt',
|
||||
'catalog.trophies.inscription': 'Trofee-inscriptie',
|
||||
'catalog.trophies.inscription.placeholder': 'Schrijf de tekst die op de trofee komt te staan...',
|
||||
|
||||
// Catalog: pets
|
||||
'catalog.pets.show.colors': 'Toon kleuren',
|
||||
'catalog.pets.choose.color': 'Kies kleur',
|
||||
'catalog.pets.choose.breed': 'Kies ras',
|
||||
'catalog.pets.back.breeds': '← Rassen',
|
||||
|
||||
// Catalog: name prefix editor
|
||||
'catalog.prefix.text': 'Tekst',
|
||||
'catalog.prefix.text.placeholder': 'Voer tekst in...',
|
||||
'catalog.prefix.icon': 'Icoon',
|
||||
'catalog.prefix.icon.remove': 'Icoon verwijderen',
|
||||
'catalog.prefix.effect': 'Effect',
|
||||
'catalog.prefix.color': 'Kleur',
|
||||
'catalog.prefix.color.single': '🎨 Enkel',
|
||||
'catalog.prefix.color.per.letter': '🌈 Per letter',
|
||||
'catalog.prefix.color.hint': 'Selecteer een letter en kies vervolgens de kleur. Gaat automatisch door.',
|
||||
'catalog.prefix.color.apply.all.title': 'Huidige kleur op alle letters toepassen',
|
||||
'catalog.prefix.color.apply.all': 'Op alles toepassen',
|
||||
'catalog.prefix.color.selected': 'Geselecteerde letter:',
|
||||
'catalog.prefix.price': 'Prijs:',
|
||||
'catalog.prefix.price.amount': '5 Credits',
|
||||
'catalog.prefix.purchased': '✓ Gekocht!',
|
||||
'catalog.prefix.purchase': 'Kopen',
|
||||
|
||||
// Catalog: gift wrapping
|
||||
'catalog.gift_wrapping.gift_sent': 'Klaar!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Group forum
|
||||
// ------------------------------------------------------------------------
|
||||
'groupforum.list.tab.most_active': 'Meest actieve onderwerpen',
|
||||
'groupforum.list.tab.my_forums': 'Mijn groepsforums',
|
||||
'groupforum.list.no_forums': 'Er zijn geen forums',
|
||||
'groupforum.view.threads': 'Aantal onderwerpen',
|
||||
'groupforum.thread.pin': 'Onderwerp vastpinnen',
|
||||
'groupforum.thread.unpin': 'Onderwerp losmaken',
|
||||
'groupforum.thread.lock': 'Onderwerp vergrendelen',
|
||||
'groupforum.thread.unlock': 'Onderwerp ontgrendelen',
|
||||
'groupforum.thread.hide': 'Onderwerp verbergen',
|
||||
'groupforum.thread.restore': 'Onderwerp weer zichtbaar maken',
|
||||
'groupforum.thread.delete': 'Onderwerp + berichten verwijderen',
|
||||
'groupforum.message.hide': 'Bericht verbergen',
|
||||
'group.forum.enable.caption': 'Groepsforum in-/uitschakelen',
|
||||
'group.forum.enable.help': 'Als je het groepsforum uitschakelt, worden ook alle berichten verwijderd!',
|
||||
'groupforum.view.no_threads': 'Er zijn op dit moment geen actieve onderwerpen',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: window
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.window.title': 'Mod Tools',
|
||||
'modtools.window.tools.room': 'Kamertool',
|
||||
'modtools.window.tools.chatlog': 'Chatlogtool',
|
||||
'modtools.window.tools.report': 'Rapporttool',
|
||||
'modtools.window.select.user': 'Selecteer een gebruiker',
|
||||
'modtools.window.no.room': 'Ga eerst een kamer binnen',
|
||||
'modtools.window.user.in_room': 'Nog steeds in deze kamer',
|
||||
'modtools.window.user.left_room': 'Niet langer in deze kamer',
|
||||
'modtools.window.user.clear': 'Selectie wissen',
|
||||
'modtools.window.tickets.open': '%count% open ticket',
|
||||
'modtools.window.tickets.open.many': '%count% open tickets',
|
||||
'modtools.window.section.room': 'Kamer',
|
||||
'modtools.window.section.user': 'Gebruiker',
|
||||
'modtools.window.section.reports': 'Rapporten',
|
||||
'modtools.window.user.open_info': 'Info openen',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: user info
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.userinfo.title': 'Gebruikersinfo: %username%',
|
||||
'modtools.userinfo.userName': 'Naam',
|
||||
'modtools.userinfo.cfhCount': 'CFH’s',
|
||||
'modtools.userinfo.abusiveCfhCount': 'Misbruikte CFH’s',
|
||||
'modtools.userinfo.cautionCount': 'Waarschuwingen',
|
||||
'modtools.userinfo.banCount': 'Bans',
|
||||
'modtools.userinfo.lastSanctionTime': 'Laatste sanctie',
|
||||
'modtools.userinfo.tradingLockCount': 'Ruilblokkades',
|
||||
'modtools.userinfo.tradingExpiryDate': 'Blokkade verloopt',
|
||||
'modtools.userinfo.minutesSinceLastLogin': 'Laatste login',
|
||||
'modtools.userinfo.lastPurchaseDate': 'Laatste aankoop',
|
||||
'modtools.userinfo.primaryEmailAddress': 'E-mail',
|
||||
'modtools.userinfo.identityRelatedBanCount': 'Verbannen accounts',
|
||||
'modtools.userinfo.registrationAgeInMinutes': 'Geregistreerd',
|
||||
'modtools.userinfo.userClassification': 'Rang',
|
||||
'modtools.userinfo.refresh': 'Gebruikersinfo vernieuwen',
|
||||
'modtools.userinfo.presence.in_room': 'In kamer',
|
||||
'modtools.userinfo.presence.in_room.title': 'In de kamer die je observeert',
|
||||
'modtools.userinfo.presence.online': 'Online',
|
||||
'modtools.userinfo.presence.online.title': 'Online op het hotel',
|
||||
'modtools.userinfo.presence.offline': 'Offline',
|
||||
'modtools.userinfo.presence.offline.title': 'Offline bij openen paneel',
|
||||
'modtools.userinfo.section.account': 'Account',
|
||||
'modtools.userinfo.section.activity': 'Activiteit',
|
||||
'modtools.userinfo.section.sanctions': 'Sancties',
|
||||
'modtools.userinfo.section.trading': 'Ruilen',
|
||||
'modtools.userinfo.button.room.chat': 'Kamerchat',
|
||||
'modtools.userinfo.button.send.message': 'Bericht verzenden',
|
||||
'modtools.userinfo.button.room.visits': 'Kamerbezoeken',
|
||||
'modtools.userinfo.button.mod.action': 'Mod-actie',
|
||||
'modtools.userinfo.stat.cfh': 'CFH',
|
||||
'modtools.userinfo.stat.cautions': 'Waarschuwingen',
|
||||
'modtools.userinfo.stat.bans': 'Bans',
|
||||
'modtools.userinfo.stat.trade.locks': 'Ruilblokkades',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: room info
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.roominfo.title': 'Kamerinfo',
|
||||
'modtools.roominfo.refresh': 'Kamerinfo vernieuwen',
|
||||
'modtools.roominfo.loading': 'Laden…',
|
||||
'modtools.roominfo.owner.here': 'Eigenaar aanwezig',
|
||||
'modtools.roominfo.owner.away': 'Eigenaar afwezig',
|
||||
'modtools.roominfo.owner.title.here': 'De kamereigenaar is op dit moment binnen',
|
||||
'modtools.roominfo.owner.title.away': 'De kamereigenaar is NIET binnen',
|
||||
'modtools.roominfo.stat.users': 'Gebruikers',
|
||||
'modtools.roominfo.stat.owner': 'Eigenaar',
|
||||
'modtools.roominfo.owner.open': 'Info van %username% openen',
|
||||
'modtools.roominfo.button.visit': 'Kamer bezoeken',
|
||||
'modtools.roominfo.button.chatlog': 'Chatlog',
|
||||
'modtools.roominfo.moderate.title': 'Kamer modereren',
|
||||
'modtools.roominfo.moderate.kick': 'Iedereen eruit kicken',
|
||||
'modtools.roominfo.moderate.doorbell': 'Deurbel inschakelen',
|
||||
'modtools.roominfo.moderate.rename': 'Kamernaam wijzigen',
|
||||
'modtools.roominfo.moderate.message.placeholder': 'Verplicht bericht dat met de actie wordt meegestuurd…',
|
||||
'modtools.roominfo.moderate.send.caution': 'Waarschuwing sturen',
|
||||
'modtools.roominfo.moderate.send.alert': 'Melding sturen',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: user message
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.user.message.title': 'Bericht verzenden',
|
||||
'modtools.user.message.recipient': 'Bericht aan',
|
||||
'modtools.user.message.label': 'Bericht',
|
||||
'modtools.user.message.placeholder': 'Schrijf iets nuttigs — de gebruiker ziet het als een moderatorbericht.',
|
||||
'modtools.user.message.empty': 'Leeg',
|
||||
'modtools.user.message.chars': '%count% tekens',
|
||||
'modtools.user.message.send': 'Bericht verzenden',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: mod action
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.user.modaction.title': 'Mod-actie: %username%',
|
||||
'modtools.user.modaction.sanctioning': 'Sanctioneren',
|
||||
'modtools.user.modaction.step.topic': '1. CFH-onderwerp',
|
||||
'modtools.user.modaction.step.topic.placeholder': 'Selecteer een onderwerp…',
|
||||
'modtools.user.modaction.step.sanction': '2. Sanctie',
|
||||
'modtools.user.modaction.step.sanction.placeholder': 'Selecteer een sanctie…',
|
||||
'modtools.user.modaction.step.message': '3. Eigen bericht',
|
||||
'modtools.user.modaction.step.message.optional': '(optioneel — overschrijft standaard)',
|
||||
'modtools.user.modaction.message.placeholder': 'Laat leeg om het standaard onderwerpbericht te gebruiken',
|
||||
'modtools.user.modaction.preview': 'Voorbeeld',
|
||||
'modtools.user.modaction.button.default': 'Standaardsanctie',
|
||||
'modtools.user.modaction.button.apply': 'Sanctie toepassen',
|
||||
'modtools.user.modaction.error.no.topic': 'Je moet een CFH-onderwerp selecteren',
|
||||
'modtools.user.modaction.error.no.action': 'Je moet een CFH-onderwerp en sanctie selecteren',
|
||||
'modtools.user.modaction.error.no.permission': 'Je hebt geen toestemming om dit te doen',
|
||||
'modtools.user.modaction.error.no.message': 'Schrijf een bericht aan de gebruiker',
|
||||
'modtools.user.modaction.error.no.permission.alert': 'Je hebt onvoldoende rechten',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: user visits
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.user.visits.title': 'Gebruikersbezoeken',
|
||||
'modtools.user.visits.recent': 'Recent bezochte kamers',
|
||||
'modtools.user.visits.entries.one': '%count% vermelding',
|
||||
'modtools.user.visits.entries.many': '%count% vermeldingen',
|
||||
'modtools.user.visits.empty': 'Geen recente bezoeken',
|
||||
'modtools.user.visits.time': 'Tijd',
|
||||
'modtools.user.visits.room': 'Kamernaam',
|
||||
'modtools.user.visits.action': 'Actie',
|
||||
'modtools.user.visits.visit': 'Bezoeken',
|
||||
'modtools.user.visits.visit.title': 'Kamer bezoeken',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: chatlog
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.user.chatlog.title': 'Gebruikerschatlog',
|
||||
'modtools.user.chatlog.title.with': 'Gebruikerschatlog: %username%',
|
||||
'modtools.user.chatlog.loading': 'Chatlog laden…',
|
||||
'modtools.room.chatlog.title': 'Kamerchatlog',
|
||||
'modtools.chatlog.column.time': 'Tijd',
|
||||
'modtools.chatlog.column.user': 'Gebruiker',
|
||||
'modtools.chatlog.column.message': 'Bericht',
|
||||
'modtools.chatlog.empty': 'Geen berichten',
|
||||
'modtools.chatlog.visit': 'Bezoeken',
|
||||
'modtools.chatlog.tools': 'Tools',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mod tools: tickets
|
||||
// ------------------------------------------------------------------------
|
||||
'modtools.tickets.title': 'Tickets',
|
||||
'modtools.tickets.tab.open': 'Open',
|
||||
'modtools.tickets.tab.mine': 'Mijn',
|
||||
'modtools.tickets.tab.picked': 'Alle opgepakt',
|
||||
'modtools.tickets.column.type': 'Type',
|
||||
'modtools.tickets.column.reported': 'Gerapporteerd',
|
||||
'modtools.tickets.column.opened': 'Geopend',
|
||||
'modtools.tickets.column.picker': 'Opgepakt door',
|
||||
'modtools.tickets.empty.open': 'Geen open meldingen',
|
||||
'modtools.tickets.empty.mine': 'Geen door jou opgepakte meldingen',
|
||||
'modtools.tickets.empty.picked': 'Geen opgepakte meldingen',
|
||||
'modtools.tickets.action.pick': 'Oppakken',
|
||||
'modtools.tickets.action.handle': 'Afhandelen',
|
||||
'modtools.tickets.action.release': 'Vrijgeven',
|
||||
'modtools.tickets.issue.title': 'Melding #%issueId% oplossen',
|
||||
'modtools.tickets.issue.label': 'Melding #%issueId%',
|
||||
'modtools.tickets.issue.details': 'Details',
|
||||
'modtools.tickets.issue.field.source': 'Bron',
|
||||
'modtools.tickets.issue.field.category': 'Categorie',
|
||||
'modtools.tickets.issue.field.description': 'Beschrijving',
|
||||
'modtools.tickets.issue.field.caller': 'Melder',
|
||||
'modtools.tickets.issue.field.reported': 'Gerapporteerd',
|
||||
'modtools.tickets.issue.chatlog.view': 'Chatlog bekijken',
|
||||
'modtools.tickets.issue.chatlog.close': 'Chatlog sluiten',
|
||||
'modtools.tickets.issue.resolve.heading': 'Oplossen als',
|
||||
'modtools.tickets.issue.resolve.resolved': 'Opgelost',
|
||||
'modtools.tickets.issue.resolve.useless': 'Nutteloos',
|
||||
'modtools.tickets.issue.resolve.abusive': 'Misbruik',
|
||||
'modtools.tickets.issue.release': 'Terug in wachtrij plaatsen',
|
||||
'modtools.tickets.cfh.chatlog.title': 'Melding #%issueId% chatlog',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Login
|
||||
// ------------------------------------------------------------------------
|
||||
'login.username': 'Wat is jou habbo naam',
|
||||
'login.forgot_password': 'Wachtwoord vergeten?',
|
||||
|
||||
// First-time visitors card
|
||||
'nitro.login.firsttime.title': 'Voor het eerst hier?',
|
||||
'nitro.login.firsttime.text': 'Heb je nog geen habbo account?',
|
||||
'nitro.login.firsttime.link': 'Je kunt er hier een aanmaken',
|
||||
'nitro.login.card.title': 'Aanmelden bij habbo',
|
||||
|
||||
// Server status checks
|
||||
'nitro.login.server.offline.short': 'De gameserver draait momenteel niet. Probeer het zo meteen opnieuw.',
|
||||
'nitro.login.server.offline.long': 'De gameserver draait momenteel niet, dus er kunnen geen nieuwe accounts worden aangemaakt. Probeer het zo meteen opnieuw.',
|
||||
'nitro.login.server.checking': 'Controleren…',
|
||||
'nitro.login.server.retry': 'Opnieuw proberen',
|
||||
|
||||
// Registration flow
|
||||
'nitro.login.register.title': 'habbo-gegevens',
|
||||
'nitro.login.register.next': 'Volgende',
|
||||
'nitro.login.register.finish': 'Voltooien',
|
||||
'nitro.login.register.creating': 'Bezig met aanmaken…',
|
||||
'nitro.login.register.intro.credentials': 'Laten we je account aanmaken. Voer je e-mailadres in en kies een wachtwoord — we controleren of dit e-mailadres nog niet in gebruik is.',
|
||||
'nitro.login.register.intro.avatar': 'Nu is het tijd om je eigen habbo-personage te maken! Begin met het kiezen van je habbo-naam.',
|
||||
'nitro.login.register.intro.room': 'Laatste stap — kies een startkamer, of sla dit over en maak later je eigen kamer.',
|
||||
'nitro.login.register.confirm.label': 'Bevestig wachtwoord',
|
||||
'nitro.login.register.username.placeholder': 'HabboNaam',
|
||||
'nitro.login.register.hotlooks.count': '%count% looks beschikbaar',
|
||||
'nitro.login.register.hotlooks.none': 'Geen looks geladen',
|
||||
'nitro.login.register.room.skip.title': 'Prima — ik maak mijn eigen kamers',
|
||||
'nitro.login.register.room.skip.description': 'Sla dit over en begin met een lege hotelinventaris.',
|
||||
'nitro.login.register.room.loading': 'Kamers laden…',
|
||||
'nitro.login.register.room.error': 'Kon kameropties niet laden. Je kunt deze stap nog steeds overslaan.',
|
||||
'nitro.login.register.success': 'Welkom aan boord, %username%! Je account is klaar — log hieronder in met het wachtwoord dat je zojuist hebt gekozen.',
|
||||
|
||||
// Forgot password
|
||||
'nitro.login.forgot.title': 'Wachtwoord resetten',
|
||||
'nitro.login.forgot.email.label': 'E-mailadres',
|
||||
'nitro.login.forgot.send': 'E-mail verzenden',
|
||||
'nitro.login.forgot.success': 'E-mail verzonden! Als er een account bij dit adres hoort, vind je binnenkort een resetlink in je inbox (controleer je spam als je binnen een minuut niets ziet).',
|
||||
|
||||
// Login errors (validation + transport)
|
||||
'nitro.login.error.missing_credentials': 'Voer zowel je habbo-naam als wachtwoord in.',
|
||||
'nitro.login.error.invalid_credentials': 'Ongeldige habbo-naam of wachtwoord.',
|
||||
'nitro.login.error.too_many_attempts': 'Te veel pogingen. Probeer het opnieuw over %seconds%s.',
|
||||
'nitro.login.error.turnstile': 'Voltooi de beveiligingscontrole.',
|
||||
'nitro.login.error.server_offline': 'De gameserver draait niet. Probeer het later opnieuw.',
|
||||
'nitro.login.error.login_unreachable': 'Kan de inlogservice niet bereiken. Probeer het opnieuw.',
|
||||
'nitro.login.error.register_failed': 'Kan je account niet aanmaken.',
|
||||
'nitro.login.error.register_unreachable': 'Kan de registratieservice niet bereiken.',
|
||||
'nitro.login.error.forgot_failed': 'Kan momenteel geen reset-e-mail verzenden.',
|
||||
'nitro.login.error.forgot_unreachable': 'Kan de wachtwoordresetservice niet bereiken.',
|
||||
'nitro.login.error.missing_fields': 'Vul alle velden in.',
|
||||
'nitro.login.error.invalid_email': 'Voer een geldig e-mailadres in.',
|
||||
'nitro.login.error.password_too_short': 'Je wachtwoord moet minimaal 8 tekens lang zijn.',
|
||||
'nitro.login.error.password_mismatch': 'Wachtwoorden komen niet overeen.',
|
||||
'nitro.login.error.email_taken': 'Dit e-mailadres is al in gebruik.',
|
||||
'nitro.login.error.missing_username': 'Kies een habbo-naam.',
|
||||
'nitro.login.error.username_length': 'De habbo-naam moet 3–16 tekens bevatten.',
|
||||
'nitro.login.error.username_taken': 'Deze habbo-naam is al in gebruik.',
|
||||
'nitro.login.error.missing_email': 'Voer je e-mailadres in.',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Inventory
|
||||
// ------------------------------------------------------------------------
|
||||
'inventory.effects.activate': 'Gebruik effect',
|
||||
'inventory.effects.remove': 'verwijder effect',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Loading screen — boot-stage labels read by App.tsx (taskLabel)
|
||||
// ------------------------------------------------------------------------
|
||||
'loading.task.session': 'Sessie verifiëren...',
|
||||
'loading.task.renderer': 'Renderer initialiseren...',
|
||||
'loading.task.assets': 'Spelmiddelen laden...',
|
||||
'loading.task.localization': 'Vertalingen laden...',
|
||||
'loading.task.avatar': 'Garderobe laden...',
|
||||
'loading.task.sounds': 'Geluiden laden...',
|
||||
'loading.task.startsession': 'Sessie starten...',
|
||||
'loading.task.userdata': 'Gebruikersgegevens laden...',
|
||||
'loading.task.rooms': 'Kamers laden...',
|
||||
'loading.task.engine': 'Grafische engine laden...',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Housekeeping
|
||||
// ------------------------------------------------------------------------
|
||||
'housekeeping.title': 'Beheer',
|
||||
'housekeeping.mode.light': 'Licht',
|
||||
|
||||
// Housekeeping: tabs
|
||||
'housekeeping.tab.dashboard': 'Dashboard',
|
||||
'housekeeping.tab.users': 'Gebruikers',
|
||||
'housekeeping.tab.rooms': 'Kamers',
|
||||
'housekeeping.tab.economy': 'Economie',
|
||||
'housekeeping.tab.audit': 'Logboek',
|
||||
|
||||
// Housekeeping: confirm + status
|
||||
'housekeeping.confirm.title': 'Actie bevestigen',
|
||||
'housekeeping.confirm.proceed': 'Doorgaan',
|
||||
'housekeeping.confirm.cancel': 'Annuleren',
|
||||
'housekeeping.status.dismiss': 'Sluiten',
|
||||
|
||||
// Housekeeping: action status
|
||||
'housekeeping.action.pending': 'Actie bezig…',
|
||||
'housekeeping.action.success': 'Actie voltooid',
|
||||
'housekeeping.action.error': 'Actie mislukt',
|
||||
'housekeeping.action.reset_password.done': 'Wachtwoord gereset — nieuw wachtwoord hieronder.',
|
||||
|
||||
// Housekeeping: generated password card
|
||||
'housekeeping.password.title': '%username% (#%id%) · nieuw wachtwoord',
|
||||
'housekeeping.password.value_label': 'Gegenereerd wachtwoord',
|
||||
'housekeeping.password.copy': 'Kopiëren',
|
||||
'housekeeping.password.copied': 'Gekopieerd',
|
||||
'housekeeping.password.copy_failed': 'Kopiëren mislukt',
|
||||
'housekeeping.password.dismiss': 'Sluiten',
|
||||
'housekeeping.password.hint': 'Deel dit buiten het hotel om met de gebruiker. Dit wordt eenmalig getoond — sluit deze kaart als je klaar bent; het wachtwoord wordt nooit meer weergegeven.',
|
||||
|
||||
// Housekeeping: errors
|
||||
'housekeeping.error.invalid_input': 'Ongeldige invoer — controleer de gebruikers-ID en de ingevoerde waarde.',
|
||||
'housekeeping.error.user_not_found': 'Gebruiker niet gevonden.',
|
||||
'housekeeping.error.user_offline': 'Gebruiker is offline — deze actie werkt alleen bij online gebruikers.',
|
||||
'housekeeping.error.target_unkickable': 'Deze gebruiker kan niet gekickt worden.',
|
||||
'housekeeping.error.ban_failed': 'Ban kon niet worden toegepast — de server weigerde het verzoek.',
|
||||
'housekeeping.error.no_active_ban': 'Geen actieve ban om op te heffen voor deze gebruiker.',
|
||||
'housekeeping.error.rank_not_found': 'Rang niet gevonden — kies een rang die bestaat in permission_ranks.',
|
||||
'housekeeping.error.db_failed': 'Databasefout — zie het emulator-log voor de SQL-uitzondering.',
|
||||
'housekeeping.error.hash_failed': 'Kon het nieuwe wachtwoord niet hashen — SHA-256 niet beschikbaar op deze JVM.',
|
||||
'housekeeping.error.room_not_found': 'Kamer niet gevonden.',
|
||||
'housekeeping.error.room_action_failed': 'Kameractie kon niet worden toegepast.',
|
||||
'housekeeping.error.new_owner_not_found': 'Nieuwe eigenaar niet gevonden.',
|
||||
'housekeeping.error.economy_failed': 'Economie-actie kon niet worden toegepast — controleer de gebruikers-ID en het aantal.',
|
||||
'housekeeping.error.alert_empty': 'Hotelmelding mag niet leeg zijn.',
|
||||
|
||||
// Housekeeping: actions
|
||||
'housekeeping.action.ban_h': 'Ban %h%u',
|
||||
'housekeeping.action.mute_min': 'Mute %m%m',
|
||||
'housekeeping.action.trade_lock_h': 'Ruilblokkade %h%u',
|
||||
'housekeeping.action.kick': 'Kick',
|
||||
'housekeeping.action.unban': 'Ban opheffen',
|
||||
'housekeeping.action.force_disconnect': 'Verbinding verbreken',
|
||||
'housekeeping.action.set_rank': 'Rang instellen',
|
||||
'housekeeping.action.reset_password': 'Wachtwoord resetten',
|
||||
|
||||
// Housekeeping: user panel
|
||||
'housekeeping.user.search.placeholder': 'Zoek op gebruikersnaam…',
|
||||
'housekeeping.user.search.button': 'Zoeken',
|
||||
'housekeeping.user.clear': 'Selectie wissen',
|
||||
'housekeeping.user.none': 'Geen gebruiker geselecteerd — zoek hierboven om er een te kiezen.',
|
||||
'housekeeping.user.not_found': 'Gebruiker niet gevonden.',
|
||||
'housekeeping.user.credits': 'Credits',
|
||||
'housekeeping.user.duckets': 'Duckets / pixels',
|
||||
'housekeeping.user.diamonds': 'Diamonds',
|
||||
'housekeeping.user.audit_hint': 'Alle acties worden vastgelegd in het logboek-tabblad.',
|
||||
'housekeeping.user.live.label': 'Live (in huidige kamer)',
|
||||
'housekeeping.user.live.kick': 'Kick',
|
||||
'housekeeping.user.live.mute_2m': 'Mute 2m',
|
||||
'housekeeping.user.live.mute_10m': 'Mute 10m',
|
||||
'housekeeping.user.live.ban_h': 'Ban 1u',
|
||||
'housekeeping.user.live.ban_d': 'Ban 1d',
|
||||
|
||||
// Housekeeping: room panel
|
||||
'housekeeping.room.search.placeholder': 'Kamer-ID…',
|
||||
'housekeeping.room.search.button': 'Zoeken',
|
||||
'housekeeping.room.clear': 'Selectie wissen',
|
||||
'housekeeping.room.none': 'Geen kamer geselecteerd — voer hierboven een ID in.',
|
||||
'housekeeping.room.not_found': 'Kamer niet gevonden.',
|
||||
'housekeeping.room.open': 'Openen',
|
||||
'housekeeping.room.close': 'Sluiten',
|
||||
'housekeeping.room.mute_min': 'Mute %m%m',
|
||||
'housekeeping.room.kick_all': 'Iedereen kicken',
|
||||
'housekeeping.room.kick_all.confirm': 'Elke gebruiker die nu in de kamer is kicken?',
|
||||
'housekeeping.room.delete': 'Kamer verwijderen',
|
||||
'housekeeping.room.delete.confirm': 'Deze kamer en alle meubels permanent verwijderen?',
|
||||
'housekeeping.room.transfer': 'Overdragen',
|
||||
'housekeeping.room.transfer.label': 'Eigendom overdragen',
|
||||
'housekeeping.room.transfer.new_owner': 'ID nieuwe eigenaar',
|
||||
|
||||
// Housekeeping: economy
|
||||
'housekeeping.economy.select_user': 'Kies eerst een gebruiker in het tabblad Gebruikers.',
|
||||
'housekeeping.economy.target': 'Doel: %username% (#%id%)',
|
||||
'housekeeping.economy.give_credits': 'Credits geven',
|
||||
'housekeeping.economy.give_duckets': 'Duckets geven',
|
||||
'housekeeping.economy.give_diamonds': 'Diamonds geven',
|
||||
'housekeeping.economy.grant_item': 'Item toekennen',
|
||||
'housekeeping.economy.grant_item.label': 'Catalogusitem toekennen',
|
||||
'housekeeping.economy.item_id': 'Item-ID',
|
||||
'housekeeping.economy.item_quantity': 'Aantal',
|
||||
'housekeeping.economy.set_hc_days': 'HC-dagen instellen',
|
||||
|
||||
// Housekeeping: hotel-wide alert
|
||||
'housekeeping.hotel.alert.label': 'Hotelbrede melding',
|
||||
'housekeeping.hotel.alert.placeholder': 'Bericht dat naar elke verbonden gebruiker wordt uitgezonden…',
|
||||
'housekeeping.hotel.alert.send': 'Naar hotel sturen',
|
||||
'housekeeping.hotel.alert.confirm': 'Melding van %count% tekens naar elke verbonden gebruiker uitzenden?',
|
||||
|
||||
// Housekeeping: dashboard
|
||||
'housekeeping.dashboard.title': 'Overzicht',
|
||||
'housekeeping.dashboard.refresh': 'Vernieuwen',
|
||||
'housekeeping.dashboard.loading': 'Dashboard laden…',
|
||||
'housekeeping.dashboard.unavailable': 'Dashboard niet beschikbaar — controleer het admin-endpoint.',
|
||||
'housekeeping.dashboard.online': 'Online',
|
||||
'housekeeping.dashboard.total_users': '%count% totaal',
|
||||
'housekeeping.dashboard.rooms_active': 'Actieve kamers',
|
||||
'housekeeping.dashboard.total_rooms': '%count% totaal',
|
||||
'housekeeping.dashboard.peak_today': 'Piek vandaag',
|
||||
'housekeeping.dashboard.peak_alltime': 'Aller-tijden piek %count%',
|
||||
'housekeeping.dashboard.pending_tickets': 'Tickets',
|
||||
'housekeeping.dashboard.sanctions_24h': '%count% sancties / 24u',
|
||||
'housekeeping.dashboard.server': 'Server',
|
||||
'housekeeping.dashboard.recent_sanctions': 'Recente sancties',
|
||||
'housekeeping.dashboard.recent_lookups': 'Recente opzoekingen',
|
||||
|
||||
// Housekeeping: audit log
|
||||
'housekeeping.audit.title': 'Logboek',
|
||||
'housekeeping.audit.refresh': 'Vernieuwen',
|
||||
'housekeeping.audit.filter.all': 'Alle',
|
||||
'housekeeping.audit.filter.users': 'Gebruikers',
|
||||
'housekeeping.audit.filter.rooms': 'Kamers',
|
||||
'housekeeping.audit.filter.hotel': 'Hotel',
|
||||
'housekeeping.audit.search.placeholder': 'Zoek uitvoerder / doel / actie…',
|
||||
'housekeeping.audit.empty': 'Nog geen logboekvermeldingen.',
|
||||
'housekeeping.audit.no_match': 'Geen vermeldingen komen overeen met de huidige filters.',
|
||||
|
||||
// Housekeeping: shared fields
|
||||
'housekeeping.field.reason': 'Reden',
|
||||
'housekeeping.field.reason.placeholder': 'Vrije reden (optioneel)',
|
||||
'housekeeping.field.duration': 'Duur',
|
||||
'housekeeping.reason.default': 'Geen reden opgegeven.',
|
||||
|
||||
// Housekeeping: context menu
|
||||
'housekeeping.menu.send_to_hk': 'Naar beheer sturen',
|
||||
|
||||
// Housekeeping: bulk actions
|
||||
'housekeeping.bulk.done': 'Bulk klaar',
|
||||
'housekeeping.bulk.success': 'Alle bulkacties geslaagd.',
|
||||
'housekeeping.bulk.partial': 'Bulk voltooid met enkele mislukkingen.',
|
||||
'housekeeping.bulk.failed': 'Elke bulkactie is mislukt.',
|
||||
'housekeeping.bulk.confirm': '%action% toepassen op %count% geselecteerde gebruikers?',
|
||||
'housekeeping.bulk.label': '%count% geselecteerd',
|
||||
'housekeeping.bulk.clear': 'Selectie wissen',
|
||||
'housekeeping.bulk.apply': 'Toepassen op selectie',
|
||||
|
||||
// Housekeeping: telemetry
|
||||
'housekeeping.telemetry.title': 'Telemetrie',
|
||||
'housekeeping.telemetry.empty': 'Nog geen acties waargenomen.',
|
||||
'housekeeping.telemetry.reset': 'Statistieken resetten',
|
||||
|
||||
// Housekeeping: live room session
|
||||
'housekeeping.live.no_room': 'Geen actieve kamersessie.',
|
||||
'housekeeping.live.kicked': 'Uit de kamer gekickt.',
|
||||
'housekeeping.live.banned': 'Verbannen uit de kamer.',
|
||||
'housekeeping.live.muted': 'Gemute in de kamer.',
|
||||
|
||||
// Housekeeping: validation
|
||||
'housekeeping.validation.empty_username': 'Gebruikersnaam mag niet leeg zijn.',
|
||||
'housekeeping.validation.invalid_user_id': 'Ongeldige gebruikers-ID.',
|
||||
'housekeeping.validation.invalid_room_id': 'Ongeldige kamer-ID.',
|
||||
'housekeeping.validation.invalid_amount': 'Ongeldig aantal.',
|
||||
'housekeeping.validation.amount_too_large': 'Aantal overschrijdt de veiligheidslimiet.',
|
||||
'housekeeping.validation.empty_reason': 'Reden mag niet leeg zijn.',
|
||||
'housekeeping.validation.invalid_hours': 'Ongeldige duur in uren.',
|
||||
'housekeeping.validation.invalid_rank': 'Ongeldige rang — moet tussen 1 en 12 liggen.',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Fortune Wheel
|
||||
// ------------------------------------------------------------------------
|
||||
'wheel.title': 'Rad van Fortuin',
|
||||
'wheel.free.today': 'Je hebt vandaag %count% gratis draaibeurten!',
|
||||
'wheel.extra': 'Extra draaibeurten: %count%',
|
||||
'wheel.spin': 'DRAAIEN',
|
||||
'wheel.settings': 'Settings',
|
||||
'wheel.settings.title': 'Rad van Fortuin Settings',
|
||||
'wheel.buy': 'Draaibeurt kopen',
|
||||
'wheel.winners': 'Laatste winnaars',
|
||||
'wheel.winners.empty': 'Nog geen winnaars',
|
||||
'wheel.win.title': 'Gewonnen!',
|
||||
'wheel.win.jackpot': '★ Jackpot ★',
|
||||
'wheel.win.nothing': 'Volgende keer beter!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Soundboard
|
||||
// ------------------------------------------------------------------------
|
||||
'soundboard.title': 'Soundboard',
|
||||
'soundboard.empty': 'Geen geluiden beschikbaar',
|
||||
'soundboard.lastplayed': 'Afgespeeld door %user%',
|
||||
'soundboard.room.setting.desc': 'Laat mensen in deze kamer geluidseffecten afspelen',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Radio
|
||||
// ------------------------------------------------------------------------
|
||||
'radio.title': 'Radio',
|
||||
'radio.empty': 'Geen stations',
|
||||
'radio.error': 'Kon de stations niet laden',
|
||||
'radio.stop': 'Stoppen',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Rare Values
|
||||
// ------------------------------------------------------------------------
|
||||
'rarevalues.title': 'Zeldzame waarden',
|
||||
'rarevalues.loading': 'Waarden laden…',
|
||||
'rarevalues.empty': 'Geen rares gevonden',
|
||||
'rarevalues.infostand.label': 'Waarde:',
|
||||
|
||||
// Rare Values: editor
|
||||
'rarevalues.editor.tab': 'Bewerken',
|
||||
'rarevalues.editor.type': 'Type',
|
||||
'rarevalues.editor.value': 'Waarde',
|
||||
'rarevalues.editor.weight': 'Kans',
|
||||
'rarevalues.editor.label': 'Label',
|
||||
'rarevalues.editor.save': 'Opslaan',
|
||||
'rarevalues.editor.add': '+ Prijs toevoegen',
|
||||
'rarevalues.editor.remove': 'Prijs verwijderen',
|
||||
'rarevalues.editor.cat.item': 'Meubel (ID)',
|
||||
'rarevalues.editor.cat.spin': 'Extra draaien',
|
||||
'rarevalues.editor.cat.nothing': 'Niets',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Chat commands: client
|
||||
// ------------------------------------------------------------------------
|
||||
'chatcmd.client.shake': 'Schud de kamer',
|
||||
'chatcmd.client.rotate': 'Draai de kamer',
|
||||
'chatcmd.client.zoom': 'Zoom in/uit',
|
||||
'chatcmd.client.flip': 'Reset zoom',
|
||||
'chatcmd.client.iddqd': 'Zet kamer op zijn kop',
|
||||
'chatcmd.client.screenshot': 'Schermafbeelding van de kamer',
|
||||
'chatcmd.client.togglefps': 'FPS aan/uit',
|
||||
'chatcmd.client.laugh': 'Lach (VIP)',
|
||||
'chatcmd.client.kiss': 'Stuur een kus (VIP)',
|
||||
'chatcmd.client.jump': 'Spring (VIP)',
|
||||
'chatcmd.client.idle': 'Ga afwezig',
|
||||
'chatcmd.client.sign': 'Toon bordje',
|
||||
'chatcmd.client.furni': 'Meubelkiezer',
|
||||
'chatcmd.client.chooser': 'Gebruikerskiezer',
|
||||
'chatcmd.client.floor': 'Vloer-editor',
|
||||
'chatcmd.client.pickall': 'Pak alle meubels op',
|
||||
'chatcmd.client.ejectall': 'Verwijder alle meubels',
|
||||
'chatcmd.client.settings': 'Kamerinstellingen',
|
||||
'chatcmd.client.info': 'Client info',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Me-menu settings + User account settings window
|
||||
// ------------------------------------------------------------------------
|
||||
'usersettings.tab.general': "Algemeen",
|
||||
'usersettings.tab.themes': "Thema's",
|
||||
'memenu.settings.other.place.multiple.objects': "Meerdere objecten plaatsen",
|
||||
'memenu.settings.other.skip.purchase.confirmation': "Aankoopbevestiging overslaan",
|
||||
'memenu.settings.other.enable.chat.window': "Chatvenster inschakelen",
|
||||
'memenu.settings.other.catalog.classic.style': "Catalogus: klassieke stijl",
|
||||
'usersettings.open.title': "Gebruikersinstellingen",
|
||||
'usersettings.open.subtitle': "Wachtwoord & account",
|
||||
'usersettings.themes.custom': "Aangepast thema",
|
||||
'usersettings.themes.default_option': "Standaard (geen thema)",
|
||||
'usersettings.themes.active_pieces': "Actieve onderdelen",
|
||||
'usersettings.themes.invalid': "Thema ongeldig of onbereikbaar — standaard wordt gebruikt.",
|
||||
'usersettings.themes.none': "Geen thema's beschikbaar. Voeg een map toe in custom-themes/ op de server.",
|
||||
'usersettings.title': "Gebruikersinstellingen",
|
||||
'usersettings.account.label': "Mijn account",
|
||||
'usersettings.guest': "Gast",
|
||||
'usersettings.subtitle': "Beheer je account en beveiliging",
|
||||
'usersettings.menu.section': "Account",
|
||||
'usersettings.menu.password.title': "Wachtwoord wijzigen",
|
||||
'usersettings.menu.password.desc': "Wijzig het wachtwoord waarmee je inlogt.",
|
||||
'usersettings.menu.email.title': "E-mail wijzigen",
|
||||
'usersettings.menu.email.desc': "Werk het e-mailadres van je account bij.",
|
||||
'usersettings.menu.username.title': "Gebruikersnaam wijzigen",
|
||||
'usersettings.menu.username.desc': "Kies een nieuwe naam. Je moet daarna opnieuw inloggen.",
|
||||
'usersettings.menu.soon.title': "Meer komt binnenkort",
|
||||
'usersettings.menu.soon.desc': "Tweestapsverificatie en meer.",
|
||||
'usersettings.password.hint': "Gebruik minimaal %count% tekens. Combineer hoofd- en kleine letters, cijfers en symbolen voor een sterker wachtwoord.",
|
||||
'usersettings.email.hint': "Voor de veiligheid vragen we je je huidige wachtwoord te bevestigen voordat je het e-mailadres van je account wijzigt.",
|
||||
'usersettings.username.hint': "Door je naam te wijzigen word je uitgelogd en je kunt pas na 30 dagen opnieuw wijzigen. Zorg dat je vrienden je nieuwe naam kennen!",
|
||||
'usersettings.field.current_password': "Huidig wachtwoord",
|
||||
'usersettings.field.new_password': "Nieuw wachtwoord",
|
||||
'usersettings.field.retype_password': "Herhaal nieuw wachtwoord",
|
||||
'usersettings.field.new_email': "Nieuw e-mailadres",
|
||||
'usersettings.field.new_username': "Nieuwe gebruikersnaam",
|
||||
'usersettings.username.rules': "%min%-%max% tekens. Alleen letters, cijfers, punt, underscore en streepje.",
|
||||
'usersettings.strength.weak': "Zwak",
|
||||
'usersettings.strength.fair': "Redelijk",
|
||||
'usersettings.strength.good': "Goed",
|
||||
'usersettings.strength.strong': "Sterk",
|
||||
'usersettings.aria.show_password': "Wachtwoord tonen",
|
||||
'usersettings.aria.hide_password': "Wachtwoord verbergen",
|
||||
'usersettings.btn.cancel': "Annuleren",
|
||||
'usersettings.btn.saving': "Opslaan…",
|
||||
'usersettings.btn.save_password': "Wachtwoord opslaan",
|
||||
'usersettings.btn.save_email': "E-mail opslaan",
|
||||
'usersettings.btn.renaming': "Bezig met hernoemen…",
|
||||
'usersettings.btn.rename': "Hernoem mij",
|
||||
'usersettings.error.fields_required': "Alle velden zijn verplicht.",
|
||||
'usersettings.error.password_min': "Het wachtwoord moet minimaal %count% tekens bevatten.",
|
||||
'usersettings.error.password_long': "Het wachtwoord is te lang.",
|
||||
'usersettings.error.password_mismatch': "De nieuwe wachtwoorden komen niet overeen.",
|
||||
'usersettings.error.password_same': "Het nieuwe wachtwoord moet anders zijn dan het huidige wachtwoord.",
|
||||
'usersettings.error.not_authenticated': "Je bent niet ingelogd. Log opnieuw in.",
|
||||
'usersettings.error.network': "Kan de server niet bereiken. Probeer het opnieuw.",
|
||||
'usersettings.error.request_failed': "Verzoek mislukt (%status%).",
|
||||
'usersettings.error.email_long': "Het e-mailadres is te lang.",
|
||||
'usersettings.error.email_invalid': "Voer een geldig e-mailadres in.",
|
||||
'usersettings.error.username_length': "De gebruikersnaam moet tussen %min% en %max% tekens bevatten.",
|
||||
'usersettings.error.username_invalid': "De gebruikersnaam mag alleen letters, cijfers, punt, underscore en streepje bevatten.",
|
||||
'usersettings.error.username_same': "De nieuwe gebruikersnaam moet anders zijn dan de huidige.",
|
||||
'usersettings.success.password': "Wachtwoord succesvol bijgewerkt.",
|
||||
'usersettings.success.email': "E-mail succesvol bijgewerkt.",
|
||||
'usersettings.success.username': "Gebruikersnaam bijgewerkt. Log opnieuw in met je nieuwe naam.",
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
// Hotel radio stations. Copy this file to `radio-stations.json5` (without the
|
||||
// .example suffix) and add your own stations — each entry is just a streaming
|
||||
// URL the client plays with the HTML5 Audio API. JSON5: // comments and
|
||||
// trailing commas are allowed. Add / remove / reorder freely, no rebuild needed.
|
||||
//
|
||||
// Fields:
|
||||
// id - unique key (string)
|
||||
// name - label shown in the radio widget
|
||||
// genre - optional subtitle
|
||||
// url - the audio stream URL (mp3/aac/ogg Icecast or Shoutcast)
|
||||
// logo - optional image URL shown next to the station
|
||||
//
|
||||
// The first station autostarts (quietly) on client load. The list can later
|
||||
// be moved to the CMS (website_settings) so it's editable from the admin.
|
||||
stations: [
|
||||
// { id: 'mystation', name: 'My Station', genre: 'Hotel Radio', url: 'https://your-stream-host/stream' },
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"rarevalues.title": "Rare Values",
|
||||
"rarevalues.loading": "Loading values…",
|
||||
"rarevalues.empty": "No rares found",
|
||||
"rarevalues.infostand.label": "Value:"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"rarevalues.title": "Valore Rari",
|
||||
"rarevalues.loading": "Caricamento valori…",
|
||||
"rarevalues.empty": "Nessun raro trovato",
|
||||
"rarevalues.infostand.label": "Valore:"
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
"sounds.url": "${asset.url}/sounds/%sample%.mp3",
|
||||
"external.texts.url": [
|
||||
"${gamedata.url}/ExternalTexts.json?t=%timestamp%",
|
||||
"${gamedata.url}/UITexts.json?t=%timestamp%"
|
||||
"${gamedata.url}/UITexts.json5?t=%timestamp%"
|
||||
],
|
||||
"external.texts.translation.url": "${gamedata.url}/text_translate/ExternalTexts_%locale%.json?t=%timestamp%",
|
||||
"external.samples.url": "${hof.furni.url}/mp3/sound_machine_sample_%sample%.mp3",
|
||||
@@ -27,9 +27,11 @@
|
||||
"avatar.asset.effect.url": "${asset.url}/effect/%libname%.nitro",
|
||||
"furni.asset.url": "${asset.url}/furniture/%libname%.nitro",
|
||||
"furni.asset.icon.url": "${hof.furni.url}/icons/%libname%%param%_icon.png",
|
||||
"pet.asset.url": "${asset.url}/pets/%libname%.nitro",
|
||||
"pet.asset.url": "${asset.url}/pet/%libname%.nitro",
|
||||
"generic.asset.url": "${asset.url}/generic/%libname%.nitro",
|
||||
"badge.asset.url": "${image.library.url}album1584/%badgename%.gif",
|
||||
"radio.url": "${gamedata.url}/radio-stations.json5?t=%timestamp%",
|
||||
"soundboard.url": "${gamedata.url}/soundboard-sounds.json5?t=%timestamp%",
|
||||
"furni.rotation.bounce.steps": 20,
|
||||
"furni.rotation.bounce.height": 0.0625,
|
||||
"enable.avatar.arrow": false,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
// Soundboard pads loaded from a file — used as a FALLBACK when the server
|
||||
// (soundboard_sounds DB table) returns no sounds. Copy this file to
|
||||
// `soundboard-sounds.json5` (without .example) and add your sounds. JSON5:
|
||||
// // comments and trailing commas are allowed.
|
||||
//
|
||||
// Fields:
|
||||
// id - unique number (pad key)
|
||||
// name - label shown on the pad
|
||||
// url - audio file URL (mp3/ogg/wav). Relative urls resolve against
|
||||
// `soundboard.url.prefix` (falls back to `asset.url`).
|
||||
//
|
||||
// NOTE: file-defined pads play LOCALLY for the person who clicks them. To
|
||||
// broadcast a pad to everyone in the room, the sound must exist server-side
|
||||
// in the soundboard_sounds table (same flow as custom badges). The file is
|
||||
// the no-DB / offline option; the DB is the multiplayer one.
|
||||
sounds: [
|
||||
// { id: 1, name: 'Airhorn', url: 'https://your-host/airhorn.mp3' },
|
||||
],
|
||||
}
|
||||
@@ -24,11 +24,12 @@
|
||||
"wired.action.kick.from.room.max.length": 100,
|
||||
"wired.action.mute.user.max.length": 100,
|
||||
"game.center.enabled": false,
|
||||
"radio_ui.enabled": false,
|
||||
"guides.enabled": true,
|
||||
"housekeeping.enabled": true,
|
||||
"toolbar.hide.quests": true,
|
||||
"catalog.style.new": true,
|
||||
"show.google.ads": false,
|
||||
"catalog.classic.style": false,
|
||||
"loginview": {
|
||||
"images": {
|
||||
"background": "${asset.url}/c_images/reception/stretch_blue.png",
|
||||
@@ -39,28 +40,6 @@
|
||||
"right": "${asset.url}/c_images/reception/US_right.png",
|
||||
"right.repeat": "${asset.url}/c_images/reception/US_top_right.png"
|
||||
},
|
||||
"widgets": {
|
||||
"slot.1.widget": "promoarticle",
|
||||
"slot.1.conf": {},
|
||||
"slot.2.widget": "widgetcontainer",
|
||||
"slot.2.conf": {
|
||||
"image": "${image.library.url}web_promo_small/spromo_Canal_Bundle.png",
|
||||
"texts": "2021NitroPromo",
|
||||
"btnLink": ""
|
||||
},
|
||||
"slot.3.widget": "",
|
||||
"slot.3.conf": {},
|
||||
"slot.4.widget": "",
|
||||
"slot.4.conf": {},
|
||||
"slot.5.widget": "",
|
||||
"slot.5.conf": {},
|
||||
"slot.6.widget": "",
|
||||
"slot.6.conf": {
|
||||
"campaign": ""
|
||||
},
|
||||
"slot.7.widget": "",
|
||||
"slot.7.conf": {}
|
||||
}
|
||||
},
|
||||
"navigator.room.models": [
|
||||
{
|
||||
|
||||
@@ -32,6 +32,11 @@ const preloadUrl = async (url: string): Promise<void> =>
|
||||
{
|
||||
if(!url) return;
|
||||
|
||||
// Split gamedata URLs are directories (end with '/'); fetching them as a
|
||||
// file just 404s and wastes a connection at startup. The real split loader
|
||||
// handles those — only warm up actual file URLs here.
|
||||
if(url.split('?')[0].split('#')[0].endsWith('/')) return;
|
||||
|
||||
try
|
||||
{
|
||||
const response = await fetch(url, { cache: 'force-cache' });
|
||||
|
||||
@@ -27,8 +27,16 @@ export class PageLocalization implements IPageLocalization
|
||||
|
||||
if(!imageName || !imageName.length) return null;
|
||||
|
||||
// Already a full URL (any extension) -> use it directly.
|
||||
if(/^https?:\/\//i.test(imageName)) return imageName;
|
||||
|
||||
let assetUrl = GetConfigurationValue<string>('catalog.asset.image.url');
|
||||
|
||||
// The template forces ".gif" (.../%name%.gif). If the image name
|
||||
// already carries its own extension (png/jpg/webp/gif), don't append
|
||||
// the forced .gif so non-gif catalog images work too.
|
||||
if(/\.[a-z0-9]+$/i.test(imageName)) assetUrl = assetUrl.replace(/\.gif(?=$|\?)/i, '');
|
||||
|
||||
assetUrl = assetUrl.replace('%name%', imageName);
|
||||
|
||||
return assetUrl;
|
||||
|
||||
@@ -27,6 +27,8 @@ export * from './purse';
|
||||
export * from './room';
|
||||
export * from './room/events';
|
||||
export * from './room/widgets';
|
||||
export * from './soundboard';
|
||||
export * from './theme';
|
||||
export * from './ui-settings';
|
||||
export * from './user';
|
||||
export * from './utils';
|
||||
|
||||
@@ -11,6 +11,7 @@ export class BotSkillsEnum
|
||||
public static NUX_PROCEED: number = 8;
|
||||
public static CHANGE_BOT_MOTTO: number = 9;
|
||||
public static NUX_TAKE_TOUR: number = 10;
|
||||
public static ROTATE: number = 11;
|
||||
public static NO_PICK_UP: number = 12;
|
||||
public static NAVIGATOR_SEARCH: number = 14;
|
||||
public static DONATE_TO_USER: number = 24;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
let _soundboardEnabled = false;
|
||||
|
||||
export const getSoundboardRoomEnabled = () => _soundboardEnabled;
|
||||
export const setSoundboardRoomEnabled = (enabled: boolean) =>
|
||||
{
|
||||
_soundboardEnabled = enabled;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SoundboardRoomState';
|
||||
@@ -0,0 +1,122 @@
|
||||
import { NitroLogger } from '@nitrots/nitro-renderer';
|
||||
import { GetConfigurationValue } from '../nitro';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Custom theme ecosystem (graphics-only, runtime-loaded).
|
||||
//
|
||||
// A "theme" is a folder on the server (NOT bundled in the build) made of:
|
||||
// <base>/index.json -> { "themes": [ { id, name, author? } ] }
|
||||
// <base>/<id>/theme.json -> { name, pieces: [ { id, name, file } ] }
|
||||
// <base>/<id>/<file>.css -> one CSS "piece" (cards, chat, catalog, ...)
|
||||
//
|
||||
// Each enabled piece is injected as a <link> in <head>. If a piece fails to
|
||||
// load (404 / network) the link removes itself, so the UI falls back to the
|
||||
// default look for that piece (per-piece fallback, never breaks the client).
|
||||
//
|
||||
// The base url is configurable via ui-config ("theme.base.url") so themes can
|
||||
// live anywhere (and never need a client rebuild to add/change them).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ThemeInfo
|
||||
{
|
||||
id: string;
|
||||
name: string;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
export interface ThemePiece
|
||||
{
|
||||
id: string;
|
||||
name: string;
|
||||
file: string;
|
||||
}
|
||||
|
||||
export interface ThemeManifest
|
||||
{
|
||||
name: string;
|
||||
pieces: ThemePiece[];
|
||||
}
|
||||
|
||||
const LINK_ATTR = 'data-nitro-theme';
|
||||
|
||||
export const GetThemeBaseUrl = (): string =>
|
||||
GetConfigurationValue<string>('theme.base.url', 'custom-themes').replace(/\/+$/, '');
|
||||
|
||||
export const FetchThemeIndex = async (): Promise<ThemeInfo[]> =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const response = await fetch(`${ GetThemeBaseUrl() }/index.json`, { cache: 'no-cache' });
|
||||
|
||||
if(!response.ok) return [];
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return Array.isArray(data?.themes) ? data.themes.filter((t: any) => t && t.id) : [];
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
NitroLogger.warn('[ThemeManager] index.json non caricabile, nessun tema custom', error);
|
||||
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const FetchThemeManifest = async (themeId: string): Promise<ThemeManifest> =>
|
||||
{
|
||||
if(!themeId) return null;
|
||||
|
||||
try
|
||||
{
|
||||
const response = await fetch(`${ GetThemeBaseUrl() }/${ themeId }/theme.json`, { cache: 'no-cache' });
|
||||
|
||||
if(!response.ok) return null;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if(!data || !Array.isArray(data.pieces)) return null;
|
||||
|
||||
return {
|
||||
name: data.name ?? themeId,
|
||||
pieces: data.pieces.filter((p: any) => p && p.id && p.file)
|
||||
};
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
NitroLogger.warn(`[ThemeManager] manifest non valido per tema "${ themeId }" -> fallback default`, error);
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const ClearTheme = (): void =>
|
||||
{
|
||||
document.head.querySelectorAll(`link[${ LINK_ATTR }]`).forEach(node => node.remove());
|
||||
};
|
||||
|
||||
export const ApplyThemePieces = (themeId: string, pieces: ThemePiece[]): void =>
|
||||
{
|
||||
ClearTheme();
|
||||
|
||||
if(!themeId || !pieces || !pieces.length) return;
|
||||
|
||||
const base = GetThemeBaseUrl();
|
||||
|
||||
for(const piece of pieces)
|
||||
{
|
||||
const link = document.createElement('link');
|
||||
|
||||
link.rel = 'stylesheet';
|
||||
link.setAttribute(LINK_ATTR, piece.id);
|
||||
link.href = `${ base }/${ themeId }/${ piece.file }`;
|
||||
|
||||
// Per-piece fallback: a broken piece removes itself, leaving the default.
|
||||
link.onerror = () =>
|
||||
{
|
||||
NitroLogger.warn(`[ThemeManager] pezzo tema rotto "${ themeId }/${ piece.file }" -> fallback default`);
|
||||
link.remove();
|
||||
};
|
||||
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ThemeManager';
|
||||
@@ -4,4 +4,7 @@ export class LocalStorageKeys
|
||||
public static CATALOG_SKIP_PURCHASE_CONFIRMATION: string = 'catalogSkipPurchaseConfirmation';
|
||||
public static CHAT_WINDOW_ENABLED: string = 'chatWindowEnabled';
|
||||
public static CHAT_TRANSLATION_SETTINGS: string = 'chatTranslationSettings';
|
||||
public static CATALOG_CLASSIC_STYLE: string = 'catalogClassicStyle';
|
||||
public static THEME_ACTIVE: string = 'nitroThemeActive';
|
||||
public static THEME_PIECES: string = 'nitroThemePieces';
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 695 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 806 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 551 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 880 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 381 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 891 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 798 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 645 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 632 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 656 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 534 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 625 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 661 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 325 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 326 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 455 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 533 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 326 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 408 B |
|
After Width: | Height: | Size: 112 B |
|
After Width: | Height: | Size: 825 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 831 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 408 B |