feat(strings): the app's words for every game (M20) #58

Merged
whitlocktech merged 5 commits from feat/app-vocabulary into edge 2026-10-23 01:50:51 -05:00
11 changed files with 707 additions and 407 deletions

View File

@@ -181,6 +181,13 @@ dependencies {
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
}
// StringVocabularyTest (PLAN.md M20) reads the string resources from disk. A reworded
// string changes no class, so without this input the test task would stay UP-TO-DATE
// and the guard would not run again.
tasks.withType<Test>().configureEach {
inputs.dir("src/main/res").withPathSensitivity(PathSensitivity.RELATIVE)
}
// JaCoCo XML coverage from the JVM unit tests, consumed by SonarQube (§12.1). Generated,
// DI (Hilt), and Compose-scaffold classes are excluded so they don't dilute the number;
// pure-@Composable UI is excluded on the Sonar side (sonar.coverage.exclusions) because

View File

@@ -196,8 +196,21 @@ val APP_MENU: List<MenuEntry> = listOf(
// Staff operations (§1, M10) — revealed for staff roles; the backend re-checks every call.
MenuEntry(Routes.ADMIN_DASHBOARD, R.string.menu_admin_dashboard, MenuAccess.STAFF),
MenuEntry(Routes.ADMIN_CONTENT, R.string.menu_admin_content, MenuAccess.STAFF),
MenuEntry(Routes.ADMIN_MODERATION, R.string.menu_admin_moderation, MenuAccess.MODERATOR),
MenuEntry(Routes.ADMIN_SUPPORT, R.string.menu_admin_support, MenuAccess.MODERATOR),
// Moderation and the support queue act on the shard (`/admin/shard/*`, module-uo's
// admin mount), so they need the module installed like the player rows do. On a
// Rust-only site they once showed a UO form whose every action failed (M20).
MenuEntry(
Routes.ADMIN_MODERATION,
R.string.menu_admin_moderation,
MenuAccess.MODERATOR,
capability = Capability.SHARD,
),
MenuEntry(
Routes.ADMIN_SUPPORT,
R.string.menu_admin_support,
MenuAccess.MODERATOR,
capability = Capability.SHARD,
),
)
/**

View File

@@ -161,7 +161,7 @@ fun describe(row: RustEventDto): FeedLine {
*/
private fun death(row: RustEventDto, name: String?): FeedLine {
val where = listOfNotNull(
row.str("weapon")?.let { "with ${prefabName(it)}" },
row.str("weapon")?.let { "with ${weaponName(it, row.str("weaponName"))}" },
row.num("distance")?.let { "${Math.round(it)}m" },
row.str("grid"),
if (row.flag("sleeping")) "while sleeping" else null,

View File

@@ -159,6 +159,23 @@ fun prefabName(name: String?): String {
return name.replace(Regex("[_.]+"), " ").trim()
}
/** The endings a held entity's prefab name carries that no player says. */
private val WEAPON_ENDING = Regex("[._](entity|weapon|deployed)$")
/**
* The weapon in a kill, the Kotlin half of `module-rust`'s `format.js weapon()`.
* The module sends `weaponName` (Module-Rust#60), which is the game's own name for a
* gun or the prefab name as words (`server/model/titles/weaponNames.js`), and the
* app prints that. A module too old to send it gets the same fallback here: the
* `.entity` / `.weapon` / `.deployed` ending is dropped, then [prefabName] turns the
* rest into words. So the app never prints "ak47u entity".
*/
fun weaponName(weapon: String?, weaponName: String?): String {
if (weapon.isNullOrBlank()) return ""
if (!weaponName.isNullOrBlank()) return weaponName
return prefabName(weapon.trim().replace(WEAPON_ENDING, ""))
}
/**
* An Economics balance (`docs/modules/rust/PLAN_REDESIGNS.md` §11.2.2): grouped,
* at most two places, and a dash for one the server never reported — not "0",

View File

@@ -4,9 +4,14 @@
All user-facing strings are externalized from day one (PLAN.md §2): English is
the only bundled locale, but the structure invites community translations.
No hardcoded UI strings in Kotlin.
The strings are split by owner (PLAN.md M20), and Android merges the three files
into one R.string: this file is CORE — every screen any deployment can show — and
uses core's words only. A game's words live in strings_uo.xml and strings_rust.xml.
StringVocabularyTest fails the build when a game's word lands in the wrong file.
-->
<resources>
<!-- Fixed launcher name, baked at build even though in-app branding is per-shard (PLAN.md §13). -->
<!-- Fixed launcher name, baked at build even though in-app branding is per-site (PLAN.md §13). -->
<string name="app_name">Runic Gateway</string>
<!-- ── Shared actions / errors ─────────────────────────────────────── -->
@@ -15,22 +20,22 @@
<string name="error_network">Can\'t reach the site. Check your connection and try again.</string>
<string name="error_not_found">This content couldn\'t be found.</string>
<string name="error_rate_limited">Too many requests. Please try again in a moment.</string>
<string name="error_shard_offline">The shard is offline right now.</string>
<string name="error_feature_unavailable">This shard doesn\'t publish this here.</string>
<string name="error_shard_offline">The game server is offline right now.</string>
<string name="error_feature_unavailable">This site doesn\'t publish this here.</string>
<string name="error_server">Something went wrong on the server. Please try again.</string>
<!-- ── First-run connect (§3) ──────────────────────────────────────── -->
<string name="connect_title">Connect to your shard</string>
<string name="connect_subtitle">Enter your shard\'s website address to get started.</string>
<string name="connect_title">Connect to your community</string>
<string name="connect_subtitle">Enter your community\'s website address to get started.</string>
<string name="connect_url_label">Website address</string>
<string name="connect_url_hint">https://your-shard.example.com</string>
<string name="connect_url_hint">https://your-community.example.com</string>
<string name="connect_button">Connect</string>
<string name="connect_connecting">Connecting…</string>
<string name="connect_error_blank">Enter a website address.</string>
<string name="connect_error_malformed">That doesn\'t look like a valid web address.</string>
<string name="connect_error_scheme">Only http and https addresses are supported.</string>
<string name="connect_error_insecure">A secure https address is required.</string>
<string name="connect_error_not_runic">That site isn\'t a Runic Gateway shard.</string>
<string name="connect_error_not_runic">That site isn\'t a Runic Gateway site.</string>
<string name="connect_error_version">This app is out of date for that site (it speaks API %1$s). Update the app and try again.</string>
<string name="connect_error_unreachable">Couldn\'t reach that site. Check the address and your connection.</string>
<string name="connect_error_server">The site responded with an error (%1$d). Try again shortly.</string>
@@ -43,23 +48,12 @@
<string name="menu_news">News</string>
<string name="menu_events">Events</string>
<string name="menu_wiki">Wiki</string>
<string name="menu_shard">Shard</string>
<string name="menu_rust">Rust servers</string>
<string name="menu_rules">Rules</string>
<string name="menu_atlas">Atlas</string>
<string name="menu_leaderboards">Leaderboards</string>
<string name="menu_market">Market</string>
<string name="menu_about">About</string>
<string name="menu_contact">Contact</string>
<string name="menu_account">My account</string>
<string name="menu_my_events">My events</string>
<string name="menu_my_characters">My characters</string>
<string name="menu_my_vendors">My vendors</string>
<string name="menu_my_houses">My houses</string>
<string name="menu_admin_dashboard">Dashboard</string>
<string name="menu_admin_content">Content</string>
<string name="menu_admin_moderation">Moderation</string>
<string name="menu_admin_support">Support queue</string>
<string name="menu_sign_in">Sign in</string>
<string name="menu_sign_out">Sign out</string>
<string name="menu_change_server">Change server</string>
@@ -110,41 +104,9 @@
<string name="admin_content_title_required">A title is required.</string>
<string name="admin_content_cat_fields_required">Slug and title are required.</string>
<!-- Staff moderation (shard write plane) -->
<string name="admin_mod_account_action">Account action</string>
<string name="admin_mod_account">Account</string>
<string name="admin_mod_serial">Serial (0x…)</string>
<string name="admin_mod_reason">Reason (ban)</string>
<string name="admin_mod_duration">Ban duration (seconds; blank = indefinite)</string>
<string name="admin_mod_kick">Kick</string>
<string name="admin_mod_ban">Ban</string>
<string name="admin_mod_unban">Unban</string>
<string name="admin_mod_broadcast_section">Broadcast</string>
<string name="admin_mod_broadcast_text">Message to everyone online</string>
<string name="admin_mod_broadcast">Broadcast</string>
<string name="admin_mod_kicked">Account kicked.</string>
<string name="admin_mod_banned">Account banned.</string>
<string name="admin_mod_unbanned">Ban cleared.</string>
<string name="admin_mod_broadcasted">Message broadcast.</string>
<string name="admin_mod_target_required">Enter an account or serial.</string>
<string name="admin_mod_text_required">Enter a message to broadcast.</string>
<string name="admin_mod_shard_offline">The shard is offline — the action couldn\'t be delivered.</string>
<!-- Staff support queue -->
<string name="admin_support_empty">No open help pages.</string>
<string name="admin_support_reply">Reply</string>
<string name="admin_support_close">Close</string>
<string name="admin_support_send">Send</string>
<string name="admin_support_message">Reply message</string>
<string name="admin_support_close_after">Close the page after replying</string>
<string name="admin_support_responded">Reply sent.</string>
<string name="admin_support_closed">Page closed.</string>
<string name="admin_support_message_required">Enter a reply message.</string>
<string name="admin_support_unknown_page">That page is no longer in the queue.</string>
<!-- ── Auth: login (§4.1) ──────────────────────────────────────────── -->
<string name="login_title">Sign in</string>
<string name="login_subtitle">Sign in with your shard account.</string>
<string name="login_subtitle">Sign in with your account on this site.</string>
<string name="login_username">Username</string>
<string name="login_password">Password</string>
<string name="login_totp_code">Authentication code</string>
@@ -152,7 +114,7 @@
<string name="login_button">Sign in</string>
<string name="login_register">Create an account</string>
<string name="login_forgot">Forgot your password?</string>
<!-- Single SSO entry point; the picker lists the shard's providers (native SSO, M9/M10). -->
<!-- Single SSO entry point; the picker lists the site's providers (native SSO, M9/M10). -->
<string name="login_sso_button">Sign in with SSO</string>
<string name="login_sso_pick_title">Choose a sign-in provider</string>
<!-- %1$s is the provider name, e.g. "Google" or "Discord". -->
@@ -256,77 +218,7 @@
<string name="recovery_codes_share">Share</string>
<string name="recovery_codes_done">Done</string>
<!-- ── Player: game-account linking (§6.3) ─────────────────────────── -->
<string name="player_link_title">Link your game account</string>
<string name="player_link_hint">In game, type [link to get a one-time code, then enter it here to see your characters, vendors and houses.</string>
<string name="player_link_code">Link code</string>
<string name="player_link_action">Link account</string>
<string name="player_link_ok">Account linked.</string>
<string name="player_link_bad_code">That code is unknown or has expired. Run [link in game for a new one.</string>
<string name="player_link_error">Couldn\'t link that code. Please try again.</string>
<string name="player_create_title">Create a game account</string>
<string name="player_create_account">Account name</string>
<string name="player_create_password">Password</string>
<string name="player_create_action">Create account</string>
<string name="player_create_ok">Game account created and linked.</string>
<string name="player_create_taken">That account name is already taken.</string>
<string name="player_create_capped">The account limit for your network has been reached.</string>
<string name="player_create_unavailable">Game-account signup isn\'t available right now.</string>
<string name="player_create_rejected">That account name or password wasn\'t accepted.</string>
<string name="player_create_error">Couldn\'t create that account. Please try again.</string>
<!-- ── Player: characters (§6.3) ───────────────────────────────────── -->
<string name="player_characters_empty">Link a game account above to see your characters.</string>
<string name="player_roster_loading">Loading…</string>
<string name="player_roster_empty">No characters on this account.</string>
<string name="player_char_unknown">Unknown</string>
<string name="player_char_online">Online</string>
<string name="player_char_offline">Offline</string>
<!-- Character sheet -->
<string name="player_char_governor">Governor of %1$s</string>
<string name="player_char_guildmaster">Guildmaster, %1$s</string>
<string name="player_char_attributes">Attributes</string>
<string name="player_char_vitals">Vitals</string>
<string name="player_char_str">STR</string>
<string name="player_char_dex">DEX</string>
<string name="player_char_int">INT</string>
<string name="player_char_hits">Hits</string>
<string name="player_char_mana">Mana</string>
<string name="player_char_stam">Stamina</string>
<string name="player_char_resistances">Resistances</string>
<string name="player_char_phys">Physical</string>
<string name="player_char_fire">Fire</string>
<string name="player_char_cold">Cold</string>
<string name="player_char_pois">Poison</string>
<string name="player_char_energy">Energy</string>
<string name="player_char_skills">Skills</string>
<string name="player_char_points">Loyalty &amp; Points</string>
<!-- A point system's name followed by the character's rank on that board, e.g. "Queens Loyalty · #3". -->
<string name="player_char_points_ranked">%1$s · #%2$d</string>
<!-- A score against its cap. Only shown for capped systems; an uncapped score shows the number alone. -->
<string name="player_char_points_of">%1$d / %2$d</string>
<string name="player_char_equipment">Equipment</string>
<string name="player_char_item">Item</string>
<string name="player_char_item_id">id %1$d</string>
<string name="player_char_item_hue">hue %1$d</string>
<!-- ── Player: vendors &amp; sales (§6.3) ──────────────────────────── -->
<string name="player_sales_title">Recent sales</string>
<string name="player_sales_empty">No recent sales.</string>
<string name="player_gold_amount">%1$s gp</string>
<string name="player_vendors_no_accounts">Link a game account to see your vendors.</string>
<string name="player_vendors_empty">No vendors on this account.</string>
<string name="player_vendor_fallback">Player vendor</string>
<string name="player_vendor_hold">Holding %1$s gp</string>
<string name="player_vendor_no_listings">No items listed.</string>
<string name="player_listing_item">Item %1$d ×%2$d</string>
<!-- ── Player: houses (§6.3) ───────────────────────────────────────── -->
<string name="player_houses_empty">You don\'t have any houses on your linked accounts.</string>
<string name="player_house_fallback">A house</string>
<string name="player_house_stage">Status: %1$s</string>
<!-- ── Roles (the account screen, §5) ──────────────────────────────── -->
<string name="role_player">Player</string>
<string name="role_moderator">Moderator</string>
<string name="role_editor">Editor</string>
@@ -365,140 +257,19 @@
<string name="contact_validation">Please fill in every field.</string>
<string name="contact_error">Couldn\'t send your message. Please try again.</string>
<!-- ── Shard hub &amp; live feed (§6.2) ─────────────────────────────── -->
<string name="shard_live">Live</string>
<string name="shard_offline">Offline</string>
<string name="shard_status_online">Shard online</string>
<string name="shard_status_offline">Shard offline</string>
<string name="shard_online_count">%1$d players online</string>
<string name="shard_economy_gold">Gold supply: %1$s</string>
<string name="shard_section_staff">Staff online</string>
<string name="shard_section_activity">Live activity</string>
<string name="shard_feed_empty">No recent activity.</string>
<string name="shard_nav_champs">Champion spawns</string>
<string name="shard_nav_guilds">Guilds</string>
<string name="shard_nav_governors">Governors</string>
<string name="shard_nav_houses">Falling houses</string>
<!-- ── Champion spawns (§6.2) ──────────────────────────────────────── -->
<string name="champs_empty">No champion spawns are being tracked right now.</string>
<string name="champs_fallback_name">Champion spawn</string>
<string name="champ_level">Level %1$d</string>
<string name="champ_cooldown">Restarting</string>
<!-- ── Guilds (§6.2) ───────────────────────────────────────────────── -->
<string name="guilds_empty">No guilds are being tracked right now.</string>
<string name="guilds_fallback_name">Guild</string>
<string name="guilds_members">%1$d members</string>
<string name="guilds_leader">Led by %1$s</string>
<string name="guilds_alliance">Alliance: %1$s</string>
<!-- ── Rules / ruleset (Protocol 3.0 §5, M11) ──────────────────────── -->
<!-- A successful read of a shard that has never published its ruleset — NOT the same
as the feature being switched off, which renders as an error state. -->
<string name="rules_unpublished">This shard hasn\'t published its ruleset yet.</string>
<string name="rules_section_shard">Shard</string>
<string name="rules_section_systems">Systems</string>
<string name="rules_section_caps">Skill &amp; stat caps</string>
<string name="rules_section_accounts">Accounts &amp; housing</string>
<string name="rules_section_vendors">Vendors</string>
<string name="rules_section_schedule">Saves &amp; restarts</string>
<string name="rules_name">Name</string>
<string name="rules_expansion">Expansion</string>
<string name="rules_connect">Connect</string>
<string name="rules_skill_cap">Individual skill cap</string>
<string name="rules_total_skill_cap">Total skill cap</string>
<string name="rules_stat_cap">Total stat cap</string>
<string name="rules_str_cap">Strength cap</string>
<string name="rules_dex_cap">Dexterity cap</string>
<string name="rules_int_cap">Intelligence cap</string>
<string name="rules_char_slots">Character slots</string>
<string name="rules_per_ip">Accounts per IP</string>
<string name="rules_house_limit">Houses per account</string>
<string name="rules_restock_delay">Vendor restock delay</string>
<string name="rules_max_sell">Max sell quantity</string>
<string name="rules_autosave">Auto-save every</string>
<string name="rules_autorestart">Auto-restart at</string>
<string name="rules_minutes">%1$d min</string>
<!-- ── Leaderboards (Protocol 3.0 §7, M11) ─────────────────────────── -->
<string name="leaderboards_empty">This shard isn\'t publishing any leaderboards yet.</string>
<string name="leaderboards_board_empty">Nobody has scored here yet.</string>
<!-- Where a score would sit on the placeholder row of an unscored board. An em
dash, not "0" — nobody has scored zero, nobody has scored at all. -->
<string name="leaderboards_no_score">—</string>
<string name="leaderboards_players">%1$d players</string>
<!-- Only shown for capped systems; most systems on a real shard are uncapped. -->
<string name="leaderboards_cap">Cap: %1$d</string>
<string name="leaderboards_rank_name">#%1$d %2$s</string>
<!-- A shard may publish standings without naming who holds them (the board's one
admin-configurable field). -->
<string name="leaderboards_hidden_name">Someone</string>
<!-- ── Market (Protocol 3.0 §8, M11) ───────────────────────────────── -->
<string name="market_search_label">Search every shop</string>
<string name="market_empty">No listings match that search.</string>
<string name="market_shop_empty">This shop has nothing for sale.</string>
<!-- Required, not decoration: the shard sweeps vendors round-robin, so a price can
legitimately be a full cycle old. -->
<string name="market_staleness">Prices are refreshed in rotation and may be out of date.</string>
<string name="market_price">%1$d gp</string>
<string name="market_unnamed_item">Item %1$d</string>
<string name="market_unnamed_shop">A shop</string>
<string name="market_owner">Kept by %1$s</string>
<!-- A gated location is a real answer: the shop exists, the shard just doesn't say
where it stands. -->
<string name="market_location_hidden">This shard doesn\'t publish shop locations.</string>
<string name="market_truncated">Showing %1$d of %2$d — this shop holds more than the shard publishes.</string>
<!-- ── Spawn atlas (Protocol 3.0 §6, M11) ──────────────────────────── -->
<string name="atlas_search_label">Search creatures</string>
<string name="atlas_empty">No creatures match that search.</string>
<!-- A place can legitimately hold a single spawner, and the aggregate list is full
of them — "1 spawners" on every other row is worth a plural for. -->
<plurals name="atlas_spawner_count">
<item quantity="one">%1$d spawner</item>
<item quantity="other">%1$d spawners</item>
</plurals>
<string name="atlas_total_alive">Up to %1$d alive at once</string>
<string name="atlas_facet_count">%1$s (%2$d)</string>
<!-- The aggregate: "where is it", as opposed to the raw coordinates below it. -->
<string name="atlas_section_places">Where it spawns</string>
<string name="atlas_place_max_alive">up to %1$d at once</string>
<string name="atlas_section_spawners">Spawn points</string>
<string name="atlas_section_also_here">Also spawns here</string>
<string name="atlas_spawners_truncated">More spawn points than shown.</string>
<string name="atlas_max_count">Up to %1$d</string>
<!-- The delay is already in seconds; the server normalizes XmlSpawner's mixed units. -->
<string name="atlas_respawn">Respawn %1$s</string>
<!-- ── Governors (§6.2) ────────────────────────────────────────────── -->
<string name="governors_empty">No governors — this shard may not run the City Loyalty system.</string>
<string name="governor_current">Governed by %1$s</string>
<string name="governor_none">No governor</string>
<string name="governor_election">Election: %1$s</string>
<string name="governor_history">Term history</string>
<string name="governor_history_loading">Loading…</string>
<string name="governor_history_empty">No past terms recorded.</string>
<!-- ── Falling houses / IDOC (§6.2) ────────────────────────────────── -->
<string name="houses_empty">No houses are in danger right now.</string>
<string name="houses_fallback_name">A house</string>
<string name="houses_idoc_badge">IDOC</string>
<!-- ── Push notifications (§11, M7 Part 2) ─────────────────────────── -->
<string name="menu_notifications">Notifications</string>
<string name="notifications_title">Notifications</string>
<string name="notifications_subtitle">Choose what this shard notifies you about, and how it reaches you. Nothing is sent unless you turn it on.</string>
<string name="notifications_subtitle">Choose what this site notifies you about, and how it reaches you. Nothing is sent unless you turn it on.</string>
<string name="notifications_section_general">General</string>
<string name="notifications_section_personal">Your game account</string>
<string name="notifications_requires_link">Link a game account to get this as a push notification.</string>
<string name="notifications_empty">This shard offers no notification streams yet.</string>
<string name="notifications_unsupported">This shard hasn\'t set up push notifications yet.</string>
<string name="notifications_empty">This site offers no notification streams yet.</string>
<string name="notifications_unsupported">This site hasn\'t set up push notifications yet.</string>
<string name="notifications_saved">Notification settings saved.</string>
<string name="notifications_all_off">Notifications turned off.</string>
<string name="notifications_save_error">Couldn\'t save your notification settings. Try again.</string>
<string name="notifications_relay_error">This shard\'s push relay isn\'t reachable right now.</string>
<string name="notifications_relay_error">This site\'s push relay isn\'t reachable right now.</string>
<!-- The in-app inbox and the per-channel settings (ENGAGEMENT.md phase 8). -->
<string name="notifications_mode_off">Off</string>
@@ -513,22 +284,15 @@
<string name="inbox_offline_cached">Offline — showing what was saved on this device.</string>
<!-- Notification channels + the ongoing foreground-service notification. -->
<string name="push_channel_messages">Shard notifications</string>
<string name="push_channel_messages_desc">Alerts you opted into from this shard.</string>
<string name="push_channel_messages">Notifications from your community</string>
<string name="push_channel_messages_desc">Alerts you opted into from this site.</string>
<string name="push_channel_service">Background connection</string>
<string name="push_channel_service_desc">Keeps the connection open to deliver notifications.</string>
<string name="push_service_title">Notifications active</string>
<string name="push_service_text">Listening for shard notifications.</string>
<string name="push_service_text">Listening for notifications from your community.</string>
<!-- Per-stream notification titles (content-free tickle → generic title, §11). -->
<string name="push_stream_news_post">New post</string>
<string name="push_stream_server_status">Shard status changed</string>
<string name="push_stream_idoc_warning">A house is falling (IDOC)</string>
<string name="push_stream_champ_start">Champion spawn started</string>
<string name="push_stream_governor_election">New governor elected</string>
<string name="push_stream_vendor_sale">Your vendor made a sale</string>
<string name="push_stream_house_idoc">Your house entered IDOC</string>
<string name="push_stream_account_login">Login to your account</string>
<string name="push_stream_generic">New notification</string>
<!-- ── Events (§9 M13, EVENTS.md §I) ─────────────────────────── -->
@@ -571,150 +335,4 @@
<string name="events_score">Score %1$s</string>
<string name="events_show_more">Show more</string>
<string name="events_loading">Loading…</string>
<!-- Rust module (M14, docs/modules/rust/PLAN.md §18) -->
<string name="rust_servers_empty">No Rust servers are configured on this site yet.</string>
<string name="rust_offline">Offline</string>
<string name="rust_online_count">%1$d / %2$d online</string>
<string name="rust_never_reported">has never reported</string>
<string name="rust_last_reported">last reported %1$s</string>
<string name="rust_last_reported_stale">last reported %1$s — out of date, so it is shown as offline</string>
<string name="rust_world_size">size %1$d</string>
<string name="rust_world_seed">seed %1$d</string>
<string name="rust_wiped_on">wiped %1$s</string>
<string name="rust_next_wipe">Next wipe %1$s</string>
<string name="rust_next_wipe_rescheduled">Next wipe %1$s (rescheduled)</string>
<string name="rust_refresh_failed">Could not refresh just now. This is the last thing the site heard.</string>
<string name="rust_no_such_server">No such server</string>
<string name="rust_no_such_server_detail">This address does not name a server this site follows.</string>
<string name="rust_back_to_servers">Back to the server list</string>
<string name="rust_tab_feed">Feed</string>
<string name="rust_tab_leaderboard">Leaderboard</string>
<string name="rust_tab_online">Online</string>
<string name="rust_tab_wipes">Wipes</string>
<string name="rust_tab_map">Map</string>
<!-- The Map tab (Rust phase 15, D121–D125). The wording follows the web's Map
tab, so a reader moving between the two reads the same sentences. -->
<string name="rust_map_none">No map yet. This server has not told the site which map it is on. The map will appear once the server is up.</string>
<string name="rust_map_no_picture">This server has no picture of its map, so the layers are drawn on a plain background.</string>
<string name="rust_map_picture_failed">The picture of the map could not be loaded, so the layers are drawn on a plain background.</string>
<string name="rust_map_description">Map of the server</string>
<string name="rust_map_status_nothing_live">No moving layers are shown to you on this server.</string>
<string name="rust_map_status_asking">Asking the server where things are…</string>
<string name="rust_map_status_not_now">The server did not say where things are just now. The site asks again every ten seconds.</string>
<string name="rust_map_status_offline">The server is offline, so nothing is moving on its map.</string>
<string name="rust_map_status_failed">Positions could not be loaded.</string>
<string name="rust_map_status_failed_kept">Could not refresh just now. These are the last positions the site heard.</string>
<string name="rust_map_status_as_of">Positions as of %1$s. They refresh every ten seconds while this tab is open.</string>
<string name="rust_map_layer_grid">Grid</string>
<string name="rust_map_layer_world">Monuments &amp; world events</string>
<string name="rust_map_layer_events">Site events</string>
<string name="rust_map_layer_players">Players</string>
<string name="rust_map_layer_bases">Bases</string>
<string name="rust_map_layer_mates">You and your clan</string>
<string name="rust_map_hidden_signin">Sign in to see this layer.</string>
<string name="rust_map_hidden_public">Shown to everyone.</string>
<string name="rust_map_hidden_staff">Shown to staff only.</string>
<string name="rust_map_capped">%1$s Limited by who may see who is online.</string>
<string name="rust_map_players_truncated">Not every sleeper is shown.</string>
<string name="rust_map_bases_truncated">Not every base is shown.</string>
<string name="rust_map_mates_note">Your own position, and clan mates who are online.</string>
<string name="rust_map_mates_link">Link your Steam account to see yourself and your clan here.</string>
<string name="rust_map_cargo">Cargo ship</string>
<string name="rust_map_heli">Patrol helicopter</string>
<string name="rust_map_chinook">Chinook</string>
<string name="rust_map_bradley">Bradley APC</string>
<string name="rust_map_supply">Supply drop</string>
<string name="rust_map_crate">Locked crate</string>
<string name="rust_map_crate_hack">%1$s left on the hack</string>
<string name="rust_map_crate_hacked">Hacked</string>
<string name="rust_map_event_zone">Event zone</string>
<string name="rust_map_event_crate">Event crate</string>
<string name="rust_map_event_npc">Event NPC</string>
<string name="rust_map_site_event">Site event</string>
<string name="rust_map_site_event_named">Site event · %1$s</string>
<string name="rust_map_boss">%1$s (boss)</string>
<string name="rust_map_boss_unnamed">Boss</string>
<string name="rust_map_boss_add">%1$s (a boss\'s add)</string>
<string name="rust_map_open_event">Open event</string>
<string name="rust_map_tc">Tool cupboard</string>
<string name="rust_map_vending">Vending machine</string>
<string name="rust_map_player_sleeping">Sleeping</string>
<string name="rust_map_player_offline">Asleep, offline</string>
<string name="rust_map_you">You</string>
<string name="rust_map_clan_mate">Clan mate</string>
<string name="rust_map_grid">Grid %1$s</string>
<string name="rust_map_close">Close</string>
<string name="rust_all_time">All time</string>
<string name="rust_wipe_current">%1$s (this wipe)</string>
<string name="rust_wipe_this_one">Current</string>
<string name="rust_wipe_selected">Showing</string>
<string name="rust_feed_empty">Nothing has happened on this server yet — or not during the wipe you are looking at.</string>
<string name="rust_leaderboard_empty">Nobody has scored here yet.</string>
<string name="rust_wipes_empty">This server has not reported a wipe yet.</string>
<!-- Nothing names who is online by default (org lead, 2026-09-22). Below the
operator's audience the server sends a count and no names. -->
<plurals name="rust_online_hidden_count">
<item quantity="one">%1$d player online</item>
<item quantity="other">%1$d players online</item>
</plurals>
<string name="rust_online_hidden_staff">Only this site’s staff can see who they are.</string>
<string name="rust_online_hidden_signin">Sign in to see who they are.</string>
<string name="rust_online_hidden_public">This site is not showing who they are right now.</string>
<string name="rust_feed_presence_hidden_staff">Joins, deaths and chat are not shown. Only this site’s staff can see what players did.</string>
<string name="rust_feed_presence_hidden_signin">Joins, deaths and chat are not shown. Sign in to see what players did.</string>
<string name="rust_nobody_on">The server is up and the island is empty. Somebody has to be first.</string>
<string name="rust_presence_offline">Presence is the one thing on this page that cannot be answered from the record — it is who is connected now, and nothing is.</string>
<string name="rust_presence_live">On the server right now.</string>
<string name="rust_presence_last_known">The last board this server sent. It is offline, so this is who was on then — not who is on now.</string>
<string name="rust_sleeping">Sleeping</string>
<string name="rust_col_player">Player</string>
<string name="rust_col_kills">Kills</string>
<string name="rust_col_deaths">Deaths</string>
<string name="rust_col_npc_kills">NPC</string>
<string name="rust_col_structures">Built</string>
<string name="rust_col_played">Played</string>
<string name="rust_online_badge">%1$d players online</string>
<!-- The player's own Rust account (M15 — module-rust phase 8) -->
<string name="menu_rust_account">My Rust account</string>
<string name="rust_link_title">Link your Steam account</string>
<string name="rust_link_hint">Join any of our Rust servers and type /link in chat. The server replies with a six-character code only you can see, good for five minutes. Enter it here — it works once.</string>
<string name="rust_link_code">Link code</string>
<string name="rust_link_action">Link account</string>
<string name="rust_link_ok">Linked. Your play on our servers now appears under your name here.</string>
<string name="rust_link_already">That account was already linked to you.</string>
<string name="rust_link_bad_code">That code is unknown or has expired. Type /link in game for a new one.</string>
<string name="rust_link_taken">That Steam account is linked to another website account. Run /unlink in game to release it.</string>
<string name="rust_link_capped">Too many attempts just now. Try again in a few minutes.</string>
<string name="rust_link_unreachable">A server could not be reached, so that code could not be checked. Your code is still good — try again in a minute.</string>
<string name="rust_link_error">Could not link that code.</string>
<string name="rust_unlink_action">Unlink</string>
<string name="rust_unlink_error">Could not unlink that account.</string>
<string name="rust_account_linked_when">linked %1$s</string>
<string name="rust_account_none">No Steam account is linked to this profile yet.</string>
<string name="rust_account_fleet_note">A link covers every server this community runs — a Steam account is one person wherever they play. You can also type /unlink in game.</string>
<string name="rust_held_title">What you can do in game</string>
<string name="rust_held_empty">Nothing yet. Ranks and rewards this site hands out show up here, and reach you in game on the servers they cover.</string>
<string name="rust_held_unlinked">None of this reaches the game yet — link a Steam account above and the site pushes it across on its next sync.</string>
<string name="rust_held_waiting_note">A server marked waiting has not confirmed it yet. One that is offline catches up when it comes back.</string>
<string name="rust_held_no_servers">No servers are configured yet.</string>
<string name="rust_coins_title">Coins</string>
<string name="rust_coins_unlinked">Link a Steam account to see your balance.</string>
<string name="rust_coins_missing">Economics is not installed on this server.</string>
<string name="rust_coins_balance">%1$s · %2$s coins</string>
<string name="rust_coins_last_known">Last known — the server did not answer just now.</string>
<string name="rust_backpack_title">Backpack</string>
<string name="rust_backpack_unlinked">Link a Steam account to see your backpack.</string>
<string name="rust_backpack_open">Open backpack</string>
<string name="rust_backpack_read_again">Read again</string>
<string name="rust_backpack_close">Close</string>
<string name="rust_backpack_missing">Backpacks is not installed on this server.</string>
<string name="rust_backpack_unavailable">This site cannot show backpacks yet.</string>
<string name="rust_backpack_none">No backpack here yet — it has never been opened.</string>
<string name="rust_backpack_summary">%1$d stacks in %2$d slots</string>
<string name="rust_backpack_overflow">%1$d past your size — they move to your inventory, or drop at your feet if it is full, the next time you open it.</string>
<string name="rust_backpack_page">Page %1$d</string>
<string name="rust_reach_live">%1$s · has it</string>
<string name="rust_reach_waiting">%1$s · waiting</string>
</resources>

View File

@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
<!--
The Rust module's screens (PLAN.md M20): the server list and server page, the map,
and the player's own Rust account. Every row that reaches these is gated on the rust
capability, so Rust's words are right here. Core strings live in strings.xml.
-->
<resources>
<!-- ── Navigation menu (§5) ────────────────────────────────────────── -->
<string name="menu_rust">Rust servers</string>
<!-- Rust module (M14, docs/modules/rust/PLAN.md §18) -->
<string name="rust_servers_empty">No Rust servers are configured on this site yet.</string>
<string name="rust_offline">Offline</string>
<string name="rust_online_count">%1$d / %2$d online</string>
<string name="rust_never_reported">has never reported</string>
<string name="rust_last_reported">last reported %1$s</string>
<string name="rust_last_reported_stale">last reported %1$s — out of date, so it is shown as offline</string>
<string name="rust_world_size">size %1$d</string>
<string name="rust_world_seed">seed %1$d</string>
<string name="rust_wiped_on">wiped %1$s</string>
<string name="rust_next_wipe">Next wipe %1$s</string>
<string name="rust_next_wipe_rescheduled">Next wipe %1$s (rescheduled)</string>
<string name="rust_refresh_failed">Could not refresh just now. This is the last thing the site heard.</string>
<string name="rust_no_such_server">No such server</string>
<string name="rust_no_such_server_detail">This address does not name a server this site follows.</string>
<string name="rust_back_to_servers">Back to the server list</string>
<string name="rust_tab_feed">Feed</string>
<string name="rust_tab_leaderboard">Leaderboard</string>
<string name="rust_tab_online">Online</string>
<string name="rust_tab_wipes">Wipes</string>
<string name="rust_tab_map">Map</string>
<!-- The Map tab (Rust phase 15, D121–D125). The wording follows the web's Map
tab, so a reader moving between the two reads the same sentences. -->
<string name="rust_map_none">No map yet. This server has not told the site which map it is on. The map will appear once the server is up.</string>
<string name="rust_map_no_picture">This server has no picture of its map, so the layers are drawn on a plain background.</string>
<string name="rust_map_picture_failed">The picture of the map could not be loaded, so the layers are drawn on a plain background.</string>
<string name="rust_map_description">Map of the server</string>
<string name="rust_map_status_nothing_live">No moving layers are shown to you on this server.</string>
<string name="rust_map_status_asking">Asking the server where things are…</string>
<string name="rust_map_status_not_now">The server did not say where things are just now. The site asks again every ten seconds.</string>
<string name="rust_map_status_offline">The server is offline, so nothing is moving on its map.</string>
<string name="rust_map_status_failed">Positions could not be loaded.</string>
<string name="rust_map_status_failed_kept">Could not refresh just now. These are the last positions the site heard.</string>
<string name="rust_map_status_as_of">Positions as of %1$s. They refresh every ten seconds while this tab is open.</string>
<string name="rust_map_layer_grid">Grid</string>
<string name="rust_map_layer_world">Monuments &amp; world events</string>
<string name="rust_map_layer_events">Site events</string>
<string name="rust_map_layer_players">Players</string>
<string name="rust_map_layer_bases">Bases</string>
<string name="rust_map_layer_mates">You and your clan</string>
<string name="rust_map_hidden_signin">Sign in to see this layer.</string>
<string name="rust_map_hidden_public">Shown to everyone.</string>
<string name="rust_map_hidden_staff">Shown to staff only.</string>
<string name="rust_map_capped">%1$s Limited by who may see who is online.</string>
<string name="rust_map_players_truncated">Not every sleeper is shown.</string>
<string name="rust_map_bases_truncated">Not every base is shown.</string>
<string name="rust_map_mates_note">Your own position, and clan mates who are online.</string>
<string name="rust_map_mates_link">Link your Steam account to see yourself and your clan here.</string>
<string name="rust_map_cargo">Cargo ship</string>
<string name="rust_map_heli">Patrol helicopter</string>
<string name="rust_map_chinook">Chinook</string>
<string name="rust_map_bradley">Bradley APC</string>
<string name="rust_map_supply">Supply drop</string>
<string name="rust_map_crate">Locked crate</string>
<string name="rust_map_crate_hack">%1$s left on the hack</string>
<string name="rust_map_crate_hacked">Hacked</string>
<string name="rust_map_event_zone">Event zone</string>
<string name="rust_map_event_crate">Event crate</string>
<string name="rust_map_event_npc">Event NPC</string>
<string name="rust_map_site_event">Site event</string>
<string name="rust_map_site_event_named">Site event · %1$s</string>
<string name="rust_map_boss">%1$s (boss)</string>
<string name="rust_map_boss_unnamed">Boss</string>
<string name="rust_map_boss_add">%1$s (a boss\'s add)</string>
<string name="rust_map_open_event">Open event</string>
<string name="rust_map_tc">Tool cupboard</string>
<string name="rust_map_vending">Vending machine</string>
<string name="rust_map_player_sleeping">Sleeping</string>
<string name="rust_map_player_offline">Asleep, offline</string>
<string name="rust_map_you">You</string>
<string name="rust_map_clan_mate">Clan mate</string>
<string name="rust_map_grid">Grid %1$s</string>
<string name="rust_map_close">Close</string>
<string name="rust_all_time">All time</string>
<string name="rust_wipe_current">%1$s (this wipe)</string>
<string name="rust_wipe_this_one">Current</string>
<string name="rust_wipe_selected">Showing</string>
<string name="rust_feed_empty">Nothing has happened on this server yet — or not during the wipe you are looking at.</string>
<string name="rust_leaderboard_empty">Nobody has scored here yet.</string>
<string name="rust_wipes_empty">This server has not reported a wipe yet.</string>
<!-- Nothing names who is online by default (org lead, 2026-09-22). Below the
operator's audience the server sends a count and no names. -->
<plurals name="rust_online_hidden_count">
<item quantity="one">%1$d player online</item>
<item quantity="other">%1$d players online</item>
</plurals>
<string name="rust_online_hidden_staff">Only this site’s staff can see who they are.</string>
<string name="rust_online_hidden_signin">Sign in to see who they are.</string>
<string name="rust_online_hidden_public">This site is not showing who they are right now.</string>
<string name="rust_feed_presence_hidden_staff">Joins, deaths and chat are not shown. Only this site’s staff can see what players did.</string>
<string name="rust_feed_presence_hidden_signin">Joins, deaths and chat are not shown. Sign in to see what players did.</string>
<string name="rust_nobody_on">The server is up and the island is empty. Somebody has to be first.</string>
<string name="rust_presence_offline">Presence is the one thing on this page that cannot be answered from the record — it is who is connected now, and nothing is.</string>
<string name="rust_presence_live">On the server right now.</string>
<string name="rust_presence_last_known">The last board this server sent. It is offline, so this is who was on then — not who is on now.</string>
<string name="rust_sleeping">Sleeping</string>
<string name="rust_col_player">Player</string>
<string name="rust_col_kills">Kills</string>
<string name="rust_col_deaths">Deaths</string>
<string name="rust_col_npc_kills">NPC</string>
<string name="rust_col_structures">Built</string>
<string name="rust_col_played">Played</string>
<string name="rust_online_badge">%1$d players online</string>
<!-- The player's own Rust account (M15 — module-rust phase 8) -->
<string name="menu_rust_account">My Rust account</string>
<string name="rust_link_title">Link your Steam account</string>
<string name="rust_link_hint">Join any of our Rust servers and type /link in chat. The server replies with a six-character code only you can see, good for five minutes. Enter it here — it works once.</string>
<string name="rust_link_code">Link code</string>
<string name="rust_link_action">Link account</string>
<string name="rust_link_ok">Linked. Your play on our servers now appears under your name here.</string>
<string name="rust_link_already">That account was already linked to you.</string>
<string name="rust_link_bad_code">That code is unknown or has expired. Type /link in game for a new one.</string>
<string name="rust_link_taken">That Steam account is linked to another website account. Run /unlink in game to release it.</string>
<string name="rust_link_capped">Too many attempts just now. Try again in a few minutes.</string>
<string name="rust_link_unreachable">A server could not be reached, so that code could not be checked. Your code is still good — try again in a minute.</string>
<string name="rust_link_error">Could not link that code.</string>
<string name="rust_unlink_action">Unlink</string>
<string name="rust_unlink_error">Could not unlink that account.</string>
<string name="rust_account_linked_when">linked %1$s</string>
<string name="rust_account_none">No Steam account is linked to this profile yet.</string>
<string name="rust_account_fleet_note">A link covers every server this community runs — a Steam account is one person wherever they play. You can also type /unlink in game.</string>
<string name="rust_held_title">What you can do in game</string>
<string name="rust_held_empty">Nothing yet. Ranks and rewards this site hands out show up here, and reach you in game on the servers they cover.</string>
<string name="rust_held_unlinked">None of this reaches the game yet — link a Steam account above and the site pushes it across on its next sync.</string>
<string name="rust_held_waiting_note">A server marked waiting has not confirmed it yet. One that is offline catches up when it comes back.</string>
<string name="rust_held_no_servers">No servers are configured yet.</string>
<string name="rust_coins_title">Coins</string>
<string name="rust_coins_unlinked">Link a Steam account to see your balance.</string>
<string name="rust_coins_missing">Economics is not installed on this server.</string>
<string name="rust_coins_balance">%1$s · %2$s coins</string>
<string name="rust_coins_last_known">Last known — the server did not answer just now.</string>
<string name="rust_backpack_title">Backpack</string>
<string name="rust_backpack_unlinked">Link a Steam account to see your backpack.</string>
<string name="rust_backpack_open">Open backpack</string>
<string name="rust_backpack_read_again">Read again</string>
<string name="rust_backpack_close">Close</string>
<string name="rust_backpack_missing">Backpacks is not installed on this server.</string>
<string name="rust_backpack_unavailable">This site cannot show backpacks yet.</string>
<string name="rust_backpack_none">No backpack here yet — it has never been opened.</string>
<string name="rust_backpack_summary">%1$d stacks in %2$d slots</string>
<string name="rust_backpack_overflow">%1$d past your size — they move to your inventory, or drop at your feet if it is full, the next time you open it.</string>
<string name="rust_backpack_page">Page %1$d</string>
<string name="rust_reach_live">%1$s · has it</string>
<string name="rust_reach_waiting">%1$s · waiting</string>
</resources>

View File

@@ -0,0 +1,256 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
<!--
The Ultima Online module's screens (PLAN.md M20): the shard hub and its pages, the
player's characters / vendors / houses, staff moderation and the support queue, and
the module's push streams. Every row that reaches these is gated on the shard
capability, so UO's words are right here. Core strings live in strings.xml.
-->
<resources>
<!-- ── Navigation menu (§5) ────────────────────────────────────────── -->
<string name="menu_shard">Shard</string>
<string name="menu_rules">Rules</string>
<string name="menu_atlas">Atlas</string>
<string name="menu_leaderboards">Leaderboards</string>
<string name="menu_market">Market</string>
<string name="menu_my_characters">My characters</string>
<string name="menu_my_vendors">My vendors</string>
<string name="menu_my_houses">My houses</string>
<string name="menu_admin_moderation">Moderation</string>
<string name="menu_admin_support">Support queue</string>
<!-- ── Staff operations (§1, M10) ──────────────────────────────────── -->
<!-- Staff moderation (shard write plane) -->
<string name="admin_mod_account_action">Account action</string>
<string name="admin_mod_account">Account</string>
<string name="admin_mod_serial">Serial (0x…)</string>
<string name="admin_mod_reason">Reason (ban)</string>
<string name="admin_mod_duration">Ban duration (seconds; blank = indefinite)</string>
<string name="admin_mod_kick">Kick</string>
<string name="admin_mod_ban">Ban</string>
<string name="admin_mod_unban">Unban</string>
<string name="admin_mod_broadcast_section">Broadcast</string>
<string name="admin_mod_broadcast_text">Message to everyone online</string>
<string name="admin_mod_broadcast">Broadcast</string>
<string name="admin_mod_kicked">Account kicked.</string>
<string name="admin_mod_banned">Account banned.</string>
<string name="admin_mod_unbanned">Ban cleared.</string>
<string name="admin_mod_broadcasted">Message broadcast.</string>
<string name="admin_mod_target_required">Enter an account or serial.</string>
<string name="admin_mod_text_required">Enter a message to broadcast.</string>
<string name="admin_mod_shard_offline">The shard is offline — the action couldn\'t be delivered.</string>
<!-- Staff support queue -->
<string name="admin_support_empty">No open help pages.</string>
<string name="admin_support_reply">Reply</string>
<string name="admin_support_close">Close</string>
<string name="admin_support_send">Send</string>
<string name="admin_support_message">Reply message</string>
<string name="admin_support_close_after">Close the page after replying</string>
<string name="admin_support_responded">Reply sent.</string>
<string name="admin_support_closed">Page closed.</string>
<string name="admin_support_message_required">Enter a reply message.</string>
<string name="admin_support_unknown_page">That page is no longer in the queue.</string>
<!-- ── Player: game-account linking (§6.3) ─────────────────────────── -->
<string name="player_link_title">Link your game account</string>
<string name="player_link_hint">In game, type [link to get a one-time code, then enter it here to see your characters, vendors and houses.</string>
<string name="player_link_code">Link code</string>
<string name="player_link_action">Link account</string>
<string name="player_link_ok">Account linked.</string>
<string name="player_link_bad_code">That code is unknown or has expired. Run [link in game for a new one.</string>
<string name="player_link_error">Couldn\'t link that code. Please try again.</string>
<string name="player_create_title">Create a game account</string>
<string name="player_create_account">Account name</string>
<string name="player_create_password">Password</string>
<string name="player_create_action">Create account</string>
<string name="player_create_ok">Game account created and linked.</string>
<string name="player_create_taken">That account name is already taken.</string>
<string name="player_create_capped">The account limit for your network has been reached.</string>
<string name="player_create_unavailable">Game-account signup isn\'t available right now.</string>
<string name="player_create_rejected">That account name or password wasn\'t accepted.</string>
<string name="player_create_error">Couldn\'t create that account. Please try again.</string>
<!-- ── Player: characters (§6.3) ───────────────────────────────────── -->
<string name="player_characters_empty">Link a game account above to see your characters.</string>
<string name="player_roster_loading">Loading…</string>
<string name="player_roster_empty">No characters on this account.</string>
<string name="player_char_unknown">Unknown</string>
<string name="player_char_online">Online</string>
<string name="player_char_offline">Offline</string>
<!-- Character sheet -->
<string name="player_char_governor">Governor of %1$s</string>
<string name="player_char_guildmaster">Guildmaster, %1$s</string>
<string name="player_char_attributes">Attributes</string>
<string name="player_char_vitals">Vitals</string>
<string name="player_char_str">STR</string>
<string name="player_char_dex">DEX</string>
<string name="player_char_int">INT</string>
<string name="player_char_hits">Hits</string>
<string name="player_char_mana">Mana</string>
<string name="player_char_stam">Stamina</string>
<string name="player_char_resistances">Resistances</string>
<string name="player_char_phys">Physical</string>
<string name="player_char_fire">Fire</string>
<string name="player_char_cold">Cold</string>
<string name="player_char_pois">Poison</string>
<string name="player_char_energy">Energy</string>
<string name="player_char_skills">Skills</string>
<string name="player_char_points">Loyalty &amp; Points</string>
<!-- A point system's name followed by the character's rank on that board, e.g. "Queens Loyalty · #3". -->
<string name="player_char_points_ranked">%1$s · #%2$d</string>
<!-- A score against its cap. Only shown for capped systems; an uncapped score shows the number alone. -->
<string name="player_char_points_of">%1$d / %2$d</string>
<string name="player_char_equipment">Equipment</string>
<string name="player_char_item">Item</string>
<string name="player_char_item_id">id %1$d</string>
<string name="player_char_item_hue">hue %1$d</string>
<!-- ── Player: vendors &amp; sales (§6.3) ──────────────────────────── -->
<string name="player_sales_title">Recent sales</string>
<string name="player_sales_empty">No recent sales.</string>
<string name="player_gold_amount">%1$s gp</string>
<string name="player_vendors_no_accounts">Link a game account to see your vendors.</string>
<string name="player_vendors_empty">No vendors on this account.</string>
<string name="player_vendor_fallback">Player vendor</string>
<string name="player_vendor_hold">Holding %1$s gp</string>
<string name="player_vendor_no_listings">No items listed.</string>
<string name="player_listing_item">Item %1$d ×%2$d</string>
<!-- ── Player: houses (§6.3) ───────────────────────────────────────── -->
<string name="player_houses_empty">You don\'t have any houses on your linked accounts.</string>
<string name="player_house_fallback">A house</string>
<string name="player_house_stage">Status: %1$s</string>
<!-- ── Shard hub &amp; live feed (§6.2) ─────────────────────────────── -->
<string name="shard_live">Live</string>
<string name="shard_offline">Offline</string>
<string name="shard_status_online">Shard online</string>
<string name="shard_status_offline">Shard offline</string>
<string name="shard_online_count">%1$d players online</string>
<string name="shard_economy_gold">Gold supply: %1$s</string>
<string name="shard_section_staff">Staff online</string>
<string name="shard_section_activity">Live activity</string>
<string name="shard_feed_empty">No recent activity.</string>
<string name="shard_nav_champs">Champion spawns</string>
<string name="shard_nav_guilds">Guilds</string>
<string name="shard_nav_governors">Governors</string>
<string name="shard_nav_houses">Falling houses</string>
<!-- ── Champion spawns (§6.2) ──────────────────────────────────────── -->
<string name="champs_empty">No champion spawns are being tracked right now.</string>
<string name="champs_fallback_name">Champion spawn</string>
<string name="champ_level">Level %1$d</string>
<string name="champ_cooldown">Restarting</string>
<!-- ── Guilds (§6.2) ───────────────────────────────────────────────── -->
<string name="guilds_empty">No guilds are being tracked right now.</string>
<string name="guilds_fallback_name">Guild</string>
<string name="guilds_members">%1$d members</string>
<string name="guilds_leader">Led by %1$s</string>
<string name="guilds_alliance">Alliance: %1$s</string>
<!-- ── Rules / ruleset (Protocol 3.0 §5, M11) ──────────────────────── -->
<!-- A successful read of a shard that has never published its ruleset — NOT the same
as the feature being switched off, which renders as an error state. -->
<string name="rules_unpublished">This shard hasn\'t published its ruleset yet.</string>
<string name="rules_section_shard">Shard</string>
<string name="rules_section_systems">Systems</string>
<string name="rules_section_caps">Skill &amp; stat caps</string>
<string name="rules_section_accounts">Accounts &amp; housing</string>
<string name="rules_section_vendors">Vendors</string>
<string name="rules_section_schedule">Saves &amp; restarts</string>
<string name="rules_name">Name</string>
<string name="rules_expansion">Expansion</string>
<string name="rules_connect">Connect</string>
<string name="rules_skill_cap">Individual skill cap</string>
<string name="rules_total_skill_cap">Total skill cap</string>
<string name="rules_stat_cap">Total stat cap</string>
<string name="rules_str_cap">Strength cap</string>
<string name="rules_dex_cap">Dexterity cap</string>
<string name="rules_int_cap">Intelligence cap</string>
<string name="rules_char_slots">Character slots</string>
<string name="rules_per_ip">Accounts per IP</string>
<string name="rules_house_limit">Houses per account</string>
<string name="rules_restock_delay">Vendor restock delay</string>
<string name="rules_max_sell">Max sell quantity</string>
<string name="rules_autosave">Auto-save every</string>
<string name="rules_autorestart">Auto-restart at</string>
<string name="rules_minutes">%1$d min</string>
<!-- ── Leaderboards (Protocol 3.0 §7, M11) ─────────────────────────── -->
<string name="leaderboards_empty">This shard isn\'t publishing any leaderboards yet.</string>
<string name="leaderboards_board_empty">Nobody has scored here yet.</string>
<!-- Where a score would sit on the placeholder row of an unscored board. An em
dash, not "0" — nobody has scored zero, nobody has scored at all. -->
<string name="leaderboards_no_score">—</string>
<string name="leaderboards_players">%1$d players</string>
<!-- Only shown for capped systems; most systems on a real shard are uncapped. -->
<string name="leaderboards_cap">Cap: %1$d</string>
<string name="leaderboards_rank_name">#%1$d %2$s</string>
<!-- A shard may publish standings without naming who holds them (the board's one
admin-configurable field). -->
<string name="leaderboards_hidden_name">Someone</string>
<!-- ── Market (Protocol 3.0 §8, M11) ───────────────────────────────── -->
<string name="market_search_label">Search every shop</string>
<string name="market_empty">No listings match that search.</string>
<string name="market_shop_empty">This shop has nothing for sale.</string>
<!-- Required, not decoration: the shard sweeps vendors round-robin, so a price can
legitimately be a full cycle old. -->
<string name="market_staleness">Prices are refreshed in rotation and may be out of date.</string>
<string name="market_price">%1$d gp</string>
<string name="market_unnamed_item">Item %1$d</string>
<string name="market_unnamed_shop">A shop</string>
<string name="market_owner">Kept by %1$s</string>
<!-- A gated location is a real answer: the shop exists, the shard just doesn't say
where it stands. -->
<string name="market_location_hidden">This shard doesn\'t publish shop locations.</string>
<string name="market_truncated">Showing %1$d of %2$d — this shop holds more than the shard publishes.</string>
<!-- ── Spawn atlas (Protocol 3.0 §6, M11) ──────────────────────────── -->
<string name="atlas_search_label">Search creatures</string>
<string name="atlas_empty">No creatures match that search.</string>
<!-- A place can legitimately hold a single spawner, and the aggregate list is full
of them — "1 spawners" on every other row is worth a plural for. -->
<plurals name="atlas_spawner_count">
<item quantity="one">%1$d spawner</item>
<item quantity="other">%1$d spawners</item>
</plurals>
<string name="atlas_total_alive">Up to %1$d alive at once</string>
<string name="atlas_facet_count">%1$s (%2$d)</string>
<!-- The aggregate: "where is it", as opposed to the raw coordinates below it. -->
<string name="atlas_section_places">Where it spawns</string>
<string name="atlas_place_max_alive">up to %1$d at once</string>
<string name="atlas_section_spawners">Spawn points</string>
<string name="atlas_section_also_here">Also spawns here</string>
<string name="atlas_spawners_truncated">More spawn points than shown.</string>
<string name="atlas_max_count">Up to %1$d</string>
<!-- The delay is already in seconds; the server normalizes XmlSpawner's mixed units. -->
<string name="atlas_respawn">Respawn %1$s</string>
<!-- ── Governors (§6.2) ────────────────────────────────────────────── -->
<string name="governors_empty">No governors — this shard may not run the City Loyalty system.</string>
<string name="governor_current">Governed by %1$s</string>
<string name="governor_none">No governor</string>
<string name="governor_election">Election: %1$s</string>
<string name="governor_history">Term history</string>
<string name="governor_history_loading">Loading…</string>
<string name="governor_history_empty">No past terms recorded.</string>
<!-- ── Falling houses / IDOC (§6.2) ────────────────────────────────── -->
<string name="houses_empty">No houses are in danger right now.</string>
<string name="houses_fallback_name">A house</string>
<string name="houses_idoc_badge">IDOC</string>
<!-- ── Push notifications (§11, M7 Part 2) ─────────────────────────── -->
<string name="push_stream_server_status">Shard status changed</string>
<string name="push_stream_idoc_warning">A house is falling (IDOC)</string>
<string name="push_stream_champ_start">Champion spawn started</string>
<string name="push_stream_governor_election">New governor elected</string>
<string name="push_stream_vendor_sale">Your vendor made a sale</string>
<string name="push_stream_house_idoc">Your house entered IDOC</string>
<string name="push_stream_account_login">Login to your account</string>
</resources>

View File

@@ -0,0 +1,168 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.runicgateway.app
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Test
import org.w3c.dom.Element
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory
/**
* The guard on the app's words (PLAN.md M20). A game's words stay on that game's
* screens, and the screens every deployment shows use core's words. This is the
* app's half of the rule the website docs and runicgateway.com already follow
* (runicgateway.com PLAN.md §16, D63/D64).
*
* The strings are split by owner into three resource files that Android merges as
* one `R.string`: `strings.xml` (core), `strings_uo.xml` and `strings_rust.xml`.
* A string's owner is the screens that use it, not its key: a string any screen a
* Rust deployment can reach is core, even when its key says `shard_`. This test
* reads the three files and fails, naming the key and the word, when
* - the core file holds either game's word;
* - the UO file holds a Rust word;
* - the Rust file holds a UO word.
*
* It checks every `values*` directory, so a translation is held to the same rule.
* Comments are not read, because nobody using the app sees them.
*/
class StringVocabularyTest {
private enum class Owner(val file: String, val label: String) {
CORE("strings.xml", "core"),
UO("strings_uo.xml", "Ultima Online"),
RUST("strings_rust.xml", "Rust"),
}
private class Term(val game: Owner, val word: String, pattern: String) {
val regex = Regex(pattern, RegexOption.IGNORE_CASE)
}
/**
* runicgateway.com's `scripts/checkTerms.mjs` list, word for word. Each word is a game's
* name for something core names differently (shard → game server, guild and clan →
* Team), or the name of one game's component. Game NAMES (Ultima Online, Rust) are not
* on it, and neither is Steam, which is a core sign-in provider. Matching is on word
* boundaries, so "wipe" does not match "wiped" and "shard" does not match `SHARD_X`.
*/
private val terms = listOf(
Term(Owner.UO, "shard", """\bshards?\b"""),
Term(Owner.UO, "ServUO", """\bservuo\b"""),
Term(Owner.UO, "uo-link", """\buo-?link\b"""),
Term(Owner.UO, "Module-uo", """\bmodule-uo\b"""),
Term(Owner.UO, "Bridge.cfg", """\bbridge\.cfg\b"""),
Term(Owner.UO, "cliloc", """\bclilocs?\b"""),
Term(Owner.UO, "guild", """\bguilds?\b"""),
Term(Owner.UO, "IDOC", """\bidocs?\b"""),
Term(Owner.RUST, "rust-link", """\brust-?link\b"""),
Term(Owner.RUST, "Rust-Plugins", """\brust-plugins\b"""),
Term(Owner.RUST, "Module-Rust", """\bmodule-rust\b"""),
Term(Owner.RUST, "Oxide", """\boxide\b"""),
Term(Owner.RUST, "Carbon", """\bcarbon\b"""),
Term(Owner.RUST, "uMod", """\bumod\b"""),
Term(Owner.RUST, "clan", """\bclans?\b"""),
Term(Owner.RUST, "wipe", """\bwipes?\b"""),
)
/** The words [owner]'s file may not use. */
private fun forbiddenIn(owner: Owner): List<Term> = when (owner) {
Owner.CORE -> terms
Owner.UO -> terms.filter { it.game == Owner.RUST }
Owner.RUST -> terms.filter { it.game == Owner.UO }
}
/** The words in [text] that [owner]'s file may not use. */
private fun hits(owner: Owner, text: String): List<Pair<Term, String>> =
forbiddenIn(owner).flatMap { term -> term.regex.findAll(text).map { term to it.value }.toList() }
/**
* The module's `res`, wherever the test runs from: Gradle runs unit tests in the
* module directory, and an IDE may run them from the repository root.
*/
private val res: File = listOf(File("src/main/res"), File("app/src/main/res"))
.firstOrNull { it.isDirectory }
?: error("Can't find src/main/res from ${File(".").absolutePath}")
private val valuesDirs: List<File>
get() = res.listFiles { f -> f.isDirectory && f.name.startsWith("values") }!!.sortedBy { it.name }
/** Every resource in [file] that the app shows: key → its visible text. */
private fun visibleText(file: File): List<Pair<String, String>> {
val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file)
val children = doc.documentElement.childNodes
return (0 until children.length)
.map { children.item(it) }
.filterIsInstance<Element>()
// A plural's items arrive as one text, which is all a word scan needs.
.map { it.getAttribute("name") to it.textContent }
}
@Test fun eachFileUsesOnlyItsOwnGamesWords() {
val failures = mutableListOf<String>()
for (dir in valuesDirs) {
for (owner in Owner.entries) {
val file = File(dir, owner.file)
if (!file.exists()) continue
for ((key, text) in visibleText(file)) {
for ((term, found) in hits(owner, text)) {
failures += "${dir.name}/${owner.file}: $key uses \"$found\", " +
"${term.game.label} vocabulary, in the ${owner.label} file"
}
}
}
}
if (failures.isNotEmpty()) {
fail(
failures.joinToString(
separator = "\n ",
prefix = "${failures.size} game word(s) outside their game's file:\n ",
postfix = "\n\nPLAN.md M20: a string any deployment's screen shows is core, and uses " +
"core's words (site, community, game server, Team). Reword it, or, if only one " +
"game's screens use it, move it to that game's file.",
),
)
}
}
/**
* A fourth strings file would be read by Android and skipped by this guard, so it
* is refused. A new game gets its own file and its own row in [Owner].
*/
@Test fun everyStringsFileHasAnOwner() {
val known = Owner.entries.map { it.file }.toSet()
val strays = valuesDirs.flatMap { dir ->
dir.listFiles { f -> f.name.startsWith("strings") && f.name !in known }!!.map { "${dir.name}/${it.name}" }
}
assertEquals("Strings files with no owner in StringVocabularyTest", emptyList<String>(), strays)
}
/** Without this a wrong path would let the guard pass on nothing. */
@Test fun theDefaultFilesAreAllThere() {
for (owner in Owner.entries) {
val file = File(res, "values/${owner.file}")
assertTrue("${file.path} is missing", file.exists())
assertTrue("${file.path} holds no strings", visibleText(file).isNotEmpty())
}
}
/** The matcher itself, so a broken pattern can't make every file look clean. */
@Test fun theWordListCatchesWhatItShould() {
fun words(owner: Owner, text: String) = hits(owner, text).map { it.first.word }
assertEquals(listOf("shard"), words(Owner.CORE, "Connect to your shard"))
assertEquals(listOf("guild"), words(Owner.CORE, "Guilds"))
assertEquals(listOf("clan"), words(Owner.CORE, "You and your clan"))
assertEquals(listOf("wipe"), words(Owner.UO, "Next wipe %1\$s"))
assertEquals(listOf("IDOC"), words(Owner.RUST, "Your house entered IDOC"))
// Each game's own words are fine in its own file.
assertEquals(emptyList<String>(), words(Owner.UO, "Shard online"))
assertEquals(emptyList<String>(), words(Owner.RUST, "Clan mate"))
// Word boundaries: not a prefix, not an identifier.
assertEquals(emptyList<String>(), words(Owner.CORE, "wiped %1\$s"))
assertEquals(emptyList<String>(), words(Owner.CORE, "Guildmaster"))
assertEquals(emptyList<String>(), words(Owner.CORE, "SHARD_VISIBILITY"))
}
}

View File

@@ -168,6 +168,26 @@ class MenuCapabilityGatingTest {
}
}
@Test fun moderationAndSupportNeedTheShardModule() {
// Both call `/admin/shard/*`, module-uo's admin mount. Before M20 they
// carried no capability, so a moderator on a Rust-only site saw a UO form
// whose every action failed.
val staffRows = APP_MENU.filter { it.route == Routes.ADMIN_MODERATION || it.route == Routes.ADMIN_SUPPORT }
assertEquals(2, staffRows.size)
assertTrue(staffRows.all { it.capability == Capability.SHARD })
val moderator = signedIn(Role.MODERATOR)
val rustOnly = visibleEntries(APP_MENU, moderator, null, serving(Capability.RUST)).map { it.route }
assertFalse(Routes.ADMIN_MODERATION in rustOnly)
assertFalse(Routes.ADMIN_SUPPORT in rustOnly)
// …while the core staff rows stay.
assertTrue(Routes.ADMIN_DASHBOARD in rustOnly)
val uo = visibleEntries(APP_MENU, moderator, null, serving(Capability.SHARD)).map { it.route }
assertTrue(Routes.ADMIN_MODERATION in uo)
assertTrue(Routes.ADMIN_SUPPORT in uo)
}
@Test fun bothEventRowsDeclareCoresCapabilityAndNoFeature() {
// Events are core's. A `feature` on one of them would gate a core screen on
// a module's visibility config, which is the coupling this separation exists

View File

@@ -41,6 +41,35 @@ class RustFeedTest {
assertTrue(line.detail.contains("42m"))
}
@Test
fun `a kill prints the weapon name the module sends`() {
// Module-Rust#60: the module reads the game's own name off its table and
// sends it beside the prefab, which stays the counting key.
val line = describe(
row(
"player.death",
"""{"name":"Bob","attackerType":"player","attackerName":"Alice",
"weapon":"ak47u.entity","weaponName":"Assault Rifle"}""",
),
)
assertEquals("with Assault Rifle", line.detail)
}
@Test
fun `an older module's kill drops the prefab ending rather than printing it`() {
// A module from before #60 sends no `weaponName`. The feed once said
// "with ak47u entity"; the ending is dropped the way the website does it.
val line = describe(
row(
"player.death",
"""{"name":"Bob","attackerType":"player","attackerName":"Alice","weapon":"ak47u.entity"}""",
),
)
assertEquals("with ak47u", line.detail)
}
@Test
fun `a fall is a death by nobody, not a kill`() {
// The failure this distinction exists to avoid: `HitInfo` is legitimately

View File

@@ -117,6 +117,21 @@ class RustFormatTest {
assertEquals("", prefabName(null))
}
@Test
fun `a weapon prints the module's name, or the prefab without its ending`() {
assertEquals("Assault Rifle", weaponName("ak47u.entity", "Assault Rifle"))
// An older module sends no name; neither does one that sends a blank.
assertEquals("ak47u", weaponName("ak47u.entity", null))
assertEquals("ak47u", weaponName("ak47u.entity", ""))
assertEquals("salvaged sword", weaponName("salvaged_sword.weapon", null))
assertEquals("autoturret", weaponName("autoturret.deployed", null))
assertEquals("rifle ak", weaponName("rifle.ak", null))
// The ending is dropped only at the end.
assertEquals("entity thing", weaponName("entity.thing", null))
// No weapon, no name, even when a name came along.
assertEquals("", weaponName(null, "Assault Rifle"))
}
@Test
fun `a nameless player shows a shortened id, not the word unknown`() {
// A steam id is not a name and does not look like one, which is the point: