From a48717340bd99d391ecef195efc5787746ee6a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 22 Oct 2026 22:18:28 +0500 Subject: [PATCH] chore(reborn): MR3.1 semantic audit, i18n localization, and fail-closed cleanup --- crates/coa-core/src/portable/session/host.rs | 18 ++- crates/coa-core/src/portable/session/tests.rs | 14 ++ docs/REBORN_PORTABLE_AUDIT.md | 124 +++++++++++------- docs/WARCRAFT-REBORN.md | 2 +- src/i18n/locales/de.ts | 6 + src/i18n/locales/en.ts | 6 + src/i18n/locales/es.ts | 6 + src/i18n/locales/fr.ts | 6 + src/i18n/locales/ru.ts | 6 + src/i18n/locales/zh.ts | 6 + src/screens/BrowsePage.tsx | 6 +- src/screens/PortablePage.tsx | 4 +- 12 files changed, 140 insertions(+), 64 deletions(-) diff --git a/crates/coa-core/src/portable/session/host.rs b/crates/coa-core/src/portable/session/host.rs index 5030ef8..4bccc22 100644 --- a/crates/coa-core/src/portable/session/host.rs +++ b/crates/coa-core/src/portable/session/host.rs @@ -183,13 +183,17 @@ impl<'a> HostService<'a> { }); } if let Some((_, rs)) = self.server_id.split_once(':') { - if let Ok(host_ruleset) = rs.parse::() { - if offer.ruleset != host_ruleset || model.ruleset != host_ruleset { - return Err(PortableError::RulesetMismatch { - character: model.ruleset, - realm: host_ruleset, - }); - } + let host_ruleset = rs.parse::().map_err(|_| { + PortableError::Invalid(format!( + "malformed or unknown ruleset suffix in server_id: {:?}", + self.server_id + )) + })?; + if offer.ruleset != host_ruleset || model.ruleset != host_ruleset { + return Err(PortableError::RulesetMismatch { + character: model.ruleset, + realm: host_ruleset, + }); } } self.store diff --git a/crates/coa-core/src/portable/session/tests.rs b/crates/coa-core/src/portable/session/tests.rs index 1c414fe..0dd7dc7 100644 --- a/crates/coa-core/src/portable/session/tests.rs +++ b/crates/coa-core/src/portable/session/tests.rs @@ -1561,6 +1561,20 @@ fn reborn_hostile_attacks_are_rejected_with_zero_mutation() { coll_err2.is_err(), "Reborn merging wildcard:vanity must fail" ); + + // 9. HostService with malformed server_id ruleset suffixes fails closed + for bad_server_id in ["srv-1:unknown", "srv-1:garbage", "srv-1:reborn:extra"] { + let mut h_bad = HostService::new(&mut host, bad_server_id, HostConfig::default()); + let mut offer_bad = OwnerService::new(&mut owner) + .offer(id_reb, "srv-1:reborn") + .unwrap(); + offer_bad.server_id = bad_server_id.to_string(); + let res_bad = h_bad.accept_offer(host_profile, &offer_bad); + assert!( + res_bad.is_err(), + "accept_offer on malformed server id {bad_server_id} must fail closed" + ); + } } #[test] diff --git a/docs/REBORN_PORTABLE_AUDIT.md b/docs/REBORN_PORTABLE_AUDIT.md index 68016fb..953082d 100644 --- a/docs/REBORN_PORTABLE_AUDIT.md +++ b/docs/REBORN_PORTABLE_AUDIT.md @@ -1,62 +1,84 @@ -# Warcraft Reborn Portable Characters Audit +# Warcraft Reborn Portable Characters Audit & Semantic State -## 1. Overview & Baseline vs Current Implementation +## 1. Historical Baseline +The released v0.7.0 master branch codebase contained foundational support for local realm execution: +- Mode: `Mode::Reborn` +- Realm ID: `3` +- Databases: characters DB `acore_characters_reborn`, world DB `acore_world_reborn`, shared auth DB `acore_auth` +- Policy: Reborn runs as the active primary realm on the main world port (8085). Simultaneous execution with CoA/Wildcard is strictly unsupported (`"Simultaneous startup supports CoA and Wildcard only."`). -### 1.1 Baseline Discovered Before Implementation -The released v0.7.0 master branch codebase contained foundational support for local realm execution (`Mode::Reborn`, realm ID `3`, characters DB `acore_characters_reborn`, world DB `acore_world_reborn`, shared auth DB `acore_auth`). -Portable character transfers into Warcraft Reborn were temporarily guarded by: +Before the multi-ruleset implementation, portable character transfers into Warcraft Reborn were temporarily blocked by: ```rust // crates/coa-core/src/portable/realm/mod.rs Mode::Reborn => Err(PortableError::Invalid("Portable characters are not supported on Warcraft Reborn realms yet.".into())), ``` -### 1.2 Implemented in 58a99f0+ and MR1.1 -- **RulesetId / Ruleset**: `Ruleset::Reborn` is established across `coa-core`, `coa-registry-proto`, `coa-control-proto`, and TypeScript models (`"reborn"` wire value, display name `"Warcraft Reborn"`). -- **SQLite Portable Storage:** Migration `009_multiruleset.sql` upgrades released databases to permit `reborn` characters in the `character` table CHECK constraint without dropping child records. -- **Collections:** `reborn:appearance` and `reborn:vanity` defined and carried under exact allowlist in `carried_kind_for`. Partitioned under `(profile_id, 'reborn', kind)`. -- **Wire Protocols:** Control protocol v2, Session protocol v3, and Collection protocol v2 bind `ruleset: Ruleset` explicitly in all messages. +## 2. Current MR3 Implementation +In MR1.1, MR2, and MR3, generic multi-ruleset infrastructure was completed and enabled for Reborn: +- **Ruleset Identity**: `Ruleset::Reborn` established across `coa-core`, `coa-registry-proto`, `coa-control-proto`, and frontend TypeScript models (`"reborn"` wire value, display name `"Warcraft Reborn"`). +- **SQLite Storage**: Migration `009_multiruleset.sql` upgrades released databases to permit `reborn` in `character` CHECK constraints without dropping existing character rows or foreign keys. +- **Collections**: `reborn:appearance` and `reborn:vanity` defined and carried under allowlist in `carried_kind_for`. Partitioned under `(profile_id, 'reborn', kind)`. +- **Wire Protocols**: Control Protocol v2, Session Protocol v3, and Collection Protocol v2 bind `ruleset: Ruleset` explicitly in all messages. +- **Fail-Closed Routing & Ingress**: `srv-X:reborn` resolves strictly to the Reborn realm and fails closed if inactive. HostService rejects malformed server-id ruleset suffixes. +- **Automated Verification**: End-to-end claiming, export, snapshot import, runtime session lifecycle, intermediate/final checkpoints, and logout sync pass. +- **Three-Space Isolation**: Torture fixtures prove zero mutation across rulesets: + - Reborn character -> CoA / Wildcard realm (rejected with zero mutation). + - CoA / Wildcard character -> Reborn realm (rejected with zero mutation). + - Reborn collection rejects foreign namespaced entries (`coa:*`, `wildcard:*`). -### 1.3 Implemented in MR3 Vertical Slice -- Concrete support for Warcraft Reborn character import/export, claiming, session lifecycle, checkpoints, and logout sync. -- `ruleset_of(&reborn_db)` returns `Ok(Ruleset::Reborn)`. -- Reborn character class formatting correctly maps vanilla/standard classes (`reborn:class:1..11`) alongside custom classes. -- Direct ingress and routing respect active primary Reborn realm (realm ID 3, `acore_characters_reborn`, `acore_world_reborn`, port 8085) and fail closed when Reborn is inactive. -- Frontend UI displays `RulesetBadge` with human-facing labels ("Warcraft Reborn", "Wildcard", "Conquest of Azeroth"), ruleset filter tabs in Portable Characters screen, and ruleset-filtered Character Picker in the JOIN drawer. -- Three-space isolation fixture and torture tests prove complete mutation isolation: - - Reborn character -> CoA realm (rejected with zero DB mutation). - - Reborn character -> Wildcard realm (rejected with zero DB mutation). - - CoA character -> Reborn realm (rejected with zero DB mutation). - - Wildcard character -> Reborn realm (rejected with zero DB mutation). - - Reborn collections (`reborn:appearance`, `reborn:vanity`) reject foreign namespaced collections. -- Physical external-client smoke is deferred to the combined manual verification gate. +## 3. Actual Reborn Semantic-State Audit -## 2. Ruleset Identity & DB Schemas -- **RulesetId / Ruleset**: - - `Ruleset::Reborn` exists in `crates/coa-core/src/portable/model.rs` and `crates/coa-registry-proto/src/caps.rs`. - - Serialized string: `"reborn"`. - - Display name: `"Warcraft Reborn"`. - - Associated Realm ID: `3`. - - Character Database: `acore_characters_reborn`. - - World Database: `acore_world_reborn`. - - Auth Database: `acore_auth` (shared across all modes). -- **Collections**: - - `reborn:appearance` - - `reborn:vanity` - - Partitioned under `(profile_id, 'reborn', kind)` in canonical SQLite store (`portable.db`). +### 3.1 Gameplay Identity and Class Architecture +Warcraft Reborn in this core is configured via `CoA.ClassModel = "coa wcr"` / `wcr` mode: +- Classes in Warcraft Reborn are standard classic WoW classes (Warrior, Paladin, Hunter, Rogue, Priest, Death Knight, Shaman, Mage, Warlock, Druid; class IDs 1..11) rather than CoA custom classes (Barbarian, Witch Doctor, etc., IDs 12..32). +- Reborn spells, class abilities, talent trees, and creature summons use dedicated spell/creature ID ranges and corrections (e.g. `REBORN_CREATURE_OFFSET = 1100000`, spell twin scripts), which operate on standard AzerothCore character persistence structures. +- Reborn characters use the exact same character persistence schema (`acore_characters_reborn`) as vanilla/CoA character databases, cloned directly via `db.clone_structure("acore_characters", &stage_chars, &cache)` in `crates/coa-core/src/realms.rs`. -## 3. Simultaneous Startup & Multiworld Policy -- As verified in `crates/coa-core/src/multiworld.rs` and `crates/coa-core/src/realms.rs`: - - `CoA + Wildcard` simultaneous execution is supported. - - `Reborn` simultaneous execution is explicitly unsupported (`"Simultaneous startup supports CoA and Wildcard only."`). - - This policy is preserved strictly without changes. +### 3.2 Table & State Classification Matrix -## 4. Enabling Reborn in Portable Architecture -To complete the Reborn vertical slice with full strict isolation: -1. Map `Mode::Reborn => Ok(Ruleset::Reborn)` in `crates/coa-core/src/portable/realm/mod.rs:ruleset_of`. -2. Update tests verifying `ruleset_of(&reborn_db).unwrap() == Ruleset::Reborn`. -3. Verify Reborn claiming, export, import, session runtime, checkpoints, and collection isolation. -4. Verify non-negotiable isolation tests: - - Reborn character -> CoA realm (rejected). - - Reborn character -> Wildcard realm (rejected). - - CoA character -> Reborn realm (rejected). - - Wildcard character -> Reborn realm (rejected). +| Category / State | Database Table(s) | Portable Character Mapping | Classification | +| :--- | :--- | :--- | :--- | +| **Identity (name, race, class, gender)** | `characters` | `PortableCharacter.name`, `.race`, `.class`, `.gender` | **Already covered by generic PortableCharacter** | +| **Progression (level, XP, money)** | `characters` | `Progression.level`, `.xp`, `.money` | **Already covered by generic PortableCharacter** | +| **Inventory & Equipment** | `character_inventory`, `item_instance` | `Item` records, container links, slot indices, item entry | **Already covered by generic PortableCharacter** | +| **Enchants, Gems, Random Properties** | `item_instance` (`enchantments`, `randomPropertyId`, `durability`) | `Item.enchantments`, `random_property_id`, `durability` | **Already covered by generic PortableCharacter** | +| **Bags & Bank** | `character_inventory` (bag slots, bank slots) | `Item` container slots; personal bank slots | **Already covered by generic PortableCharacter** | +| **Spells** | `character_spell` | `PortableCharacter.spells` (array of spell IDs) | **Already covered by generic PortableCharacter** | +| **Talents** | `character_talent` | `PortableCharacter.talents` (spec, talent IDs, ranks) | **Already covered by generic PortableCharacter** | +| **Glyphs** | `character_glyphs` | `PortableCharacter.glyphs` (spec, slot, glyph ID) | **Already covered by generic PortableCharacter** | +| **Action Bars** | `character_action` | `PortableCharacter.actions` (button, action, type) | **Already covered by generic PortableCharacter** | +| **Skills & Weapon proficiencies** | `character_skills` | `PortableCharacter.skills` (skill ID, value, max) | **Already covered by generic PortableCharacter** | +| **Quests (Active & Rewarded)** | `character_queststatus`, `character_queststatus_rewarded` | `Quest` records, objective counters, rewarded list | **Already covered by generic PortableCharacter** | +| **Reputation** | `character_reputation` | `Reputation` records (faction ID, standing, flags) | **Already covered by generic PortableCharacter** | +| **Pets & Pet Spells** | `character_pet`, `pet_spell` | `Pet` records, entry, name, spells | **Already covered by generic PortableCharacter** | +| **Character Settings & Keybindings** | `character_settings`, `character_account_data` | `Setting` records (source, data payload) | **Already covered by generic PortableCharacter** | +| **Vanity / Transmog Collections** | `account_appearance_collection`, `account_vanity_collection` | `reborn:appearance`, `reborn:vanity` collections | **Account-wide Reborn collection** | +| **Active Auras / Buffs** | `character_aura` | Not persisted in portable snapshot | **Realm-local and intentionally not portable** (refreshed on login) | +| **Spell Cooldowns** | `character_spell_cooldown` | Not persisted in portable snapshot | **Realm-local and intentionally not portable** | +| **Daily / Weekly Quest Resets** | `character_queststatus_daily`, `weekly`, `monthly` | Not persisted in portable snapshot | **Realm-local and intentionally not portable** | +| **Instance Bindings** | `character_instance`, `character_homebind` | Not persisted in portable snapshot | **Realm-local and intentionally not portable** | +| **Battleground / Arena Ratings** | `character_arena_stats`, `character_battleground_random` | Not persisted in portable snapshot | **Realm-local and intentionally not portable** | +| **Manastorm / Challenge states** | `ascension_manastorm_*`, `coa_character_challenge` | Classified as `Blocking` if active | **Blocking** (blocks export while active challenge exists) | +| **Custom Reborn Tables** | None exist in database | N/A | **Irrelevant** (no custom tables in `acore_characters_reborn`) | + +### 3.3 Reborn Extension Requirement Decision + +```text +Reborn extension required: NO +``` + +**Rationale:** +1. Warcraft Reborn builds persistent character state entirely within standard AzerothCore character tables (`characters`, `character_spell`, `character_talent`, `character_action`, `character_skills`, `character_inventory`, `item_instance`, `character_reputation`, `character_pet`). +2. There are no separate Reborn-specific character tables in `acore_characters_reborn` (the schema is identical to `acore_characters`). +3. Reborn class abilities, spells, and talent configurations are encoded natively via standard spell IDs and talent IDs supported by `PortableCharacter.spells` and `PortableCharacter.talents`. +4. Character settings (such as build selections or client UI configs) persist through `character_settings`, which is already captured in `PortableCharacter.settings`. +5. Transmog and cosmetic appearance selections are synchronized cleanly via the `reborn:appearance` and `reborn:vanity` account collections. +6. Therefore, adding a custom typed extension is not needed and would introduce redundant schema without semantic benefit. The generic `PortableCharacter` model provides 100% semantic continuity for Reborn character builds. + +## 4. Remaining Manual Verification +- **Combined Physical Real-Client Gate**: + - Live client connection to a running Warcraft Reborn realm. + - Character creation on Reborn, progression, claiming to Manager. + - Transfer/import to a secondary Reborn realm and runtime session verification. + - Verification that CoA and Wildcard realms remain completely isolated. + - Status: **PENDING COMBINED MANUAL GATE**. diff --git a/docs/WARCRAFT-REBORN.md b/docs/WARCRAFT-REBORN.md index fddbd1e..0fbbdf1 100644 --- a/docs/WARCRAFT-REBORN.md +++ b/docs/WARCRAFT-REBORN.md @@ -7,7 +7,7 @@ have its saved realm updated immediately. Accounts are shared with your other local realms. Warcraft Reborn starts with separate characters; switching profiles does not copy existing characters or progress. -Bots are unavailable on Warcraft Reborn. Other optional modules are experimental. Portable character transfers into Warcraft Reborn are not supported. **Portable characters remain experimental.** +Bots are unavailable on Warcraft Reborn. Other optional modules are experimental. Portable Characters support for Warcraft Reborn is implemented experimentally. Automated isolation/import/session tests pass. Physical real-client verification is still pending the combined manual gate. **Portable characters remain experimental.** Backups and updates include every created realm. Check the realm recorded in a recovery point before restoring it. diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 95413e0..d2b72ea 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -888,6 +888,12 @@ const de: Partial> = { "crash.hideRaw": "Vollständigen Bericht verbergen", "crash.copyReport": "Bericht kopieren", "crash.copied": "Kopiert!", + "portable.filter.none": "Keine Charaktere entsprechen dem ausgewählten Spielmodus.", + "portable.filter.all": "Alle", + "browse.hidden_characters_one": "{n} Charakter aus anderen Spielmodi ist ausgeblendet", + "browse.hidden_characters_few": "{n} Charaktere aus anderen Spielmodi sind ausgeblendet", + "browse.hidden_characters_many": "{n} Charaktere aus anderen Spielmodi sind ausgeblendet", + "browse.hidden_characters_other": "{n} Charaktere aus anderen Spielmodi sind ausgeblendet", }; export default de; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 1cbbb73..10a28fd 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -1029,6 +1029,12 @@ const en = { "portable.err.not_prepared": "Only a realm added from a file can be removed here.", "portable.err.launch": "The game could not be started. Check the client in Settings.", "portable.err.other": "Something went wrong. Nothing was lost.", + "portable.filter.none": "No characters match the selected game mode.", + "portable.filter.all": "All", + "browse.hidden_characters_one": "{n} character from other game modes is hidden", + "browse.hidden_characters_few": "{n} characters from other game modes are hidden", + "browse.hidden_characters_many": "{n} characters from other game modes are hidden", + "browse.hidden_characters_other": "{n} characters from other game modes are hidden", "portable.diag.title": "Diagnostics", "portable.diag.text": "A report for a bug report: realms, versions, sessions and recent problems. No passwords and no character data.", "portable.diag.copy": "Copy diagnostics", diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index bfcda03..16d31b0 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -888,6 +888,12 @@ const es: Partial> = { "crash.hideRaw": "Ocultar informe completo", "crash.copyReport": "Copiar informe", "crash.copied": "¡Copiado!", + "portable.filter.none": "Ningún personaje coincide con el modo de juego seleccionado.", + "portable.filter.all": "Todos", + "browse.hidden_characters_one": "{n} personaje de otros modos de juego está oculto", + "browse.hidden_characters_few": "{n} personajes de otros modos de juego están ocultos", + "browse.hidden_characters_many": "{n} personajes de otros modos de juego están ocultos", + "browse.hidden_characters_other": "{n} personajes de otros modos de juego están ocultos", }; export default es; diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index ce75aaa..06334bb 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -888,6 +888,12 @@ const fr: Partial> = { "crash.hideRaw": "Masquer le rapport complet", "crash.copyReport": "Copier le rapport", "crash.copied": "Copié !", + "portable.filter.none": "Aucun personnage ne correspond au mode de jeu sélectionné.", + "portable.filter.all": "Tous", + "browse.hidden_characters_one": "{n} personnage d'autres modes de jeu est masqué", + "browse.hidden_characters_few": "{n} personnages d'autres modes de jeu sont masqués", + "browse.hidden_characters_many": "{n} personnages d'autres modes de jeu sont masqués", + "browse.hidden_characters_other": "{n} personnages d'autres modes de jeu sont masqués", }; export default fr; diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index c162632..d92939f 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -1011,6 +1011,12 @@ const ru: Partial> = { "portable.err.not_prepared": "Здесь можно убрать только реалм, добавленный из файла.", "portable.err.launch": "Не удалось запустить игру. Проверьте клиент в настройках.", "portable.err.other": "Что-то пошло не так. Ничего не потеряно.", + "portable.filter.none": "Нет персонажей для выбранного игрового режима.", + "portable.filter.all": "Все", + "browse.hidden_characters_one": "{n} персонаж из других игровых режимов скрыт", + "browse.hidden_characters_few": "{n} персонажа из других игровых режимов скрыто", + "browse.hidden_characters_many": "{n} персонажей из других игровых режимов скрыто", + "browse.hidden_characters_other": "{n} персонажей из других игровых режимов скрыто", "portable.diag.title": "Диагностика", "portable.diag.text": "Отчёт для сообщения об ошибке: реалмы, версии, сессии и недавние проблемы. Без паролей и без данных персонажей.", "portable.diag.copy": "Скопировать диагностику", diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index dee58b9..93b8710 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -890,6 +890,12 @@ const zh = { "crash.hideRaw": "隐藏完整日志", "crash.copyReport": "复制崩溃报告", "crash.copied": "已复制!", + "portable.filter.none": "没有符合所选游戏模式的角色。", + "portable.filter.all": "全部", + "browse.hidden_characters_one": "已隐藏 {n} 个其他游戏模式的角色", + "browse.hidden_characters_few": "已隐藏 {n} 个其他游戏模式的角色", + "browse.hidden_characters_many": "已隐藏 {n} 个其他游戏模式的角色", + "browse.hidden_characters_other": "已隐藏 {n} 个其他游戏模式的角色", } as const; export default zh; diff --git a/src/screens/BrowsePage.tsx b/src/screens/BrowsePage.tsx index f6961c9..988d612 100644 --- a/src/screens/BrowsePage.tsx +++ b/src/screens/BrowsePage.tsx @@ -68,7 +68,7 @@ function SortHeader({ id, sort, order, onSort, children, className }: { id: Sort } function Drawer({ summary, catalog, onClose }: { summary: RealmSummary; catalog: Catalog; onClose: () => void }) { - const { t, locale } = useI18n(); + const { t, tn, locale } = useI18n(); const { state, refresh: refreshPortable } = usePortable(); const [detail, setDetail] = useState(null); const [error, setError] = useState(null); @@ -159,7 +159,7 @@ function Drawer({ summary, catalog, onClose }: { summary: RealmSummary; catalog:

{t("browse.noCharacter")}

{hiddenCount > 0 && (

- {hiddenCount} {hiddenCount === 1 ? "character" : "characters"} from other game modes {hiddenCount === 1 ? "is" : "are"} hidden + {tn("browse.hidden_characters", hiddenCount)}

)} @@ -171,7 +171,7 @@ function Drawer({ summary, catalog, onClose }: { summary: RealmSummary; catalog: {hiddenCount > 0 && (

- {hiddenCount} {hiddenCount === 1 ? "character" : "characters"} from other game modes {hiddenCount === 1 ? "is" : "are"} hidden + {tn("browse.hidden_characters", hiddenCount)}

)} diff --git a/src/screens/PortablePage.tsx b/src/screens/PortablePage.tsx index b688013..fd6f436 100644 --- a/src/screens/PortablePage.tsx +++ b/src/screens/PortablePage.tsx @@ -318,7 +318,7 @@ export function PortablePage({ onOpenSettings }: { onOpenSettings?: () => void } rulesetFilter === "" ? "bg-white/10 text-ink shadow-sm" : "text-muted hover:text-ink" )} > - {t("browse.any")} + {t("portable.filter.all")}