From 598fb5c6790a07b157f151119142b194ca158694 Mon Sep 17 00:00:00 2001 From: Ap_Tx <3045929398@qq.com> Date: Mon, 19 Oct 2026 21:48:11 +0800 Subject: [PATCH 01/11] =?UTF-8?q?chore:=20=E5=8F=91=E5=B8=83=201.0.2-beta9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/app-frontend/package.json | 2 +- .../app-frontend/src/announcements/catalog.ts | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/apps/app-frontend/package.json b/apps/app-frontend/package.json index b24ed39..a884400 100644 --- a/apps/app-frontend/package.json +++ b/apps/app-frontend/package.json @@ -1,7 +1,7 @@ { "name": "@modrinth/app-frontend", "private": true, - "version": "1.0.2-beta8", + "version": "1.0.2-beta9", "type": "module", "scripts": { "dev": "vite", diff --git a/apps/app-frontend/src/announcements/catalog.ts b/apps/app-frontend/src/announcements/catalog.ts index 55536d5..3bc807a 100644 --- a/apps/app-frontend/src/announcements/catalog.ts +++ b/apps/app-frontend/src/announcements/catalog.ts @@ -32,6 +32,51 @@ export const ANNOUNCEMENT_CHANGE_TYPES: readonly AnnouncementChangeType[] = [ ] export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ + { + id: 'launcher-1.0.2-beta9', + version: '1.0.2-beta9', + publishedAt: '2026-10-19', + title: { + 'en-US': 'Starlight Launcher 1.0.2-beta9', + 'zh-CN': 'Starlight Launcher 1.0.2-beta9', + }, + changes: { + added: [ + { + 'en-US': 'Finished the skin site admin console: system overview, announcements, user and player management, review paper editor, mod upload with batch operations, chat room management with real-time voice, servers, audit log, resource downloads, donations and launcher releases.', + 'zh-CN': '补全皮肤站管理台:系统概览、公告、用户与玩家管理、审核试卷编辑器、Mod 上传与批量操作、聊天室房间管理与实时语音、服务器、审计日志、资源下载、捐赠与启动器版本管理。', + }, + { + 'en-US': 'Added Wiki management to the admin console, including a MediaWiki migration center that imports pages from an existing wiki and lets you review, publish or roll back the result.', + 'zh-CN': '管理台新增维基管理,包含 MediaWiki 迁移中心:可从现有 Wiki 导入页面,并对结果进行审核、发布或回滚。', + }, + { + 'en-US': 'Added a read-only Wiki reader in the launcher: render pages, follow links between entries, and keep a back stack while browsing.', + 'zh-CN': '启动器新增维基阅读页:渲染词条正文、跟随词条链接跳转,并在浏览时保留返回栈。', + }, + ], + changed: [ + { + 'en-US': 'Releases are now published to the skin site automatically when a tag is pushed, so the download page stays in sync without a manual upload.', + 'zh-CN': '推送 tag 发版时会自动把版本同步到皮肤站,下载页无需再手动上传即可保持同步。', + }, + { + 'en-US': 'Server management pages are only shown to web administrators.', + 'zh-CN': '服务器管理页仅对网页管理员展示。', + }, + ], + fixed: [ + { + 'en-US': 'Fixed the Chinese and English text on the skin site home navigation cards.', + 'zh-CN': '修复皮肤站主页导航卡片的中英文案。', + }, + { + 'en-US': 'Renamed the public download and map endpoints to resolve a duplicate export.', + 'zh-CN': '重命名公开下载与地图接口,解决导出冲突。', + }, + ], + }, + }, { id: 'launcher-1.0.2-beta8', version: '1.0.2-beta8', From 159f9e3f389df8e23bf963416c7c5b4aeb86bb37 Mon Sep 17 00:00:00 2001 From: Disy Date: Tue, 20 Oct 2026 17:04:46 +0800 Subject: [PATCH 02/11] fix(security): backport upstream modpack path traversal fixes Backport Mystic-Stars/Axolotl b2f81391e584aebf3ef07e8db2fc9cdad2ffb4c7. Adapt validation to this fork and protect CurseForge archive paths and MRPack uninstall cleanup. --- packages/app-lib/src/api/curseforge.rs | 31 +-- packages/app-lib/src/api/pack/archive_util.rs | 102 ++++++-- packages/app-lib/src/api/pack/install_from.rs | 88 +++++-- .../app-lib/src/api/pack/install_mrpack.rs | 76 ++++-- packages/app-lib/src/api/servers/files.rs | 74 +++--- packages/app-lib/src/api/servers/forge.rs | 4 +- packages/app-lib/src/api/servers/lifecycle.rs | 11 +- packages/app-lib/src/api/servers/manage.rs | 3 +- packages/app-lib/src/api/servers/manifest.rs | 2 +- packages/app-lib/src/api/servers/modpack.rs | 218 +++++++++++++++--- packages/app-lib/src/state/dirs.rs | 26 ++- packages/app-lib/src/util/io.rs | 63 ++++- 12 files changed, 545 insertions(+), 153 deletions(-) diff --git a/packages/app-lib/src/api/curseforge.rs b/packages/app-lib/src/api/curseforge.rs index 4e8e333..bd9e147 100644 --- a/packages/app-lib/src/api/curseforge.rs +++ b/packages/app-lib/src/api/curseforge.rs @@ -5197,19 +5197,9 @@ fn modpack_zip_error(error: zip::result::ZipError) -> crate::Error { } fn safe_archive_relative_path(value: &str) -> crate::Result { - let path = Path::new(value); - if value.is_empty() - || path.is_absolute() - || path - .components() - .any(|component| !matches!(component, Component::Normal(_))) - { - return Err(ErrorKind::InputError( - "CurseForge modpack contains an invalid override path".to_string(), - ) - .into()); - } - Ok(path.to_string_lossy().replace('\\', "/")) + let normalized = value.replace('\\', "/"); + crate::util::io::safe_relative_join(Path::new(""), &normalized)?; + Ok(normalized) } pub(crate) fn loader_family(loader_id: &str) -> &str { @@ -10933,6 +10923,21 @@ mod tests { ); assert!(safe_archive_relative_path("../options.txt").is_err()); assert!(safe_archive_relative_path("/options.txt").is_err()); + for path in [ + r"..\options.txt", + "config/.. /outside", + "config/file:stream", + "C:/outside", + "C:outside", + "config/NUL", + "config/file.", + ] { + assert!(safe_archive_relative_path(path).is_err(), "{path}"); + } + assert_eq!( + safe_archive_relative_path(r"config\example.toml").unwrap(), + "config/example.toml" + ); } #[test] diff --git a/packages/app-lib/src/api/pack/archive_util.rs b/packages/app-lib/src/api/pack/archive_util.rs index 254a11b..5c74296 100644 --- a/packages/app-lib/src/api/pack/archive_util.rs +++ b/packages/app-lib/src/api/pack/archive_util.rs @@ -1,6 +1,6 @@ //! Shared helpers for extracting content from local modpack archives. -use std::path::{Component, Path, PathBuf}; +use std::path::{Path, PathBuf}; use super::detect::decode_zip_entry_name; use crate::util::io; @@ -13,22 +13,6 @@ fn archive_error(error: zip::result::ZipError) -> crate::Error { .into() } -pub(crate) fn safe_relative_path(value: &str) -> crate::Result { - let path = Path::new(value); - if value.is_empty() - || path.is_absolute() - || path - .components() - .any(|component| !matches!(component, Component::Normal(_))) - { - return Err(crate::ErrorKind::InputError( - "Modpack archive contains an invalid file path".to_string(), - ) - .into()); - } - Ok(path.to_string_lossy().replace('\\', "/")) -} - /// Extracts every file under `prefix` in the archive into `target_dir`, /// preserving the directory structure below the prefix. Returns the number of /// files written. @@ -114,7 +98,8 @@ fn extract_archive_subdir_sync( ) .into()); } - let target = target_dir.join(safe_relative_path(relative)?); + let target = + io::safe_relative_join(target_dir, &relative.replace('\\', "/"))?; if let Some(parent) = target.parent() { std::fs::create_dir_all(parent) .map_err(|error| io::IOError::with_path(error, parent))?; @@ -252,6 +237,87 @@ pub(crate) async fn create_import_scratch_dir( #[cfg(test)] mod tests { use super::*; + use std::io::Write; + + #[test] + fn unsafe_override_paths_fail_without_writing_outside_target() { + let root = tempfile::tempdir().unwrap(); + let archive_path = root.path().join("pack.zip"); + let target_dir = root.path().join("server"); + std::fs::create_dir(&target_dir).unwrap(); + let outside = root.path().join("outside.txt"); + std::fs::write(&outside, b"unchanged").unwrap(); + for prefix in ["overrides/", "server/overrides/"] { + for relative in [ + "../outside.txt", + r"..\outside.txt", + "config/../../outside.txt", + "/outside.txt", + "C:/outside.txt", + "C:outside.txt", + r"\\server\share\outside.txt", + "config/.. /outside.txt", + "config/mod.jar:payload", + "config/NUL", + "config/secret.", + ] { + let mut archive = zip::ZipWriter::new( + std::fs::File::create(&archive_path).unwrap(), + ); + archive + .start_file( + format!("{prefix}{relative}"), + zip::write::SimpleFileOptions::default(), + ) + .unwrap(); + archive.write_all(b"malicious").unwrap(); + archive.finish().unwrap(); + assert!( + extract_archive_subdir_sync( + &archive_path, + prefix, + &target_dir, + None, + ) + .is_err(), + "{prefix}{relative}" + ); + assert_eq!(std::fs::read(&outside).unwrap(), b"unchanged"); + assert_eq!(std::fs::read_dir(&target_dir).unwrap().count(), 0); + } + } + } + + #[test] + fn override_paths_with_backslashes_are_normalized() { + let root = tempfile::tempdir().unwrap(); + let archive_path = root.path().join("pack.zip"); + let target_dir = root.path().join("server"); + let mut archive = + zip::ZipWriter::new(std::fs::File::create(&archive_path).unwrap()); + archive + .start_file( + r"overrides/config\example.toml", + zip::write::SimpleFileOptions::default(), + ) + .unwrap(); + archive.write_all(b"valid").unwrap(); + archive.finish().unwrap(); + assert_eq!( + extract_archive_subdir_sync( + &archive_path, + "overrides/", + &target_dir, + None, + ) + .unwrap(), + 1 + ); + assert_eq!( + std::fs::read(target_dir.join("config/example.toml")).unwrap(), + b"valid" + ); + } struct CancelOnRead { cancellation: CancellationToken, diff --git a/packages/app-lib/src/api/pack/install_from.rs b/packages/app-lib/src/api/pack/install_from.rs index 53c812b..245ca05 100644 --- a/packages/app-lib/src/api/pack/install_from.rs +++ b/packages/app-lib/src/api/pack/install_from.rs @@ -20,7 +20,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::future::Future; -use std::path::{Component, Path, PathBuf}; +use std::path::{Path, PathBuf}; use std::pin::Pin; use zip::ZipArchive; @@ -320,23 +320,13 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter( "Specified version has no files".to_string(), ) })?; - let file_name = Path::new(&pack_file.filename); - if file_name.components().count() != 1 - || !matches!(file_name.components().next(), Some(Component::Normal(_))) - { - return Err(crate::ErrorKind::InputError( - "Modrinth returned an invalid modpack file name".to_string(), - ) - .into()); - } let hash = pack_file.hashes.get("sha1"); - let pack_path = state - .directories - .caches_dir() - .join("modpacks") - .join(&project_id) - .join(&version_id) - .join(file_name); + let pack_path = modpack_cache_path( + &state.directories.caches_dir().join("modpacks"), + &project_id, + &version_id, + &pack_file.filename, + )?; let details = InstallPhaseDetails::Modpack { project_id: Some(project_id.clone()), @@ -517,8 +507,70 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter( }) } -#[tracing::instrument] +fn modpack_cache_path( + root: &Path, + project_id: &str, + version_id: &str, + filename: &str, +) -> crate::Result { + let mut path = root.to_path_buf(); + for segment in [project_id, version_id, filename] { + if segment.contains('/') { + return Err(crate::ErrorKind::InputError( + "Invalid modpack cache path component".to_string(), + ) + .into()); + } + path = crate::util::io::safe_relative_join(&path, segment)?; + } + Ok(path) +} +#[cfg(test)] +mod cache_path_tests { + use super::modpack_cache_path; + use std::path::Path; + + #[test] + fn cache_components_cannot_escape_their_directories() { + let root = Path::new("modpacks"); + assert_eq!( + modpack_cache_path(root, "project", "version", "pack.mrpack") + .unwrap(), + root.join("project/version/pack.mrpack") + ); + for malicious in [ + "../outside", + "nested/name", + r"..\outside", + "C:/outside", + "C:outside", + r"\\server\share", + ".", + "", + "NUL", + "pack.mrpack:stream", + ".. ", + ] { + for position in 0..3 { + let mut components = ["project", "version", "pack.mrpack"]; + components[position] = malicious; + assert!( + modpack_cache_path( + root, + components[0], + components[1], + components[2] + ) + .is_err(), + "{position}: {malicious:?}" + ); + } + } + } +} + +#[tracing::instrument] pub async fn generate_pack_from_file( path: PathBuf, instance_id: String, diff --git a/packages/app-lib/src/api/pack/install_mrpack.rs b/packages/app-lib/src/api/pack/install_mrpack.rs index 7de173b..d5127a5 100644 --- a/packages/app-lib/src/api/pack/install_mrpack.rs +++ b/packages/app-lib/src/api/pack/install_mrpack.rs @@ -891,6 +891,12 @@ pub(crate) async fn install_zipped_mrpack_files_with_reporter( num_files, content_total_bytes, }; + for file in &pack.files { + io::join_within_root( + &content_context.instance_full_path, + &content_context.resolve_install_path(file), + )?; + } let skipped_missing_content_paths = Arc::new( reporter .current_state() @@ -1039,8 +1045,14 @@ pub(crate) async fn install_zipped_mrpack_files_with_reporter( let project_size = project.file_size as u64; let project_path = content_context.resolve_install_path(&project); - let target_path = - content_context.instance_full_path.join(&project_path); + let target_path = io::join_within_root( + &content_context.instance_full_path, + &project_path, + ).map_err(|error| RequiredFileFailure::new( + manifest_index, + &project_path, + &error.to_string(), + ))?; let context = InstallErrorContext::new("download modpack content file") .maybe_project_id( @@ -1451,7 +1463,10 @@ pub(crate) async fn install_zipped_mrpack_files_with_reporter( entry_name: filename.to_string(), crc32: file.crc32(), uncompressed_size: file.uncompressed_size(), - target_path: instance_full_path.join(&relative_path), + target_path: io::safe_relative_join( + &instance_full_path, + &relative_path, + )?, relative_path, }) }) @@ -1739,6 +1754,7 @@ fn override_relative_path( "Invalid modpack override path: {relative}" )) })?; + io::safe_relative_join(Path::new(""), relative.as_str())?; Ok(relative.as_str().to_string()) } @@ -1885,7 +1901,11 @@ pub async fn remove_all_related_files( // Iterate over all Modrinth project file paths in the json, and remove them // (There should be few, but this removes any files the .mrpack intended as Modrinth projects but were unrecognized) for file in pack.files { - match io::remove_file(instance_full_path.join(file.path.as_str())).await + match io::remove_file(io::safe_relative_join( + &instance_full_path, + file.path.as_str(), + )?) + .await { Ok(_) => (), Err(err) if err.kind() == ErrorKind::NotFound => (), @@ -1904,23 +1924,14 @@ pub async fn remove_all_related_files( for file in override_file_entries { let entry_name = file.filename().as_str().unwrap(); - let entry_name = - entry_name.strip_prefix(&archive_base).unwrap_or(entry_name); let relative_override_file_path = - SafeRelativeUtf8UnixPathBuf::try_from(entry_name.to_string())?; - let relative_override_file_path = relative_override_file_path - .strip_prefix("overrides") - .or_else(|_| relative_override_file_path.strip_prefix("client-overrides")) - .map_err(|_| { - crate::Error::from(crate::ErrorKind::OtherError( - format!("Failed to strip override prefix from override file path: {relative_override_file_path}") - )) - })?; + override_relative_path(entry_name, &archive_base)?; // Remove this file if a corresponding one exists in the filesystem - match io::remove_file( - instance_full_path.join(relative_override_file_path.as_str()), - ) + match io::remove_file(io::safe_relative_join( + &instance_full_path, + &relative_override_file_path, + )?) .await { Ok(_) => (), @@ -1939,6 +1950,35 @@ mod tests { use async_zip::{Compression, ZipEntryBuilder}; use std::sync::Mutex as StdMutex; + #[test] + fn override_paths_reject_portable_traversal_and_windows_aliases() { + for prefix in ["overrides/", "client-overrides/"] { + assert_eq!( + override_relative_path( + &format!("{prefix}config/example.toml"), + "" + ) + .unwrap(), + "config/example.toml" + ); + for relative in [ + "../outside", + "config/.. /outside", + "config/file:stream", + "C:/outside", + "C:outside", + "config/NUL", + "config/file.", + ] { + assert!( + override_relative_path(&format!("{prefix}{relative}"), "") + .is_err(), + "{prefix}{relative}" + ); + } + } + } + fn override_spec(index: usize, target: &str) -> OverrideExtractionSpec { OverrideExtractionSpec { index, diff --git a/packages/app-lib/src/api/servers/files.rs b/packages/app-lib/src/api/servers/files.rs index 6007c65..6580a7c 100644 --- a/packages/app-lib/src/api/servers/files.rs +++ b/packages/app-lib/src/api/servers/files.rs @@ -12,6 +12,8 @@ use crate::{ErrorKind, Result}; use super::manifest::server_path; +pub(super) use crate::util::io::safe_relative_join; + const DOWNLOAD_PROGRESS_STEP: u64 = 512 * 1024; pub async fn read_file(server_id: &str, file: &str) -> Result { @@ -124,44 +126,7 @@ pub(super) async fn download_to_dir( async fn resolve_server_file(server_id: &str, file: &str) -> Result { let dir = server_path(server_id).await?; - safe_join(&dir, file) -} - -fn safe_join(dir: &Path, file: &str) -> Result { - if file.is_empty() - || file.contains('\\') - || file.starts_with('/') - || file - .split('/') - .any(|segment| segment == ".." || segment.is_empty()) - { - return Err(ErrorKind::InputError(format!( - "Invalid file name: {file}" - )) - .as_error()); - } - Ok(dir.join(file)) -} - -/// Joins a relative path that may contain `/` separators but never escapes the -/// server directory (no `..`, empty segments, or absolute/Windows roots). -pub(super) fn safe_relative_join(dir: &Path, rel: &str) -> Result { - let is_windows_drive_root = - rel.as_bytes().get(1).is_some_and(|byte| *byte == b':') - && rel.as_bytes().first().is_some_and(u8::is_ascii_alphabetic); - if rel.is_empty() - || Path::new(rel).is_absolute() - || is_windows_drive_root - || rel.starts_with('/') - || rel.starts_with('\\') - || rel.split(['/', '\\']).any(|segment| { - segment.is_empty() || segment == ".." || segment == "." - }) - { - return Err(ErrorKind::InputError(format!("Invalid file path: {rel}")) - .as_error()); - } - Ok(dir.join(rel)) + safe_relative_join(&dir, file) } #[cfg(test)] @@ -169,13 +134,34 @@ mod tests { use super::*; #[test] - fn safe_join_rejects_traversal() { + fn safe_relative_join_rejects_unsafe_paths() { let dir = Path::new("/tmp/servers/a"); - assert!(safe_join(dir, "server.properties").is_ok()); - assert!(safe_join(dir, "../secret").is_err()); - assert!(safe_join(dir, "/etc/passwd").is_err()); - assert!(safe_join(dir, "a//b").is_err()); - assert!(safe_join(dir, "").is_err()); + for path in [ + "../secret", + "mods/../../secret", + r"..\secret", + r"mods/..\../secret", + "/etc/passwd", + "C:/windows/system32", + r"C:\windows\system32", + "C:secret", + r"\\server\share\secret", + r"\secret", + r"\\?\C:\secret", + "a//b", + "a/./b", + ".", + "", + "mods/.. /secret", + "mods/secret.", + "mods/secret ", + "mods/mod.jar:payload", + "NUL/secret", + "mods/CON.jar", + "mods/mod\0.jar", + ] { + assert!(safe_relative_join(dir, path).is_err(), "{path:?}"); + } } #[test] diff --git a/packages/app-lib/src/api/servers/forge.rs b/packages/app-lib/src/api/servers/forge.rs index 0d1f4a3..efb8b44 100644 --- a/packages/app-lib/src/api/servers/forge.rs +++ b/packages/app-lib/src/api/servers/forge.rs @@ -14,7 +14,7 @@ use crate::event::emit::emit_server; use crate::util::io::IOError; use crate::{ErrorKind, Result}; -use super::files::download_to_dir; +use super::files::{download_to_dir, safe_relative_join}; use super::manifest::{ InstallState, read_manifest, server_path, write_manifest, }; @@ -41,6 +41,7 @@ pub async fn install_forge( drop(manifest); let installer_name = format!("forge-{mc_version}-{build}-installer.jar"); + let installer_path = safe_relative_join(&dir, &installer_name)?; let installer_url = format!("{FORGE_MAVEN}/{mc_version}-{build}/{installer_name}"); @@ -52,7 +53,6 @@ pub async fn install_forge( download_to_dir(server_id, &dir, &installer_url, &installer_name, None) .await?; - let installer_path = dir.join(&installer_name); let java = java_path.clone().unwrap_or_else(|| "java".to_string()); log(server_id, "Running Forge installer (this may take a while)").await?; diff --git a/packages/app-lib/src/api/servers/lifecycle.rs b/packages/app-lib/src/api/servers/lifecycle.rs index f259b76..45814d2 100644 --- a/packages/app-lib/src/api/servers/lifecycle.rs +++ b/packages/app-lib/src/api/servers/lifecycle.rs @@ -18,6 +18,7 @@ use crate::state::{clear_log_buffer, get_log_buffer, push_log_line}; use crate::util::io::IOError; use crate::{ErrorKind, Result}; +use super::files::safe_relative_join; use super::logs::{ analyze_exit_reason, stream_server_output, stream_server_pty_output, tail_server_log_file, @@ -131,7 +132,7 @@ async fn start_inner( forge_launch_args(&dir)? } else { let jar_name = resolve_jar_name(&manifest); - let jar_path = dir.join(&jar_name); + let jar_path = safe_relative_join(&dir, &jar_name)?; if !jar_path.exists() { return Err(ErrorKind::LauncherError(format!( "Server jar not found: {jar_name}. Download the server files first." @@ -275,9 +276,11 @@ async fn start_inner( // so these lines stop the console from looking frozen. let loader_first_run = matches!(manifest.server_type.as_str(), "fabric" | "quilt") - && !dir - .join(format!("{}-server-launch.jar", manifest.server_type)) - .exists(); + && !safe_relative_join( + &dir, + &format!("{}-server-launch.jar", manifest.server_type), + )? + .exists(); log_server_step( server_id, diff --git a/packages/app-lib/src/api/servers/manage.rs b/packages/app-lib/src/api/servers/manage.rs index f6b517c..5890c59 100644 --- a/packages/app-lib/src/api/servers/manage.rs +++ b/packages/app-lib/src/api/servers/manage.rs @@ -7,6 +7,7 @@ use crate::state::remove_log_buffer; use crate::util::io::{self, IOError}; use crate::{ErrorKind, Result, State}; +use super::files::safe_relative_join; use super::lifecycle::is_running; use super::manifest::{ ServerInfo, ServerManifest, build_server_info, read_manifest, @@ -72,7 +73,7 @@ pub async fn create( let state = State::get().await?; let id = Uuid::new_v4().to_string(); let dir_name = format!("{}-{}", sanitize_folder_name(name), &id[..8]); - let dir = state.directories.servers_dir().join(&dir_name); + let dir = safe_relative_join(&state.directories.servers_dir(), &dir_name)?; io::create_dir_all(&dir).await?; let manifest = ServerManifest { diff --git a/packages/app-lib/src/api/servers/manifest.rs b/packages/app-lib/src/api/servers/manifest.rs index 8d2d464..1985d9d 100644 --- a/packages/app-lib/src/api/servers/manifest.rs +++ b/packages/app-lib/src/api/servers/manifest.rs @@ -118,7 +118,7 @@ pub(super) async fn server_path(server_id: &str) -> Result { .as_error()); } let state = State::get().await?; - let path = state.directories.server_dir(server_id); + let path = state.directories.server_dir(server_id)?; if !path.is_dir() { return Err(ErrorKind::InputError(format!( "Unknown server: {server_id}" diff --git a/packages/app-lib/src/api/servers/modpack.rs b/packages/app-lib/src/api/servers/modpack.rs index cf73c93..7e0d252 100644 --- a/packages/app-lib/src/api/servers/modpack.rs +++ b/packages/app-lib/src/api/servers/modpack.rs @@ -33,7 +33,7 @@ use crate::util::fetch::{ use crate::util::io::IOError; use crate::{ErrorKind, Result}; -use super::files::download_to_dir; +use super::files::{download_to_dir, safe_relative_join}; use super::manifest::{ InstallState, ModpackInfo, read_manifest, server_path, write_manifest, }; @@ -92,10 +92,52 @@ struct MrpackFile { env: Option, #[serde(default)] downloads: Vec, - #[serde(default)] + #[serde(default, rename = "fileSize")] file_size: Option, } +impl MrpackFile { + fn integrity(&self) -> Result { + let sha1 = self.hashes.get("sha1").cloned(); + let sha512 = self.hashes.get("sha512").cloned(); + if sha1.is_none() && sha512.is_none() { + return Err(ErrorKind::InputError(format!( + "Modpack file {} is missing a SHA-1 or SHA-512 checksum", + self.path + )) + .as_error()); + } + for (hash, length) in [(&sha1, 40), (&sha512, 128)] { + if let Some(hash) = hash + && (hash.len() != length + || !hash.bytes().all(|byte| byte.is_ascii_hexdigit())) + { + return Err(ErrorKind::InputError(format!( + "Modpack file {} has an invalid checksum", + self.path + )) + .as_error()); + } + } + Ok(Integrity { + sha1, + sha512, + size: self.file_size, + ..Integrity::default() + }) + } +} + +/// Validate the entire index before starting concurrent downloads, including +/// entries excluded from server installation but fetched for metadata. +fn validate_index(index: &MrpackIndex, dir: &Path) -> Result<()> { + for file in &index.files { + safe_relative_join(dir, &file.path)?; + file.integrity()?; + } + Ok(()) +} + #[derive(Deserialize, Clone)] struct MrpackEnv { #[serde(default)] @@ -272,7 +314,7 @@ async fn run_modpack_install( &state, mrpack_url, &dir.join(MRPACK_FILENAME), - mrpack_sha1.map(str::to_string), + mrpack_sha1.map(Integrity::sha1).unwrap_or_default(), ResourceClass::Modpack, AggregateProgress::new(0), ) @@ -282,6 +324,7 @@ async fn run_modpack_install( let manifest_entry = find_manifest_entry(&archive_path).await?; let base_folder = base_folder(&manifest_entry); let index = parse_index(&archive_path, &manifest_entry).await?; + validate_index(&index, dir)?; let (installable_files, excluded_files): ( Vec<&MrpackFile>, @@ -310,19 +353,19 @@ async fn run_modpack_install( ) .await?; - let files: Vec<(String, String, Option, u64)> = installable_files + let files: Vec<(String, String, Integrity, u64)> = installable_files .iter() .filter_map(|file| { file.downloads.first().map(|url| { - ( + Ok(( file.path.clone(), url.clone(), - file.hashes.get("sha1").cloned(), + file.integrity()?, file.file_size.unwrap_or(0), - ) + )) }) }) - .collect(); + .collect::>()?; let progress = AggregateProgress::new(total_bytes); let state_ref = state.clone(); @@ -336,14 +379,14 @@ async fn run_modpack_install( 0.0, num_files, None, - move |(path, url, sha1, size)| { + move |(path, url, integrity, size)| { let server_id = server_id_ref.clone(); let dir = dir_ref.clone(); let state = state_ref.clone(); let progress = progress.clone(); async move { log(&server_id, &format!("Downloading {path}")).await?; - let destination = dir.join(&path); + let destination = safe_relative_join(&dir, &path)?; if let Some(parent) = destination.parent() { crate::util::io::create_dir_all(parent).await?; } @@ -352,7 +395,7 @@ async fn run_modpack_install( &state, &url, &destination, - sha1, + integrity, ResourceClass::Modpack, progress.clone(), ) @@ -446,7 +489,7 @@ async fn download_icon(dir: &Path, icon_url: &str) -> Result { ) }) .unwrap_or_else(|| "png".to_string()); - let destination = dir.join(format!("icon.{extension}")); + let destination = safe_relative_join(dir, &format!("icon.{extension}"))?; let client = reqwest::Client::builder() .user_agent(crate::launcher_user_agent()) @@ -476,15 +519,13 @@ async fn download_with_engine( state: &State, url: &str, destination: &Path, - sha1: Option, + integrity: Integrity, resource: ResourceClass, progress: AggregateProgress, ) -> Result<()> { - let mut request = DownloadRequest::new(url, resource); - if let Some(sha1) = &sha1 { - request = request.with_integrity(Integrity::sha1(sha1.clone())); - } - request = request.with_segmented_download(true); + let request = DownloadRequest::new(url, resource) + .with_integrity(integrity) + .with_segmented_download(true); let server_id = server_id.to_string(); let progress = progress.clone(); @@ -579,7 +620,7 @@ async fn fetch_excluded_mod_ids( state: &State, excluded_files: &[&MrpackFile], ) -> Result> { - let candidates: Vec<(String, String, Option)> = excluded_files + let candidates: Vec<(String, String, Integrity)> = excluded_files .iter() .filter_map(|file| { let path = file.path.replace('\\', "/"); @@ -589,14 +630,14 @@ async fn fetch_excluded_mod_ids( return None; } file.downloads.first().map(|url| { - ( + Ok(( path.rsplit('/').next().unwrap_or(&path).to_string(), url.clone(), - file.hashes.get("sha1").cloned(), - ) + file.integrity()?, + )) }) }) - .collect(); + .collect::>()?; if candidates.is_empty() { return Ok(HashSet::new()); } @@ -611,9 +652,10 @@ async fn fetch_excluded_mod_ids( ErrorKind::FSError(format!("Failed to create temporary directory: {e}")) })?; let mut unavailable = HashSet::new(); - for (filename, url, sha1) in candidates { - let destination = temp_dir.path().join(&filename); - match download_metadata_jar(state, &url, &destination, sha1).await { + for (filename, url, integrity) in candidates { + let destination = safe_relative_join(temp_dir.path(), &filename)?; + match download_metadata_jar(state, &url, &destination, integrity).await + { Ok(()) => match read_mod_metadata(&destination) { Some(metadata) => { unavailable @@ -647,12 +689,10 @@ async fn download_metadata_jar( state: &State, url: &str, destination: &Path, - sha1: Option, + integrity: Integrity, ) -> Result<()> { - let mut request = DownloadRequest::new(url, ResourceClass::Modpack); - if let Some(sha1) = &sha1 { - request = request.with_integrity(Integrity::sha1(sha1.clone())); - } + let request = DownloadRequest::new(url, ResourceClass::Modpack) + .with_integrity(integrity); download_to_path( request, destination, @@ -908,6 +948,122 @@ mod tests { use super::*; use std::io::Write; + fn index_file(path: &str) -> serde_json::Value { + serde_json::json!({ + "path": path, + "hashes": { "sha1": "0".repeat(40) }, + "downloads": ["https://example.com/mod.jar"], + "fileSize": 10 + }) + } + + #[tokio::test] + async fn malicious_index_paths_fail_before_materializing_files() { + let temp = tempfile::tempdir().unwrap(); + let server = temp.path().join("server"); + std::fs::create_dir(&server).unwrap(); + let outside = temp.path().join("outside.txt"); + std::fs::write(&outside, "unchanged").unwrap(); + let archive = temp.path().join("pack.mrpack"); + for path in [ + "../outside.txt", + "mods/../../outside.txt", + r"..\outside.txt", + r"mods/..\../outside.txt", + "/tmp/outside.txt", + "C:/outside.txt", + r"C:\outside.txt", + "C:outside.txt", + r"\\server\share\outside.txt", + r"\outside.txt", + r"\\?\C:\outside.txt", + "mods//mod.jar", + "mods/./mod.jar", + "mods/.. /outside.txt", + "mods/mod.jar:payload", + "NUL/outside.txt", + "mods/CON.jar", + "", + ] { + for environment in ["required", "unsupported"] { + let mut malicious = index_file(path); + // Filtered entries must also fail before any downloads. + malicious["env"] = serde_json::json!({"server": environment}); + let contents = serde_json::json!({ + "files": [index_file("mods/valid.jar"), malicious] + }) + .to_string(); + create_jar(&archive, &[(MRPACK_MANIFEST_ENTRY, &contents)]); + let index = + parse_index(&archive, MRPACK_MANIFEST_ENTRY).await.unwrap(); + assert!(validate_index(&index, &server).is_err(), "{path:?}"); + assert_eq!( + std::fs::read_to_string(&outside).unwrap(), + "unchanged" + ); + assert_eq!(std::fs::read_dir(&server).unwrap().count(), 0); + } + } + } + + #[test] + fn index_requires_valid_supported_checksums() { + for hashes in [ + serde_json::json!({}), + serde_json::json!({"md5": "0".repeat(32)}), + serde_json::json!({"sha1": ""}), + serde_json::json!({"sha1": "0".repeat(39)}), + serde_json::json!({"sha512": "g".repeat(128)}), + serde_json::json!({"sha1": "invalid", "sha512": "0".repeat(128)}), + ] { + let mut file = index_file("mods/mod.jar"); + file["hashes"] = hashes; + let index: MrpackIndex = + serde_json::from_value(serde_json::json!({"files": [file]})) + .unwrap(); + assert!(validate_index(&index, Path::new("server")).is_err()); + } + let mut file = index_file("mods/mod.jar"); + file.as_object_mut().unwrap().remove("hashes"); + let index: MrpackIndex = + serde_json::from_value(serde_json::json!({"files": [file]})) + .unwrap(); + assert!(validate_index(&index, Path::new("server")).is_err()); + } + + #[tokio::test] + async fn index_checksums_verify_file_contents() { + use crate::util::fetch::verify_file; + use sha2::{Digest, Sha512}; + + let temp = tempfile::tempdir().unwrap(); + let target = temp.path().join("mod.jar"); + let contents = b"verified mod"; + std::fs::write(&target, contents).unwrap(); + for hashes in [ + serde_json::json!({"sha1": sha1_smol::Sha1::from(contents).digest().to_string()}), + serde_json::json!({"sha512": format!("{:x}", Sha512::digest(contents))}), + serde_json::json!({ + "sha1": sha1_smol::Sha1::from(contents).digest().to_string(), + "sha512": format!("{:x}", Sha512::digest(contents)) + }), + ] { + let mut file = index_file("mods/mod.jar"); + file["hashes"] = hashes; + file["fileSize"] = serde_json::json!(contents.len()); + let index: MrpackIndex = + serde_json::from_value(serde_json::json!({"files": [file]})) + .unwrap(); + validate_index(&index, temp.path()).unwrap(); + let integrity = index.files[0].integrity().unwrap(); + assert_eq!(integrity.size, Some(contents.len() as u64)); + verify_file(&target, &integrity).await.unwrap(); + std::fs::write(&target, b"tampered mod").unwrap(); + assert!(verify_file(&target, &integrity).await.is_err()); + std::fs::write(&target, contents).unwrap(); + } + } + #[test] fn server_only_files_are_installable() { let installable = MrpackFile { diff --git a/packages/app-lib/src/state/dirs.rs b/packages/app-lib/src/state/dirs.rs index 4d48b76..c29bf92 100644 --- a/packages/app-lib/src/state/dirs.rs +++ b/packages/app-lib/src/state/dirs.rs @@ -171,8 +171,8 @@ impl DirectoryInfo { /// Gets the directory of a managed dedicated server by id #[inline] - pub fn server_dir(&self, server_id: &str) -> PathBuf { - self.servers_dir().join(server_id) + pub fn server_dir(&self, server_id: &str) -> crate::Result { + crate::util::io::safe_relative_join(&self.servers_dir(), server_id) } #[inline] @@ -708,6 +708,28 @@ mod resolve_game_dir_tests { } } + #[test] + fn server_directory_rejects_absolute_and_windows_alias_paths() { + let dirs = dirs(); + assert_eq!( + dirs.server_dir("server-1234").unwrap(), + dirs.servers_dir().join("server-1234") + ); + for id in [ + "../outside", + "C:outside", + "C:/outside", + r"\outside", + r"\\server\share", + ".", + "NUL", + "server:stream", + "server.", + ] { + assert!(dirs.server_dir(id).is_err(), "{id:?}"); + } + } + #[test] fn override_used_when_set_and_absolute() { let dirs = dirs(); diff --git a/packages/app-lib/src/util/io.rs b/packages/app-lib/src/util/io.rs index 6b6fa57..1ac23d0 100644 --- a/packages/app-lib/src/util/io.rs +++ b/packages/app-lib/src/util/io.rs @@ -60,6 +60,34 @@ pub(crate) fn io_error_with_lock_info( io_error_with_lock_info_for_paths(source, path, &[path]) } +/// Joins an untrusted Unix-style relative file path, rejecting traversal and +/// Windows aliases on every platform. This is a lexical check, not a symlink +/// sandbox. +pub(crate) fn safe_relative_join( + root: &Path, + relative: &str, +) -> crate::Result { + path_util::SafeRelativeUtf8UnixPathBuf::try_from(relative.to_string()) + .map_err(|error| { + crate::ErrorKind::InputError(format!( + "Invalid file path {relative}: {error}" + )) + })?; + if relative.split('/').any(|segment| { + segment.is_empty() + || segment == "." + || segment.ends_with(['.', ' ']) + || segment.contains(':') + || segment.chars().any(char::is_control) + }) { + return Err(crate::ErrorKind::InputError(format!( + "Invalid file path: {relative}" + )) + .into()); + } + Ok(root.join(relative)) +} + /// Join a user-provided relative path onto `root`, rejecting traversal. pub(crate) fn join_within_root( root: &Path, @@ -95,7 +123,7 @@ pub(crate) fn join_within_root( ) .into()); } - Ok(root.join(safe)) + safe_relative_join(root, &safe.to_string_lossy().replace('\\', "/")) } pub(crate) fn io_error_with_lock_info_for_paths( @@ -895,6 +923,39 @@ mod windows_sharing_violation_tests { } } +#[cfg(test)] +mod path_join_tests { + use super::*; + + #[test] + fn join_within_root_uses_portable_path_validation() { + let root = Path::new("root"); + assert_eq!( + join_within_root(root, r"config\example.toml").unwrap(), + root.join("config/example.toml") + ); + assert_eq!( + join_within_root(root, "./config/./example.toml").unwrap(), + root.join("config/example.toml") + ); + for relative in [ + "../outside", + r"..\outside", + "/outside", + "C:/outside", + "C:outside", + r"\\server\share", + "config/.. /outside", + "config/file:stream", + "config/NUL", + "config/file.", + "config/file\0", + ] { + assert!(join_within_root(root, relative).is_err(), "{relative:?}"); + } + } +} + #[cfg(test)] mod copy_dir_tests { use super::*; From 629fe9c26a14fd07312f2fc77b80359fc7147634 Mon Sep 17 00:00:00 2001 From: Disy Date: Tue, 20 Oct 2026 17:07:32 +0800 Subject: [PATCH 03/11] chore: bump launcher version to 1.0.2-beta10 --- Cargo.lock | 4 ++-- apps/app-frontend/package.json | 2 +- apps/app/Cargo.toml | 2 +- packages/app-lib/Cargo.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f52fb7..0bac7a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6841,7 +6841,7 @@ dependencies = [ [[package]] name = "theseus" -version = "1.0.2-beta7" +version = "1.0.2-beta10" dependencies = [ "ariadne", "async-minecraft-ping", @@ -6945,7 +6945,7 @@ dependencies = [ [[package]] name = "theseus_gui" -version = "1.0.2-beta7" +version = "1.0.2-beta10" dependencies = [ "ashpd", "async_zip", diff --git a/apps/app-frontend/package.json b/apps/app-frontend/package.json index a884400..372548f 100644 --- a/apps/app-frontend/package.json +++ b/apps/app-frontend/package.json @@ -1,7 +1,7 @@ { "name": "@modrinth/app-frontend", "private": true, - "version": "1.0.2-beta9", + "version": "1.0.2-beta10", "type": "module", "scripts": { "dev": "vite", diff --git a/apps/app/Cargo.toml b/apps/app/Cargo.toml index d5b4f48..624d7d6 100644 --- a/apps/app/Cargo.toml +++ b/apps/app/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "theseus_gui" # The actual version is set by the theseus-build workflow on tagging -version = "1.0.2-beta7" +version = "1.0.2-beta10" edition.workspace = true description = "Axolotl Launcher is a cross-platform Minecraft launcher" homepage = "https://www.ghs.red" diff --git a/packages/app-lib/Cargo.toml b/packages/app-lib/Cargo.toml index 0e58320..6714417 100644 --- a/packages/app-lib/Cargo.toml +++ b/packages/app-lib/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "theseus" # The actual version is set by the theseus-build workflow on tagging -version = "1.0.2-beta7" +version = "1.0.2-beta10" edition.workspace = true [dependencies] From 0098e9eb07333b537014fdc4ca4da17fd7d11b55 Mon Sep 17 00:00:00 2001 From: Disy Date: Tue, 20 Oct 2026 17:10:39 +0800 Subject: [PATCH 04/11] docs: add beta10 security release notes --- .../app-frontend/src/announcements/catalog.ts | 237 ++++++++++++------ 1 file changed, 155 insertions(+), 82 deletions(-) diff --git a/apps/app-frontend/src/announcements/catalog.ts b/apps/app-frontend/src/announcements/catalog.ts index 3bc807a..9a88a76 100644 --- a/apps/app-frontend/src/announcements/catalog.ts +++ b/apps/app-frontend/src/announcements/catalog.ts @@ -32,6 +32,36 @@ export const ANNOUNCEMENT_CHANGE_TYPES: readonly AnnouncementChangeType[] = [ ] export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ + { + id: 'launcher-1.0.2-beta10', + version: '1.0.2-beta10', + publishedAt: '2026-10-20', + title: { + 'en-US': 'Starlight Launcher 1.0.2-beta10', + 'zh-CN': 'Starlight Launcher 1.0.2-beta10', + }, + changes: { + security: [ + { + 'en-US': + 'Backported the upstream Axolotl modpack path traversal fix to prevent malicious modpacks from writing files outside the intended installation directory.', + 'zh-CN': + '合入上游 Axolotl 整合包路径穿越安全补丁,阻止恶意整合包向安装目录之外写入文件。', + }, + { + 'en-US': + 'Strengthened path validation for Modrinth and CurseForge modpacks, server installations, cached files and modpack uninstall cleanup, including unsafe Windows path aliases.', + 'zh-CN': + '加强 Modrinth 与 CurseForge 整合包、服务端安装、缓存文件及整合包卸载清理的路径校验,拦截不安全的 Windows 路径别名。', + }, + ], + }, + notes: { + 'en-US': + 'Users of earlier versions should update promptly, especially before installing modpacks from untrusted sources.', + 'zh-CN': '建议旧版本用户尽快更新,尤其是在安装来源不明的整合包之前。', + }, + }, { id: 'launcher-1.0.2-beta9', version: '1.0.2-beta9', @@ -43,21 +73,27 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ changes: { added: [ { - 'en-US': 'Finished the skin site admin console: system overview, announcements, user and player management, review paper editor, mod upload with batch operations, chat room management with real-time voice, servers, audit log, resource downloads, donations and launcher releases.', - 'zh-CN': '补全皮肤站管理台:系统概览、公告、用户与玩家管理、审核试卷编辑器、Mod 上传与批量操作、聊天室房间管理与实时语音、服务器、审计日志、资源下载、捐赠与启动器版本管理。', + 'en-US': + 'Finished the skin site admin console: system overview, announcements, user and player management, review paper editor, mod upload with batch operations, chat room management with real-time voice, servers, audit log, resource downloads, donations and launcher releases.', + 'zh-CN': + '补全皮肤站管理台:系统概览、公告、用户与玩家管理、审核试卷编辑器、Mod 上传与批量操作、聊天室房间管理与实时语音、服务器、审计日志、资源下载、捐赠与启动器版本管理。', }, { - 'en-US': 'Added Wiki management to the admin console, including a MediaWiki migration center that imports pages from an existing wiki and lets you review, publish or roll back the result.', - 'zh-CN': '管理台新增维基管理,包含 MediaWiki 迁移中心:可从现有 Wiki 导入页面,并对结果进行审核、发布或回滚。', + 'en-US': + 'Added Wiki management to the admin console, including a MediaWiki migration center that imports pages from an existing wiki and lets you review, publish or roll back the result.', + 'zh-CN': + '管理台新增维基管理,包含 MediaWiki 迁移中心:可从现有 Wiki 导入页面,并对结果进行审核、发布或回滚。', }, { - 'en-US': 'Added a read-only Wiki reader in the launcher: render pages, follow links between entries, and keep a back stack while browsing.', + 'en-US': + 'Added a read-only Wiki reader in the launcher: render pages, follow links between entries, and keep a back stack while browsing.', 'zh-CN': '启动器新增维基阅读页:渲染词条正文、跟随词条链接跳转,并在浏览时保留返回栈。', }, ], changed: [ { - 'en-US': 'Releases are now published to the skin site automatically when a tag is pushed, so the download page stays in sync without a manual upload.', + 'en-US': + 'Releases are now published to the skin site automatically when a tag is pushed, so the download page stays in sync without a manual upload.', 'zh-CN': '推送 tag 发版时会自动把版本同步到皮肤站,下载页无需再手动上传即可保持同步。', }, { @@ -88,27 +124,35 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ changes: { added: [ { - 'en-US': 'The skin site is now fully native inside the launcher: sign-in, registration, password reset, account settings, players, downloads, world map and player review all run through the launcher UI instead of an embedded web page.', - 'zh-CN': '皮肤站全面改为启动器原生界面:登录、注册、找回密码、账号设置、角色管理、资源下载、服务器地图与玩家审核均不再依赖内嵌网页。', + 'en-US': + 'The skin site is now fully native inside the launcher: sign-in, registration, password reset, account settings, players, downloads, world map and player review all run through the launcher UI instead of an embedded web page.', + 'zh-CN': + '皮肤站全面改为启动器原生界面:登录、注册、找回密码、账号设置、角色管理、资源下载、服务器地图与玩家审核均不再依赖内嵌网页。', }, { - 'en-US': 'Added a bug tracker: browse and filter issues, read details and comments, post replies, create new issues and follow notifications.', - 'zh-CN': '新增问题追踪:可浏览与筛选问题、查看详情与评论、发表回复、提交新问题并接收通知。', + 'en-US': + 'Added a bug tracker: browse and filter issues, read details and comments, post replies, create new issues and follow notifications.', + 'zh-CN': + '新增问题追踪:可浏览与筛选问题、查看详情与评论、发表回复、提交新问题并接收通知。', }, { - 'en-US': 'Added an admin console with a permission gate: system overview, announcements, users, players, player review and launcher releases.', - 'zh-CN': '新增管理控制台(带权限校验):系统概览、公告管理、用户管理、玩家管理、玩家审核与启动器版本管理。', + 'en-US': + 'Added an admin console with a permission gate: system overview, announcements, users, players, player review and launcher releases.', + 'zh-CN': + '新增管理控制台(带权限校验):系统概览、公告管理、用户管理、玩家管理、玩家审核与启动器版本管理。', }, ], changed: [ { - 'en-US': 'Hosted modpack downloads now go through the dedicated download server, keeping large downloads off the main API host.', + 'en-US': + 'Hosted modpack downloads now go through the dedicated download server, keeping large downloads off the main API host.', 'zh-CN': '整合包下载改为走独立的下载服务器,避免大文件下载挤占主 API 服务。', }, ], fixed: [ { - 'en-US': 'LogShare now collects logs with a lightweight reader, fixing crash log uploads that previously failed.', + 'en-US': + 'LogShare now collects logs with a lightweight reader, fixing crash log uploads that previously failed.', 'zh-CN': 'LogShare 改用轻量日志收集,修复崩溃日志上传失败的问题。', }, { @@ -137,12 +181,16 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ 'zh-CN': '修复实时日志积压导致控制台卡顿的问题。', }, { - 'en-US': 'The update channel now defaults to the build type: installing a Beta build starts on the Beta channel instead of Release.', - 'zh-CN': '更新通道默认值改为跟随安装包版本:安装 Beta 版后默认使用 Beta 通道,不再回落为 Release。', + 'en-US': + 'The update channel now defaults to the build type: installing a Beta build starts on the Beta channel instead of Release.', + 'zh-CN': + '更新通道默认值改为跟随安装包版本:安装 Beta 版后默认使用 Beta 通道,不再回落为 Release。', }, { - 'en-US': 'On Linux, the default game directory for StarLight instances no longer points at the system install path (e.g. /usr/bin); it falls back to the home directory.', - 'zh-CN': 'Linux 上 StarLight 实例的默认游戏目录不再指向系统安装路径(如 /usr/bin),改为回退到用户主目录。', + 'en-US': + 'On Linux, the default game directory for StarLight instances no longer points at the system install path (e.g. /usr/bin); it falls back to the home directory.', + 'zh-CN': + 'Linux 上 StarLight 实例的默认游戏目录不再指向系统安装路径(如 /usr/bin),改为回退到用户主目录。', }, ], }, @@ -158,15 +206,19 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ changes: { added: [ { - 'en-US': 'Added keyboard shortcut settings: enable and rebind navigation and quick-scrolling shortcuts.', + 'en-US': + 'Added keyboard shortcut settings: enable and rebind navigation and quick-scrolling shortcuts.', 'zh-CN': '新增键盘快捷键设置:可启用并自定义导航与快速滚动快捷键。', }, { - 'en-US': 'Added crash log sharing and AI analysis: share crash diagnostics to LogShare for a structured summary and optional AI analysis, with a rebuilt crash window that browses log files and streams the analysis.', - 'zh-CN': '新增崩溃日志分享与 AI 分析:可将崩溃诊断分享到 LogShare 获取结构化摘要与 AI 分析;崩溃窗口重做为双栏布局,可浏览日志文件并流式查看分析结果。', + 'en-US': + 'Added crash log sharing and AI analysis: share crash diagnostics to LogShare for a structured summary and optional AI analysis, with a rebuilt crash window that browses log files and streams the analysis.', + 'zh-CN': + '新增崩溃日志分享与 AI 分析:可将崩溃诊断分享到 LogShare 获取结构化摘要与 AI 分析;崩溃窗口重做为双栏布局,可浏览日志文件并流式查看分析结果。', }, { - 'en-US': 'Crash logs now also collect JVM crash reports (hs_err_pid*.log) and launcher logs.', + 'en-US': + 'Crash logs now also collect JVM crash reports (hs_err_pid*.log) and launcher logs.', 'zh-CN': '崩溃日志现在也会收集 JVM 崩溃报告(hs_err_pid*.log)与启动器日志。', }, { @@ -207,12 +259,16 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ changes: { added: [ { - 'en-US': 'Added a remote announcements center: a bell in the top bar opens a panel listing launcher announcements fetched from the Starlight skin site.', - 'zh-CN': '新增远程公告中心:顶栏的铃铛可打开公告面板,展示从 Starlight 皮肤站拉取的启动器公告。', + 'en-US': + 'Added a remote announcements center: a bell in the top bar opens a panel listing launcher announcements fetched from the Starlight skin site.', + 'zh-CN': + '新增远程公告中心:顶栏的铃铛可打开公告面板,展示从 Starlight 皮肤站拉取的启动器公告。', }, { - 'en-US': 'Added a custom Minecraft window title option: set a global toggle and default title, and let each instance override it. The game window is renamed on Windows.', - 'zh-CN': '新增自定义 Minecraft 窗口标题:可设置全局开关与默认标题,并允许各实例单独覆盖;Windows 下会重命名游戏窗口。', + 'en-US': + 'Added a custom Minecraft window title option: set a global toggle and default title, and let each instance override it. The game window is renamed on Windows.', + 'zh-CN': + '新增自定义 Minecraft 窗口标题:可设置全局开关与默认标题,并允许各实例单独覆盖;Windows 下会重命名游戏窗口。', }, ], fixed: [ @@ -234,24 +290,31 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ changes: { added: [ { - 'en-US': 'Added armor and armor-trim rendering to the skin preview, letting you try on armor pieces and trim patterns directly in the skin editor.', - 'zh-CN': '皮肤预览新增盔甲与盔甲纹饰渲染,可在皮肤编辑器内直接试穿各部位盔甲并调整纹饰图案。', + 'en-US': + 'Added armor and armor-trim rendering to the skin preview, letting you try on armor pieces and trim patterns directly in the skin editor.', + 'zh-CN': + '皮肤预览新增盔甲与盔甲纹饰渲染,可在皮肤编辑器内直接试穿各部位盔甲并调整纹饰图案。', }, { - 'en-US': 'Armor preview selections in the skin editor are now saved locally, so they persist across sessions.', + 'en-US': + 'Armor preview selections in the skin editor are now saved locally, so they persist across sessions.', 'zh-CN': '皮肤编辑器中的盔甲预览选择现在会保存在本地,跨会话保留。', }, ], changed: [ { - 'en-US': 'Improved download scheduling to reduce concurrency automatically when tasks are waiting for capacity, making large Minecraft installations more reliable on constrained connections.', - 'zh-CN': '优化下载任务调度:任务因容量不足排队时会自动降低并发,提升大型 Minecraft 版本在受限网络环境下的安装稳定性。', + 'en-US': + 'Improved download scheduling to reduce concurrency automatically when tasks are waiting for capacity, making large Minecraft installations more reliable on constrained connections.', + 'zh-CN': + '优化下载任务调度:任务因容量不足排队时会自动降低并发,提升大型 Minecraft 版本在受限网络环境下的安装稳定性。', }, ], fixed: [ { - 'en-US': 'Fixed downloads timing out while waiting for internal connection capacity, which could cause Minecraft installations to fail even when the network was available.', - 'zh-CN': '修复下载任务等待内部连接容量时过早超时,导致网络正常的情况下 Minecraft 安装仍可能失败的问题。', + 'en-US': + 'Fixed downloads timing out while waiting for internal connection capacity, which could cause Minecraft installations to fail even when the network was available.', + 'zh-CN': + '修复下载任务等待内部连接容量时过早超时,导致网络正常的情况下 Minecraft 安装仍可能失败的问题。', }, { 'en-US': 'Fixed instance group changes not saving for some instances.', @@ -271,8 +334,10 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ changes: { fixed: [ { - 'en-US': 'Fixed the anniversary celebration banner not showing above the window title bar, and made the banner background semi-transparent so the content behind it stays visible.', - 'zh-CN': '修复周年庆庆祝横幅被窗口标题栏遮挡、无法显示的问题,并将横幅背景改为半透明,避免遮住下方内容。', + 'en-US': + 'Fixed the anniversary celebration banner not showing above the window title bar, and made the banner background semi-transparent so the content behind it stays visible.', + 'zh-CN': + '修复周年庆庆祝横幅被窗口标题栏遮挡、无法显示的问题,并将横幅背景改为半透明,避免遮住下方内容。', }, ], }, @@ -288,8 +353,10 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ changes: { fixed: [ { - 'en-US': 'Fixed the launcher failing to start with "no such table: synced_pack_catalog": the cross-instance sync migration was missing the synced_pack_catalog and synced_pack_instances tables.', - 'zh-CN': '修复启动器因 "no such table: synced_pack_catalog" 无法启动的问题:跨实例同步迁移遗漏了 synced_pack_catalog 与 synced_pack_instances 两张表。', + 'en-US': + 'Fixed the launcher failing to start with "no such table: synced_pack_catalog": the cross-instance sync migration was missing the synced_pack_catalog and synced_pack_instances tables.', + 'zh-CN': + '修复启动器因 "no such table: synced_pack_catalog" 无法启动的问题:跨实例同步迁移遗漏了 synced_pack_catalog 与 synced_pack_instances 两张表。', }, ], }, @@ -305,15 +372,20 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ changes: { added: [ { - 'en-US': 'Cross-instance sync: share game options, resource packs, data packs, multiplayer servers, command history, and creative hotbars across instances, with per-option toggles and a fine-grained game-settings editor.', - 'zh-CN': '跨实例同步:可跨实例共享游戏选项、资源包、数据包、多人服务器、命令历史与创造模式快捷栏,支持逐项开关与精细的游戏设置编辑器。', + 'en-US': + 'Cross-instance sync: share game options, resource packs, data packs, multiplayer servers, command history, and creative hotbars across instances, with per-option toggles and a fine-grained game-settings editor.', + 'zh-CN': + '跨实例同步:可跨实例共享游戏选项、资源包、数据包、多人服务器、命令历史与创造模式快捷栏,支持逐项开关与精细的游戏设置编辑器。', }, { - 'en-US': 'Starlight turns 12! On October 4th every year the launcher now shows a celebration banner and falling confetti. The anniversary number is computed from 2014.', - 'zh-CN': 'Starlight 十二周年!每年 10 月 4 日,启动器会显示庆祝横幅与飘落的彩带、蛋糕,周年数按 2014 年起算。', + 'en-US': + 'Starlight turns 12! On October 4th every year the launcher now shows a celebration banner and falling confetti. The anniversary number is computed from 2014.', + 'zh-CN': + 'Starlight 十二周年!每年 10 月 4 日,启动器会显示庆祝横幅与飘落的彩带、蛋糕,周年数按 2014 年起算。', }, { - 'en-US': 'Added a developer feature flag to preview the anniversary celebration at any time.', + 'en-US': + 'Added a developer feature flag to preview the anniversary celebration at any time.', 'zh-CN': '新增开发者功能开关,可随时预览周年庆效果。', }, ], @@ -330,8 +402,10 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ changes: { added: [ { - 'en-US': 'Instance backups: create deduplicated snapshots of an instance while it is closed, preview and restore them, exclude specific files or folders, and move the backup repository.', - 'zh-CN': '实例备份:在实例关闭时创建去重快照,可预览与恢复,支持排除指定文件或文件夹,并可迁移备份仓库。', + 'en-US': + 'Instance backups: create deduplicated snapshots of an instance while it is closed, preview and restore them, exclude specific files or folders, and move the backup repository.', + 'zh-CN': + '实例备份:在实例关闭时创建去重快照,可预览与恢复,支持排除指定文件或文件夹,并可迁移备份仓库。', }, { 'en-US': "Customize the launcher's UI font and monospace font in appearance settings.", @@ -340,12 +414,16 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ ], fixed: [ { - 'en-US': 'Fixed beta builds not auto-updating from beta9 to beta10 (prerelease versions were compared as text instead of by number).', - 'zh-CN': '修复 beta 版本无法从 beta9 自动更新到 beta10 的问题(预发布版本此前按文本而非数字比较)。', + 'en-US': + 'Fixed beta builds not auto-updating from beta9 to beta10 (prerelease versions were compared as text instead of by number).', + 'zh-CN': + '修复 beta 版本无法从 beta9 自动更新到 beta10 的问题(预发布版本此前按文本而非数字比较)。', }, { - 'en-US': 'Fixed a cross-crate visibility issue that prevented the launcher from compiling with the ignore-SSL-errors option.', - 'zh-CN': '修复因跨 crate 可见性导致的编译失败,此前启用“忽略 SSL 证书错误”选项时无法编译。', + 'en-US': + 'Fixed a cross-crate visibility issue that prevented the launcher from compiling with the ignore-SSL-errors option.', + 'zh-CN': + '修复因跨 crate 可见性导致的编译失败,此前启用“忽略 SSL 证书错误”选项时无法编译。', }, ], }, @@ -361,11 +439,13 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ changes: { added: [ { - 'en-US': 'Reworked the instance-page tour into four steps covering content, files, worlds, and logs.', + 'en-US': + 'Reworked the instance-page tour into four steps covering content, files, worlds, and logs.', 'zh-CN': '重做实例页导览,分为内容、文件、世界、日志四步。', }, { - 'en-US': 'When the install location is a drive root, the installer now creates a Starlight subfolder automatically.', + 'en-US': + 'When the install location is a drive root, the installer now creates a Starlight subfolder automatically.', 'zh-CN': '安装位置选到盘符根目录时,安装器会自动创建 Starlight 子文件夹。', }, ], @@ -379,11 +459,13 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ 'zh-CN': '修复开发者功能开关无法保存的问题。', }, { - 'en-US': 'Fixed instance group edits not saving for instances without an applied content set.', + 'en-US': + 'Fixed instance group edits not saving for instances without an applied content set.', 'zh-CN': '修复没有应用内容集的实例无法保存分组修改的问题。', }, { - 'en-US': 'Fixed the launcher crashing on startup when feature flags were out of sync between the frontend and backend.', + 'en-US': + 'Fixed the launcher crashing on startup when feature flags were out of sync between the frontend and backend.', 'zh-CN': '修复前后端功能开关不同步导致启动器启动崩溃的问题。', }, { @@ -500,12 +582,10 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ { 'en-US': 'Fixed the onboarding tour skipping steps automatically, getting stuck, or not navigating between steps; the tour bubble no longer covers the left navigation bar.', - 'zh-CN': - '修复新手导览自动跳过、卡住或不跳转的问题;导览气泡不再遮挡左侧导航栏。', + 'zh-CN': '修复新手导览自动跳过、卡住或不跳转的问题;导览气泡不再遮挡左侧导航栏。', }, { - 'en-US': - 'Fixed the onboarding tour target on the minimal home layout.', + 'en-US': 'Fixed the onboarding tour target on the minimal home layout.', 'zh-CN': '修复精简主页下导览目标找不到的问题。', }, ], @@ -536,13 +616,11 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ ], fixed: [ { - 'en-US': - 'Fixed the developer feature flags page rendering blank.', + 'en-US': 'Fixed the developer feature flags page rendering blank.', 'zh-CN': '修复开发者“功能开关”页面空白的问题。', }, { - 'en-US': - 'Fixed a build error that prevented the launcher from compiling.', + 'en-US': 'Fixed a build error that prevented the launcher from compiling.', 'zh-CN': '修复导致启动器无法编译的构建错误。', }, ], @@ -559,8 +637,7 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ changes: { fixed: [ { - 'en-US': - 'Fixed a build error in 1.0.1-beta3 that prevented the launcher from compiling.', + 'en-US': 'Fixed a build error in 1.0.1-beta3 that prevented the launcher from compiling.', 'zh-CN': '修复 1.0.1-beta3 中导致启动器无法编译的构建错误。', }, ], @@ -619,8 +696,7 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ ], fixed: [ { - 'en-US': - 'Improved the reliability of live logs and database switching.', + 'en-US': 'Improved the reliability of live logs and database switching.', 'zh-CN': '提高实时日志与数据库切换的可靠性。', }, ], @@ -712,15 +788,13 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ 'zh-CN': '修复更新清单的下载地址与发布顺序,更新检查现在可以成功。', }, { - 'en-US': - 'Updated the signing key pair used to sign update packages.', + 'en-US': 'Updated the signing key pair used to sign update packages.', 'zh-CN': '更新了用于签名更新包的密钥对。', }, ], changed: [ { - 'en-US': - 'Removed upstream release announcements prior to 1.0.0-beta1.', + 'en-US': 'Removed upstream release announcements prior to 1.0.0-beta1.', 'zh-CN': '移除了 1.0.0-beta1 之前的上游版本公告。', }, ], @@ -737,13 +811,11 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ changes: { added: [ { - 'en-US': - 'Installing a modpack now lets you choose the game directory.', + 'en-US': 'Installing a modpack now lets you choose the game directory.', 'zh-CN': '一键安装整合包时现在可以选择游戏目录。', }, { - 'en-US': - 'Approved (hosted) modpack sync and StarLight instance mode were added.', + 'en-US': 'Approved (hosted) modpack sync and StarLight instance mode were added.', 'zh-CN': '新增获批整合包同步与 StarLight 实例模式。', }, { @@ -754,8 +826,7 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ ], changed: [ { - 'en-US': - 'The about page now plays a looping wither victory animation.', + 'en-US': 'The about page now plays a looping wither victory animation.', 'zh-CN': '关于页现在会播放循环的凋灵胜利动画。', }, ], @@ -767,8 +838,7 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ '修复从 .minecraft 文件夹导入实例的多项问题(内容复制、版本隔离、符号链接、加载器版本识别等)。', }, { - 'en-US': - 'Fixed the instance log view stopping updates and overlapping log lines.', + 'en-US': 'Fixed the instance log view stopping updates and overlapping log lines.', 'zh-CN': '修复实例日志窗口停更与日志行重叠的问题。', }, { @@ -787,8 +857,7 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ 'zh-CN': '修复未登录皮肤站时启动卡住且不再弹出玩家选择的问题。', }, { - 'en-US': - 'Fixed various skin site login, player list sync, and display issues.', + 'en-US': 'Fixed various skin site login, player list sync, and display issues.', 'zh-CN': '修复皮肤站登录、玩家列表同步及若干显示问题。', }, ], @@ -807,7 +876,8 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ { 'en-US': 'The launcher has been rebranded from Axolotl Launcher to Starlight Launcher, with a new icon and a refreshed about page.', - 'zh-CN': '启动器已从 Axolotl Launcher 更名为 Starlight Launcher,并更换了全新图标与关于页。', + 'zh-CN': + '启动器已从 Axolotl Launcher 更名为 Starlight Launcher,并更换了全新图标与关于页。', }, { 'en-US': @@ -821,7 +891,8 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ ], added: [ { - 'en-US': 'A "Starlight" entry was added to the sidebar, which embeds the StarLight skin site.', + 'en-US': + 'A "Starlight" entry was added to the sidebar, which embeds the StarLight skin site.', 'zh-CN': '侧边栏新增「斯达莱特」入口,内嵌 StarLight 皮肤站。', }, { @@ -840,10 +911,12 @@ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ { 'en-US': 'The hard-coded Starlight official server was removed from "Pinned servers"; only servers you favorite yourself are shown now.', - 'zh-CN': '移除了「固定的服务器」中写死的 Starlight 官方服务器,现在只显示你自己收藏的服务器。', + 'zh-CN': + '移除了「固定的服务器」中写死的 Starlight 官方服务器,现在只显示你自己收藏的服务器。', }, { - 'en-US': 'The swimming axolotl animation on the about page was replaced with a static nether star.', + 'en-US': + 'The swimming axolotl animation on the about page was replaced with a static nether star.', 'zh-CN': '关于页中游动的美西螈动画已替换为静止的下界之星。', }, ], From be3a1eda8cfd644d7dfb834bea1092a23f120ca3 Mon Sep 17 00:00:00 2001 From: Disy Date: Tue, 20 Oct 2026 19:14:58 +0800 Subject: [PATCH 05/11] fix(ci): publish skin site releases with chunked uploads Use bounded chunks with retries, processing deadlines and verified activation. Fail publication explicitly on upload errors. Add a manual recovery workflow for existing release packages. --- .github/workflows/build-on-tag.yml | 39 +--- .github/workflows/publish-skin-site.yml | 56 +++++ scripts/axolotl/publish-skin-site.mjs | 226 +++++++++++++++++++++ scripts/axolotl/publish-skin-site.test.mjs | 172 ++++++++++++++++ 4 files changed, 464 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/publish-skin-site.yml create mode 100644 scripts/axolotl/publish-skin-site.mjs create mode 100644 scripts/axolotl/publish-skin-site.test.mjs diff --git a/.github/workflows/build-on-tag.yml b/.github/workflows/build-on-tag.yml index 2e79483..a10e884 100644 --- a/.github/workflows/build-on-tag.yml +++ b/.github/workflows/build-on-tag.yml @@ -51,6 +51,7 @@ jobs: node scripts/axolotl/check-migrations.mjs --release node scripts/axolotl/brand-guard.mjs node scripts/axolotl/i18n-check.mjs + node --test scripts/axolotl/publish-skin-site.test.mjs cargo fmt --all --check - name: Check updater signing configuration @@ -286,6 +287,9 @@ jobs: name: Verify and publish needs: build runs-on: ubuntu-24.04 + concurrency: + group: skin-site-${{ github.ref_name }} + cancel-in-progress: false env: UPDATE_SERVER_URL: ${{ secrets.UPDATE_SERVER_URL }} UPDATE_SERVER_WEBHOOK_SECRET: ${{ secrets.UPDATE_SERVER_WEBHOOK_SECRET }} @@ -298,6 +302,11 @@ jobs: fetch-depth: 0 ref: ${{ github.ref }} + - name: Set up Node.js + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version-file: .nvmrc + - name: Download updater signatures env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -375,42 +384,14 @@ jobs: gh release upload "${GITHUB_REF_NAME}" "release-check/${bundle_name}" --clobber - name: Publish to skin site - continue-on-error: true env: SKIN_SITE_TOKEN: ${{ secrets.SKIN_SITE_CI_UPLOAD_TOKEN }} SKIN_SITE_URL: ${{ vars.SKIN_SITE_URL || 'https://skin.starlight.cool' }} run: | set -euo pipefail - if [ -z "${SKIN_SITE_TOKEN:-}" ]; then - echo "::warning::SKIN_SITE_CI_UPLOAD_TOKEN not configured, skipping skin site publish" - exit 0 - fi - version="${GITHUB_REF_NAME#v}" bundle="release-check/Starlight_Launcher_${version}_skin-site-update.zip" - if [ ! -f "${bundle}" ]; then - echo "::warning::Bundle ${bundle} not found, skipping skin site publish" - exit 0 - fi - - upload_response=$(curl -fsS -X POST \ - -H "Authorization: Bearer ${SKIN_SITE_TOKEN}" \ - -F "archive=@${bundle};type=application/zip" \ - "${SKIN_SITE_URL}/starlight/launcher/admin/upload?forceUpdate=false") - - release_id=$(printf '%s' "${upload_response}" | jq -r '.payload.id // empty') - if [ -z "${release_id}" ]; then - echo "::warning::Skin site upload did not return a release id: ${upload_response}" - exit 0 - fi - - echo "Uploaded release id: ${release_id}" - - curl -fsS -X POST \ - -H "Authorization: Bearer ${SKIN_SITE_TOKEN}" \ - "${SKIN_SITE_URL}/starlight/launcher/admin/${release_id}/activate" - - echo "Skin site release ${version} published" + node scripts/axolotl/publish-skin-site.mjs "${bundle}" "${GITHUB_REF_NAME}" - name: Notify Update Server to publish release assets if: ${{ env.UPDATE_SERVER_URL != '' && env.UPDATE_SERVER_WEBHOOK_SECRET != '' }} diff --git a/.github/workflows/publish-skin-site.yml b/.github/workflows/publish-skin-site.yml new file mode 100644 index 0000000..96665c8 --- /dev/null +++ b/.github/workflows/publish-skin-site.yml @@ -0,0 +1,56 @@ +name: Sync release to skin site + +on: + workflow_dispatch: + inputs: + tag: + description: Existing GitHub release tag to sync (for example v1.0.2-beta10) + required: true + type: string + +permissions: + contents: read + +concurrency: + group: skin-site-${{ inputs.tag }} + cancel-in-progress: false + +jobs: + publish: + name: Upload and activate existing release + runs-on: ubuntu-24.04 + timeout-minutes: 40 + steps: + - name: Check out publisher + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Node.js + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version-file: .nvmrc + + - name: Download complete update package + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ inputs.tag }} + run: | + set -euo pipefail + if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo '::error::Expected a semantic version tag beginning with v' + exit 1 + fi + version="${RELEASE_TAG#v}" + gh release download "${RELEASE_TAG}" \ + --pattern "Starlight_Launcher_${version}_skin-site-update.zip" \ + --dir release-check + + - name: Publish to skin site + env: + RELEASE_TAG: ${{ inputs.tag }} + SKIN_SITE_TOKEN: ${{ secrets.SKIN_SITE_CI_UPLOAD_TOKEN }} + SKIN_SITE_URL: ${{ vars.SKIN_SITE_URL || 'https://skin.starlight.cool' }} + run: | + set -euo pipefail + version="${RELEASE_TAG#v}" + node scripts/axolotl/publish-skin-site.mjs \ + "release-check/Starlight_Launcher_${version}_skin-site-update.zip" "${RELEASE_TAG}" diff --git a/scripts/axolotl/publish-skin-site.mjs b/scripts/axolotl/publish-skin-site.mjs new file mode 100644 index 0000000..7287bfa --- /dev/null +++ b/scripts/axolotl/publish-skin-site.mjs @@ -0,0 +1,226 @@ +import { randomUUID } from 'node:crypto' +import { open, stat } from 'node:fs/promises' +import path from 'node:path' +import { pathToFileURL } from 'node:url' + +export async function publishSkinSite({ + archivePath, + tag, + token, + baseUrl = 'https://skin.starlight.cool', + fetchImpl = fetch, + sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), + now = Date.now, + timeoutMs = 30 * 60 * 1000, + pollIntervalMs = 2000, + log = console.log, +}) { + if (!token) throw new Error('SKIN_SITE_CI_UPLOAD_TOKEN is required to publish to the skin site') + if (!archivePath || !/^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(tag ?? '')) { + throw new Error('Expected a complete update ZIP and a semantic version tag') + } + const version = tag.replace(/^v/, '') + const url = new URL(baseUrl) + if (url.protocol !== 'https:' || url.username || url.password) { + throw new Error('SKIN_SITE_URL must be an HTTPS URL without credentials') + } + const { size } = await stat(archivePath) + if (!archivePath.toLowerCase().endsWith('.zip') || size < 1 || size > 4 * 1024 ** 3) { + throw new Error('Expected a nonempty complete update ZIP no larger than 4 GiB') + } + const deadline = now() + timeoutMs + function remainingTime() { + const remaining = deadline - now() + if (remaining <= 0) throw new Error('Skin site publication timed out') + return remaining + } + async function request(route, { method = 'GET', body, chunk = false } = {}) { + const attempts = chunk ? 3 : 1 + for (let attempt = 0; attempt < attempts; attempt++) { + const requestTimeout = Math.min(120_000, remainingTime()) + let response + try { + response = await fetchImpl(`${baseUrl.replace(/\/$/, '')}/starlight/${route}`, { + method, + headers: { + Authorization: `Bearer ${token}`, + ...(body && !chunk ? { 'Content-Type': 'application/json' } : {}), + }, + body: body && !chunk ? JSON.stringify(body) : body, + signal: AbortSignal.timeout(requestTimeout), + redirect: 'error', + }) + } catch { + if (attempt + 1 === attempts) + throw new Error(`Skin site ${route}: network request failed or timed out`) + await sleep(500 * (attempt + 1)) + continue + } + if (!response.ok) { + await response.body?.cancel() + if ((response.status === 429 || response.status >= 500) && attempt + 1 < attempts) { + await sleep(500 * (attempt + 1)) + continue + } + throw new Error(`Skin site ${route}: HTTP ${response.status}`) + } + let result + try { + result = await response.json() + } catch { + throw new Error(`Skin site ${route}: invalid JSON response`) + } + if (result?.success !== true || !result.payload) { + throw new Error( + `Skin site ${route}: ${result?.errorMessage || 'request was rejected or payload is missing'}`, + ) + } + return result.payload + } + } + + let status = await request('launcher/admin/uploads', { + method: 'POST', + body: { + uploadId: randomUUID(), + filename: path.basename(archivePath), + size, + forceUpdate: false, + }, + }) + if (typeof status.id !== 'string' || !status.id) + throw new Error('Skin site returned an invalid upload ID') + const uploadRoute = `launcher/admin/uploads/${encodeURIComponent(status.id)}` + log(`Skin site upload ${status.id}: ${size} bytes`) + const file = await open(archivePath, 'r') + try { + for (;;) { + remainingTime() + if (status.state === 'ready') { + const release = status.release + if (!Number.isSafeInteger(release?.id) || release.id <= 0 || release.version !== version) { + throw new Error( + `Skin site returned an invalid release or a version other than ${version}`, + ) + } + const activated = await request(`launcher/admin/${release.id}/activate`, { method: 'POST' }) + if ( + activated.id !== release.id || + activated.version !== version || + activated.active !== true + ) { + throw new Error(`Skin site did not confirm activation of ${version}`) + } + log(`Skin site release ${version} published (id ${release.id})`) + return activated + } + if (status.state === 'failed' || status.state === 'cancelled') { + throw new Error( + `Skin site archive processing ${status.state}: ${status.errorMessage || 'no error details'}`, + ) + } + if (status.state === 'processing') { + await sleep(Math.min(pollIntervalMs, remainingTime())) + status = await request(uploadRoute) + continue + } + if ( + status.state !== 'uploading' || + status.size !== size || + !Number.isSafeInteger(status.chunkSize) || + status.chunkSize < 1 || + status.chunkSize > 64 * 1024 ** 2 || + !Number.isSafeInteger(status.received) || + status.received < 0 || + status.received > size || + !Array.isArray(status.receivedChunks) + ) { + throw new Error('Skin site returned invalid upload status or chunk size') + } + const count = Math.ceil(size / status.chunkSize) + if ( + status.receivedChunks.some( + (index) => !Number.isSafeInteger(index) || index < 0 || index >= count, + ) + ) { + throw new Error('Skin site returned invalid received chunk indices') + } + const received = new Set(status.receivedChunks) + const missing = Array.from({ length: count }, (_, index) => index).filter( + (index) => !received.has(index), + ) + if (!missing.length) { + if (status.received !== size) + throw new Error('Skin site received byte count does not match the archive') + status = await request(`${uploadRoute}/complete`, { method: 'POST' }) + continue + } + const chunkSize = status.chunkSize + const concurrency = Math.min( + Number.isSafeInteger(status.uploadConcurrency) && status.uploadConcurrency > 0 + ? status.uploadConcurrency + : 1, + 6, + missing.length, + ) + let cursor = 0 + let uploadError + async function worker() { + try { + while (!uploadError && cursor < missing.length) { + remainingTime() + const index = missing[cursor++] + const start = index * chunkSize + const bytes = Buffer.alloc(Math.min(chunkSize, size - start)) + let offset = 0 + while (offset < bytes.length) { + const { bytesRead } = await file.read( + bytes, + offset, + bytes.length - offset, + start + offset, + ) + if (!bytesRead) throw new Error('Update archive changed or ended during upload') + offset += bytesRead + } + const form = new FormData() + form.append('file', new Blob([bytes]), 'chunk.bin') + await request(`${uploadRoute}/chunks/${index}`, { + method: 'POST', + body: form, + chunk: true, + }) + } + } catch (error) { + uploadError ??= error + } + } + await Promise.all(Array.from({ length: concurrency }, worker)) + if (uploadError) throw uploadError + status = await request(uploadRoute) + log(`Skin site uploaded ${status.received}/${size} bytes`) + } + } finally { + await file.close() + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { + try { + await publishSkinSite({ + archivePath: process.argv[2], + tag: process.argv[3], + token: process.env.SKIN_SITE_TOKEN, + baseUrl: process.env.SKIN_SITE_URL || 'https://skin.starlight.cool', + }) + } catch (error) { + const message = String(error.message).replaceAll( + process.env.SKIN_SITE_TOKEN || '\0', + '[redacted]', + ) + console.error( + `::error::${message.replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A')}`, + ) + process.exitCode = 1 + } +} diff --git a/scripts/axolotl/publish-skin-site.test.mjs b/scripts/axolotl/publish-skin-site.test.mjs new file mode 100644 index 0000000..9f51f63 --- /dev/null +++ b/scripts/axolotl/publish-skin-site.test.mjs @@ -0,0 +1,172 @@ +import assert from 'node:assert/strict' +import { mkdtemp, writeFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import test from 'node:test' +import { spawnSync } from 'node:child_process' +import { publishSkinSite } from './publish-skin-site.mjs' + +async function fixture(t) { + const directory = await mkdtemp(path.join(tmpdir(), 'skin-site-publish-')) + t.after(() => rm(directory, { recursive: true, force: true })) + const archivePath = path.join(directory, 'Starlight_Launcher_1.0.2-beta10_skin-site-update.zip') + const bytes = Buffer.from('a small archive split into several chunks') + await writeFile(archivePath, bytes) + return { archivePath, bytes } +} + +function apiFixture(bytes, options = {}) { + const chunks = new Map() + const events = [] + const release = { id: 17, version: '1.0.2-beta10', active: false } + let state = 'uploading' + let failedOnce = false + if (options.resume) chunks.set(0, bytes.subarray(0, 8)) + const status = () => ({ + id: 'session-1', + chunkSize: 8, + size: bytes.length, + received: [...chunks.values()].reduce((total, part) => total + part.length, 0), + receivedChunks: [...chunks.keys()], + uploadConcurrency: 3, + state, + ...(state === 'ready' ? { release } : {}), + ...(state === 'failed' ? { errorMessage: 'Invalid archive manifest' } : {}), + }) + const reply = (payload) => Response.json({ success: true, payload }) + return { + chunks, + events, + fetch: async (url, init) => { + assert.equal(init.headers.Authorization, 'Bearer test-token') + const route = new URL(url).pathname.replace('/starlight/', '') + events.push(route) + if (route === 'launcher/admin/uploads') { + assert.equal(JSON.parse(init.body).size, bytes.length) + assert.equal(JSON.parse(init.body).forceUpdate, false) + return reply({ ...status(), ...(options.invalidChunkSize ? { chunkSize: 0 } : {}) }) + } + if (route.includes('/chunks/')) { + if (options.rejectChunks) return new Response('too large', { status: 413 }) + if (options.retryChunk && !failedOnce) { + failedOnce = true + return new Response('temporary failure', { status: 503 }) + } + const index = Number(route.split('/').at(-1)) + const file = init.body.get('file') + assert.equal(file.name, 'chunk.bin') + chunks.set(index, Buffer.from(await file.arrayBuffer())) + return reply(status()) + } + if (route.endsWith('/complete')) { + assert.deepEqual( + Buffer.concat([...chunks].sort(([a], [b]) => a - b).map(([, part]) => part)), + bytes, + ) + state = 'processing' + return reply(status()) + } + if (route.endsWith('/activate')) { + assert.equal(state, 'ready') + return options.activationFailure + ? Response.json({ success: false, errorMessage: 'Activation denied' }) + : reply({ ...release, active: true }) + } + if (route === 'launcher/admin/uploads/session-1') { + if (state === 'processing' && !options.stuck) + state = options.processingFailure ? 'failed' : 'ready' + return reply({ + ...status(), + ...(options.wrongVersion && state === 'ready' + ? { release: { ...release, version: '1.0.2-beta9' } } + : {}), + }) + } + throw new Error(`Unexpected route ${route}`) + }, + } +} + +function settings(archivePath, api) { + return { + archivePath, + tag: 'v1.0.2-beta10', + token: 'test-token', + baseUrl: 'https://skin.example', + fetchImpl: api.fetch, + pollIntervalMs: 1, + sleep: async () => {}, + log: () => {}, + } +} + +test('uploads exact bounded chunks, retries temporary errors, then completes and activates', async (t) => { + const { archivePath, bytes } = await fixture(t) + const api = apiFixture(bytes, { retryChunk: true }) + const result = await publishSkinSite(settings(archivePath, api)) + assert.equal(result.active, true) + assert.equal(api.chunks.size, Math.ceil(bytes.length / 8)) + assert.ok([...api.chunks.values()].every((part) => part.length <= 8)) + assert.equal(api.events.at(-1), 'launcher/admin/17/activate') +}) + +for (const [name, options, error] of [ + ['HTTP upload rejection', { rejectChunks: true }, /413/], + ['archive processing failure', { processingFailure: true }, /Invalid archive manifest/], + ['application-level activation failure', { activationFailure: true }, /Activation denied/], + ['invalid server chunk size', { invalidChunkSize: true }, /invalid upload status/], + ['mismatched release version', { wrongVersion: true }, /version other than/], +]) { + test(`fails explicitly on ${name}`, async (t) => { + const { archivePath, bytes } = await fixture(t) + const api = apiFixture(bytes, options) + await assert.rejects(publishSkinSite(settings(archivePath, api)), error) + if (!options.activationFailure) + assert.ok(!api.events.some((event) => event.endsWith('/activate'))) + }) +} + +test('fails instead of hanging when processing never finishes', async (t) => { + const { archivePath, bytes } = await fixture(t) + const api = apiFixture(bytes, { stuck: true }) + let now = 0 + await assert.rejects( + publishSkinSite({ + ...settings(archivePath, api), + timeoutMs: 10, + now: () => now, + sleep: async () => { + now += 5 + }, + }), + /timed out/i, + ) +}) + +test('missing credentials fail before any network request', async (t) => { + const { archivePath } = await fixture(t) + await assert.rejects( + publishSkinSite({ + ...settings(archivePath, { fetch: () => assert.fail('Unexpected network request') }), + token: '', + }), + /SKIN_SITE_CI_UPLOAD_TOKEN/, + ) +}) + +test('resumes server-acknowledged chunks without uploading them again', async (t) => { + const { archivePath, bytes } = await fixture(t) + const api = apiFixture(bytes, { resume: true }) + await publishSkinSite(settings(archivePath, api)) + assert.ok(!api.events.some((event) => event.endsWith('/chunks/0'))) +}) + +test('CLI emits a GitHub error and exits nonzero when credentials are missing', () => { + const result = spawnSync( + process.execPath, + ['scripts/axolotl/publish-skin-site.mjs', 'missing.zip', 'v1.0.2-beta10'], + { encoding: 'utf8', env: { ...process.env, SKIN_SITE_TOKEN: '' } }, + ) + assert.equal(result.status, 1) + assert.match(result.stderr, /::error::.*SKIN_SITE_CI_UPLOAD_TOKEN/) +}) From f81a588dc171eee459aea6dbffd03a1b31ab44d0 Mon Sep 17 00:00:00 2001 From: Disy Date: Tue, 20 Oct 2026 19:17:07 +0800 Subject: [PATCH 06/11] fix(ci): include skin site API error details in failures --- scripts/axolotl/publish-skin-site.mjs | 6 ++++-- scripts/axolotl/publish-skin-site.test.mjs | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/scripts/axolotl/publish-skin-site.mjs b/scripts/axolotl/publish-skin-site.mjs index 7287bfa..3283fa9 100644 --- a/scripts/axolotl/publish-skin-site.mjs +++ b/scripts/axolotl/publish-skin-site.mjs @@ -57,12 +57,14 @@ export async function publishSkinSite({ continue } if (!response.ok) { - await response.body?.cancel() + const details = await response.json().catch(() => null) if ((response.status === 429 || response.status >= 500) && attempt + 1 < attempts) { await sleep(500 * (attempt + 1)) continue } - throw new Error(`Skin site ${route}: HTTP ${response.status}`) + throw new Error( + `Skin site ${route}: HTTP ${response.status}${typeof details?.errorMessage === 'string' ? ` - ${details.errorMessage.slice(0, 1000)}` : ''}`, + ) } let result try { diff --git a/scripts/axolotl/publish-skin-site.test.mjs b/scripts/axolotl/publish-skin-site.test.mjs index 9f51f63..269cf26 100644 --- a/scripts/axolotl/publish-skin-site.test.mjs +++ b/scripts/axolotl/publish-skin-site.test.mjs @@ -170,3 +170,16 @@ test('CLI emits a GitHub error and exits nonzero when credentials are missing', assert.equal(result.status, 1) assert.match(result.stderr, /::error::.*SKIN_SITE_CI_UPLOAD_TOKEN/) }) + +test('reports structured server errors when creating the upload session fails', async (t) => { + const { archivePath } = await fixture(t) + await assert.rejects( + publishSkinSite( + settings(archivePath, { + fetch: async () => + Response.json({ errorMessage: 'Upload storage unavailable' }, { status: 500 }), + }), + ), + /HTTP 500 - Upload storage unavailable/, + ) +}) From 6edc44fe265442d940129603cf197a08249413ac Mon Sep 17 00:00:00 2001 From: Disy Date: Tue, 20 Oct 2026 19:19:36 +0800 Subject: [PATCH 07/11] fix(ci): match skin site response envelopes --- scripts/axolotl/publish-skin-site.mjs | 3 ++- scripts/axolotl/publish-skin-site.test.mjs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/axolotl/publish-skin-site.mjs b/scripts/axolotl/publish-skin-site.mjs index 3283fa9..f02c568 100644 --- a/scripts/axolotl/publish-skin-site.mjs +++ b/scripts/axolotl/publish-skin-site.mjs @@ -43,6 +43,7 @@ export async function publishSkinSite({ response = await fetchImpl(`${baseUrl.replace(/\/$/, '')}/starlight/${route}`, { method, headers: { + Accept: 'application/json', Authorization: `Bearer ${token}`, ...(body && !chunk ? { 'Content-Type': 'application/json' } : {}), }, @@ -72,7 +73,7 @@ export async function publishSkinSite({ } catch { throw new Error(`Skin site ${route}: invalid JSON response`) } - if (result?.success !== true || !result.payload) { + if (result?.success === false || result?.error || !result?.payload) { throw new Error( `Skin site ${route}: ${result?.errorMessage || 'request was rejected or payload is missing'}`, ) diff --git a/scripts/axolotl/publish-skin-site.test.mjs b/scripts/axolotl/publish-skin-site.test.mjs index 269cf26..83183ec 100644 --- a/scripts/axolotl/publish-skin-site.test.mjs +++ b/scripts/axolotl/publish-skin-site.test.mjs @@ -33,7 +33,8 @@ function apiFixture(bytes, options = {}) { ...(state === 'ready' ? { release } : {}), ...(state === 'failed' ? { errorMessage: 'Invalid archive manifest' } : {}), }) - const reply = (payload) => Response.json({ success: true, payload }) + // The frontend derives success from HTTP status; the wire envelope only needs payload. + const reply = (payload) => Response.json({ payload }) return { chunks, events, From 2d7683b0bbfa982357ca0615056788059bb85e27 Mon Sep 17 00:00:00 2001 From: Disy Date: Tue, 20 Oct 2026 19:45:32 +0800 Subject: [PATCH 08/11] ci: add credential-safe skin site authentication diagnosis --- .github/workflows/check-skin-site-auth.yml | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/check-skin-site-auth.yml diff --git a/.github/workflows/check-skin-site-auth.yml b/.github/workflows/check-skin-site-auth.yml new file mode 100644 index 0000000..6928ecf --- /dev/null +++ b/.github/workflows/check-skin-site-auth.yml @@ -0,0 +1,49 @@ +name: Diagnose skin site CI authentication + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + diagnose: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Inspect token format without revealing credentials + env: + SKIN_SITE_TOKEN: ${{ secrets.SKIN_SITE_CI_UPLOAD_TOKEN }} + SKIN_SITE_URL: ${{ vars.SKIN_SITE_URL || 'https://skin.starlight.cool' }} + run: | + node --input-type=module <<'NODE' + const token = process.env.SKIN_SITE_TOKEN || '' + const checks = { + configured: token.length > 0, + containsWhitespace: /\s/.test(token), + containsComma: token.includes(','), + containsQuotes: /["']/.test(token), + containsBearerPrefix: /^Bearer\s/i.test(token), + bearerTokenFormatValid: /^[A-Za-z0-9._~+/-]+=*$/.test(token), + } + console.log(JSON.stringify(checks)) + if (!checks.bearerTokenFormatValid) { + console.error('::error::CI secret is not a valid Bearer token. No credential value has been logged.') + process.exitCode = 1 + } else { + const base = process.env.SKIN_SITE_URL.replace(/\/$/, '') + if (new URL(base).protocol !== 'https:') throw new Error('HTTPS is required') + try { + const response = await fetch(base + '/starlight/launcher/admin', { + headers: { Authorization: 'Bearer ' + token, Accept: 'application/json' }, + redirect: 'error', signal: AbortSignal.timeout(30000), + }) + console.log('Read-only CI authentication HTTP status: ' + response.status) + await response.body?.cancel() + if (!response.ok) process.exitCode = 1 + } catch { + console.error('::error::Read-only CI authentication request failed; credentials have not been logged.') + process.exitCode = 1 + } + } + NODE From 7f3a1cdf5c2f89da9d5587d0d1da18c5fd215db9 Mon Sep 17 00:00:00 2001 From: Disy Date: Tue, 20 Oct 2026 20:28:13 +0800 Subject: [PATCH 09/11] fix: localize skin site UI and renew JWT on startup Catalog missing skin-site messages and check source IDs during i18n validation. Renew the saved JWT before loading session data, guard overlapping login/logout requests, and reload announcements after authentication with a retry action. --- .../ui/skin-site/ChatUserPicker.vue | 37 +- .../skin-site/SkinSiteAnnouncements.test.ts | 142 + .../ui/skin-site/SkinSiteAnnouncements.vue | 47 +- .../ui/skin-site/admin/AdminWikiPage.vue | 252 +- .../src/composables/skin-site-session.test.ts | 208 + .../src/composables/skin-site-session.ts | 83 +- .../app-frontend/src/locales/en-US/index.json | 2389 +++++++---- .../app-frontend/src/locales/zh-CN/index.json | 3815 +++++++++++------ scripts/axolotl/i18n-check.mjs | 17 + scripts/axolotl/skin-site-i18n.test.mjs | 36 + scripts/axolotl/skin-site-messages.mjs | 59 + 11 files changed, 4744 insertions(+), 2341 deletions(-) create mode 100644 apps/app-frontend/src/components/ui/skin-site/SkinSiteAnnouncements.test.ts create mode 100644 apps/app-frontend/src/composables/skin-site-session.test.ts create mode 100644 scripts/axolotl/skin-site-i18n.test.mjs create mode 100644 scripts/axolotl/skin-site-messages.mjs diff --git a/apps/app-frontend/src/components/ui/skin-site/ChatUserPicker.vue b/apps/app-frontend/src/components/ui/skin-site/ChatUserPicker.vue index ad24d06..bff3353 100644 --- a/apps/app-frontend/src/components/ui/skin-site/ChatUserPicker.vue +++ b/apps/app-frontend/src/components/ui/skin-site/ChatUserPicker.vue @@ -70,6 +70,13 @@ const messages = defineMessages({ defaultMessage: 'Selected', }, idLabel: { id: 'app.skin-site.chat.user-picker.id', defaultMessage: 'ID' }, + emailLabel: { id: 'app.skin-site.chat.user-picker.email', defaultMessage: 'Email' }, + registeredLabel: { + id: 'app.skin-site.chat.user-picker.registered', + defaultMessage: 'Registered', + }, + bannedStatus: { id: 'app.skin-site.chat.user-picker.banned-status', defaultMessage: 'Banned' }, + activeStatus: { id: 'app.skin-site.chat.user-picker.active-status', defaultMessage: 'Active' }, }) const keyword = ref('') @@ -82,9 +89,7 @@ let timer: ReturnType | undefined let requestId = 0 const currentLabel = computed(() => props.label ?? formatMessage(messages.label)) -const currentPlaceholder = computed( - () => props.placeholder ?? formatMessage(messages.placeholder), -) +const currentPlaceholder = computed(() => props.placeholder ?? formatMessage(messages.placeholder)) watch([keyword, searchMode], () => { clearTimeout(timer) @@ -174,13 +179,19 @@ function formatTime(ts: number): string { v-model.trim="keyword" type="text" maxlength="100" - :placeholder="searchMode === 'player' ? formatMessage(messages.playerHint) : currentPlaceholder" + :placeholder=" + searchMode === 'player' ? formatMessage(messages.playerHint) : currentPlaceholder + " class="min-w-0 flex-1 rounded-lg border border-solid border-surface-5 bg-bg-raised px-3 py-1.5 text-xs text-contrast outline-none focus:border-brand" autocomplete="off" /> - {{ searchMode === 'player' ? formatMessage(messages.playerHint) : formatMessage(messages.hint) }} + {{ + searchMode === 'player' + ? formatMessage(messages.playerHint) + : formatMessage(messages.hint) + }} @@ -201,7 +212,9 @@ function formatTime(ts: number): string { @click="preview = user" > - + {{ user.username.slice(0, 1).toUpperCase() }} @@ -244,7 +257,7 @@ function formatTime(ts: number): string { class="rounded-full px-2 py-0.5 text-[10px] font-semibold" :class="preview.banned ? 'bg-red text-contrast' : 'bg-green text-contrast'" > - {{ preview.banned ? 'Banned' : 'OK' }} + {{ formatMessage(preview.banned ? messages.bannedStatus : messages.activeStatus) }}
@@ -253,7 +266,7 @@ function formatTime(ts: number): string { {{ preview.uuid }}
- Email + {{ formatMessage(messages.emailLabel) }} {{ preview.email }}
@@ -261,7 +274,7 @@ function formatTime(ts: number): string { {{ preview.qq ?? '—' }}
- Registered + {{ formatMessage(messages.registeredLabel) }} {{ formatTime(preview.registerTime) }}
@@ -283,7 +296,11 @@ function formatTime(ts: number): string { :disabled="Boolean(unavailableReason(preview))" @click="selectPreview" > - {{ model?.uuid === preview.uuid ? formatMessage(messages.selected) : formatMessage(messages.select) }} + {{ + model?.uuid === preview.uuid + ? formatMessage(messages.selected) + : formatMessage(messages.select) + }} diff --git a/apps/app-frontend/src/components/ui/skin-site/SkinSiteAnnouncements.test.ts b/apps/app-frontend/src/components/ui/skin-site/SkinSiteAnnouncements.test.ts new file mode 100644 index 0000000..f98422d --- /dev/null +++ b/apps/app-frontend/src/components/ui/skin-site/SkinSiteAnnouncements.test.ts @@ -0,0 +1,142 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { createRequire, registerHooks, stripTypeScriptTypes } from 'node:module' +import test from 'node:test' + +import { createRenderer, nextTick, ref } from 'vue' + +const testSession = { + skinSiteStatus: ref('checking'), + skinSiteUser: ref<{ uuid: string } | null>(null), +} +const testGlobal = globalThis as unknown as { + skinSiteAnnouncementSession: typeof testSession + skinSiteAnnouncementFetch: () => Promise +} +testGlobal.skinSiteAnnouncementSession = testSession +const require = createRequire(import.meta.url) +const { parse, compileScript } = require('vue/compiler-sfc') +const componentUrl = new URL('./SkinSiteAnnouncements.vue', import.meta.url).href +const stubs: Record = { + '@modrinth/assets': 'export const SpinnerIcon={}', + '@modrinth/ui': + 'export const commonMessages={retryButton:{defaultMessage:"Retry"}}; export const defineMessages=x=>x; export const useVIntl=()=>({formatMessage:x=>x.defaultMessage})', + '@/helpers/skin-site-api': + 'export const skinSiteAnnouncements=()=>globalThis.skinSiteAnnouncementFetch()', + '@/composables/skin-site-session': + 'export const {skinSiteStatus,skinSiteUser}=globalThis.skinSiteAnnouncementSession; export const reloadSkinSiteSession=async()=>{}', +} +const hooks = registerHooks({ + resolve(specifier, context, nextResolve) { + if (specifier in stubs) + return { + url: `data:text/javascript,${encodeURIComponent(stubs[specifier])}`, + shortCircuit: true, + } + return nextResolve(specifier, context) + }, + load(url, context, nextLoad) { + if (url !== componentUrl) return nextLoad(url, context) + const { descriptor } = parse(readFileSync(new URL(url), 'utf8')) + return { + format: 'module', + source: stripTypeScriptTypes( + compileScript(descriptor, { id: 'skin-announcement-test' }).content, + ), + shortCircuit: true, + } + }, +}) +const { default: Announcements } = await import(componentUrl) +hooks.deregister() +Announcements.render = () => null + +type Node = Record +const renderer = createRenderer({ + createElement: () => ({}), + createText: () => ({}), + createComment: () => ({}), + insert() {}, + remove() {}, + setText() {}, + setElementText() {}, + patchProp() {}, + parentNode: () => null, + nextSibling: () => null, +}) +const flush = async () => { + await new Promise((resolve) => setImmediate(resolve)) + await nextTick() +} +type State = { + announcements: Array<{ id: number }> + loading: boolean + failed: boolean + load: () => Promise +} +let requests = 0 +function mount(t: test.TestContext) { + const app = renderer.createApp(Announcements) + const instance = app.mount({}) + t.after(() => app.unmount()) + return instance.$.setupState as State +} +test.beforeEach(() => { + testSession.skinSiteStatus.value = 'checking' + testSession.skinSiteUser.value = null + requests = 0 + testGlobal.skinSiteAnnouncementFetch = async () => { + requests++ + return { success: true, payload: [{ id: 1, title: '公告', updateAt: 1, star: false }] } + } +}) + +test('announcements wait for startup renewal and reload when sign-in finishes', async (t) => { + const state = mount(t) + await flush() + assert.equal(requests, 0) + assert.equal(state.loading, true) + testSession.skinSiteUser.value = { uuid: 'owner' } + testSession.skinSiteStatus.value = 'signed-in' + await flush() + assert.equal(requests, 1) + assert.equal(state.announcements.length, 1) +}) + +test('signed-out users do not request protected announcements or see a load failure', async (t) => { + testSession.skinSiteStatus.value = 'signed-out' + const state = mount(t) + await flush() + assert.equal(requests, 0) + assert.equal(state.loading, false) + assert.equal(state.failed, false) +}) + +test('signing out clears announcements and ignores an old in-flight response', async (t) => { + testSession.skinSiteStatus.value = 'signed-in' + let finish!: (value: unknown) => void + testGlobal.skinSiteAnnouncementFetch = () => + new Promise((resolve) => { + finish = resolve + }) + const state = mount(t) + await flush() + testSession.skinSiteStatus.value = 'signed-out' + await flush() + finish({ success: true, payload: [{ id: 2 }] }) + await flush() + assert.deepEqual(state.announcements, []) + assert.equal(state.failed, false) +}) + +test('a failed announcement request can be retried', async (t) => { + testSession.skinSiteStatus.value = 'signed-in' + testGlobal.skinSiteAnnouncementFetch = async () => ({ success: false }) + const state = mount(t) + await flush() + assert.equal(state.failed, true) + testGlobal.skinSiteAnnouncementFetch = async () => ({ success: true, payload: [{ id: 3 }] }) + await state.load() + assert.equal(state.failed, false) + assert.equal(state.announcements[0].id, 3) +}) diff --git a/apps/app-frontend/src/components/ui/skin-site/SkinSiteAnnouncements.vue b/apps/app-frontend/src/components/ui/skin-site/SkinSiteAnnouncements.vue index 93ac47b..b5acbe0 100644 --- a/apps/app-frontend/src/components/ui/skin-site/SkinSiteAnnouncements.vue +++ b/apps/app-frontend/src/components/ui/skin-site/SkinSiteAnnouncements.vue @@ -1,8 +1,13 @@ @@ -63,8 +90,14 @@ onMounted(async () => { {{ formatMessage(messages.loading) }} -

- {{ formatMessage(messages.failed) }} +

+

{{ formatMessage(messages.failed) }}

+ +
+

+ {{ formatMessage(messages.signedOut) }}

{{ formatMessage(messages.empty) }} diff --git a/apps/app-frontend/src/components/ui/skin-site/admin/AdminWikiPage.vue b/apps/app-frontend/src/components/ui/skin-site/admin/AdminWikiPage.vue index f0bd173..a7fcaf7 100644 --- a/apps/app-frontend/src/components/ui/skin-site/admin/AdminWikiPage.vue +++ b/apps/app-frontend/src/components/ui/skin-site/admin/AdminWikiPage.vue @@ -7,6 +7,16 @@ import { skinSiteWikiAudit, type SkinSiteWikiAuditInfo, type SkinSiteWikiEditPolicy, + type SkinSiteWikiMigrationCandidate, + skinSiteWikiMigrationCandidates, + skinSiteWikiMigrationDecide, + skinSiteWikiMigrationDelete, + type SkinSiteWikiMigrationJobInfo, + skinSiteWikiMigrationJobs, + skinSiteWikiMigrationPublish, + skinSiteWikiMigrationRollback, + skinSiteWikiMigrationStartAutomatic, + skinSiteWikiMigrationStartXml, skinSiteWikiPage, type SkinSiteWikiPageDetail, skinSiteWikiProtectPage, @@ -16,22 +26,18 @@ import { type SkinSiteWikiSettings, skinSiteWikiSettings, skinSiteWikiUpdateSettings, - skinSiteWikiMigrationCandidates, - skinSiteWikiMigrationDecide, - skinSiteWikiMigrationDelete, - skinSiteWikiMigrationJobs, - skinSiteWikiMigrationPublish, - skinSiteWikiMigrationRollback, - skinSiteWikiMigrationStartAutomatic, - skinSiteWikiMigrationStartXml, - type SkinSiteWikiMigrationCandidate, - type SkinSiteWikiMigrationJobInfo, } from '@/helpers/skin-site-api' const { formatMessage } = useVIntl() const { handleError, addNotification } = injectNotificationManager() const messages = defineMessages({ + policyPublic: { id: 'app.skin-site.admin.wiki.policy-public', defaultMessage: 'Everyone' }, + policyMember: { id: 'app.skin-site.admin.wiki.policy-member', defaultMessage: 'Members' }, + policyAdmin: { + id: 'app.skin-site.admin.wiki.policy-admin', + defaultMessage: 'Website administrators', + }, title: { id: 'app.skin-site.admin.wiki.title', defaultMessage: 'Wiki' }, tabPage: { id: 'app.skin-site.admin.wiki.tab-page', defaultMessage: 'Page' }, tabAudit: { id: 'app.skin-site.admin.wiki.tab-audit', defaultMessage: 'Audit log' }, @@ -61,7 +67,8 @@ const messages = defineMessages({ }, credentialsHint: { id: 'app.skin-site.admin.wiki.credentials-hint', - defaultMessage: 'Credentials stay in memory for this job only. HTTPS is required when supplied.', + defaultMessage: + 'Credentials stay in memory for this job only. HTTPS is required when supplied.', }, allowPrivate: { id: 'app.skin-site.admin.wiki.allow-private', @@ -529,8 +536,7 @@ async function loadMigrationJobs() { if (!res.success) throw new Error(res.errorMessage) migrationJobs.value = res.payload ?? [] if (selectedJob.value) { - selectedJob.value = - migrationJobs.value.find((j) => j.id === selectedJob.value!.id) ?? null + selectedJob.value = migrationJobs.value.find((j) => j.id === selectedJob.value!.id) ?? null } } catch (error) { handleError(error) @@ -821,8 +827,8 @@ onMounted(() => void loadSettings()) v-model="protectRead" class="rounded-lg border border-solid border-surface-5 bg-bg px-2 py-1 text-xs text-contrast" > - - + + @@ -1075,9 +1117,19 @@ onMounted(() => void loadSettings()) {{ formatMessage(messages.xmlFile) }} - + - @@ -1087,16 +1139,27 @@ onMounted(() => void loadSettings())

{{ formatMessage(messages.jobs) }}

- -
+
{{ formatMessage(messages.loading) }}
-

+

{{ formatMessage(messages.jobsEmpty) }}

@@ -1107,21 +1170,43 @@ onMounted(() => void loadSettings()) :class="selectedJob?.id === job.id ? 'border-brand bg-brand/5' : 'border-surface-5'" >
- #{{ job.id }} · {{ job.source }} - + #{{ job.id }} · {{ job.source }} + {{ jobStateLabel(job.state) }}
-
+
- {{ formatMessage(messages.reviewedCount, { reviewed: job.reviewed, total: job.total }) }} - {{ formatMessage(messages.importedExcluded, { imported: job.imported, excluded: job.excluded }) }} + {{ + formatMessage(messages.reviewedCount, { + reviewed: job.reviewed, + total: job.total, + }) + }} + {{ + formatMessage(messages.importedExcluded, { + imported: job.imported, + excluded: job.excluded, + }) + }}

{{ job.error }}

- -
@@ -1151,7 +1245,10 @@ onMounted(() => void loadSettings())
-
+

{{ formatMessage(messages.candidates) }} @@ -1180,20 +1277,35 @@ onMounted(() => void loadSettings())

{{ formatMessage(messages.confirmRollback) }}

- -
-
+
{{ formatMessage(messages.loading) }}
-

+

{{ formatMessage(messages.candidatesEmpty) }}

@@ -1203,20 +1315,38 @@ onMounted(() => void loadSettings()) class="flex flex-col gap-2 rounded-lg border border-solid border-surface-5 p-3" >
- {{ candidate.title }} - + {{ + candidate.title + }} + {{ decisionLabel(candidate.decision) }} - + {{ formatMessage(messages.conflict) }}
- {{ formatMessage(messages.lastUpdated, { date: new Date(candidate.lastUpdatedAt).toLocaleDateString() }) }} - {{ formatMessage(messages.maintainerCount, { count: candidate.maintainerCount }) }} + {{ + formatMessage(messages.lastUpdated, { + date: new Date(candidate.lastUpdatedAt).toLocaleDateString(), + }) + }} + {{ + formatMessage(messages.maintainerCount, { count: candidate.maintainerCount }) + }}
- + {{ reason }}
@@ -1228,13 +1358,25 @@ onMounted(() => void loadSettings()) class="min-w-0 flex-1 rounded-lg border border-solid border-surface-5 bg-bg-raised px-2 py-1 text-xs text-contrast outline-none focus:border-brand" /> - -
@@ -1251,8 +1393,6 @@ onMounted(() => void loadSettings())

- - - +
diff --git a/apps/app-frontend/src/composables/skin-site-session.test.ts b/apps/app-frontend/src/composables/skin-site-session.test.ts new file mode 100644 index 0000000..5b6980d --- /dev/null +++ b/apps/app-frontend/src/composables/skin-site-session.test.ts @@ -0,0 +1,208 @@ +import assert from 'node:assert/strict' +import { registerHooks } from 'node:module' +import test from 'node:test' + +type FetchHandler = (url: string, options: RequestInit) => Promise +const testGlobal = globalThis as unknown as { skinSiteTestFetch: FetchHandler } +const hooks = registerHooks({ + resolve(specifier, context, nextResolve) { + if (specifier === '@tauri-apps/plugin-http') { + return { + url: 'data:text/javascript,export const fetch=(...args)=>globalThis.skinSiteTestFetch(...args)', + shortCircuit: true, + } + } + if (specifier.startsWith('@/')) { + return nextResolve(new URL(`../${specifier.slice(2)}.ts`, import.meta.url).href, context) + } + return nextResolve(specifier, context) + }, +}) +const session = await import('./skin-site-session.ts') +hooks.deregister() + +const user = { uuid: 'owner', username: 'Player', permissions: [] } +const ok = (payload: unknown) => Response.json({ payload }) +const storage = new Map() +const tokenKey = 'starlight-skin-site-token' +let calls: Array<{ path: string; token: string | undefined }> = [] +let respond: FetchHandler + +test.beforeEach(() => { + storage.clear() + Object.defineProperty(globalThis, 'localStorage', { + configurable: true, + value: { + getItem: (key: string) => storage.get(key) ?? null, + setItem: (key: string, value: string) => storage.set(key, value), + removeItem: (key: string) => storage.delete(key), + }, + }) + calls = [] + respond = async (url) => { + if (url.endsWith('/user/refresh')) return ok({ username: 'Player', jwt: 'fresh-token' }) + if (url.endsWith('/user')) return ok(user) + return ok([]) + } + testGlobal.skinSiteTestFetch = async (url, options) => { + calls.push({ + path: new URL(url).pathname, + token: (options.headers as Record)?.Authorization, + }) + return respond(url, options) + } + session.resetSkinSiteSession() +}) + +test.afterEach(async () => { + session.signOutSkinSite() + await new Promise((resolve) => setImmediate(resolve)) + Reflect.deleteProperty(globalThis, 'localStorage') +}) + +test('startup renews the stored JWT before loading the user with the new token', async () => { + storage.set(tokenKey, 'old-token') + await session.initSkinSiteSession() + assert.deepEqual(calls.slice(0, 2), [ + { path: '/starlight/user/refresh', token: 'Bearer old-token' }, + { path: '/starlight/user', token: 'Bearer fresh-token' }, + ]) + assert.equal(storage.get(tokenKey), 'fresh-token') + assert.equal(session.skinSiteStatus.value, 'signed-in') +}) + +test('simultaneous startup consumers share one refresh', async () => { + storage.set(tokenKey, 'old-token') + await Promise.all([ + session.initSkinSiteSession(), + session.initSkinSiteSession(), + session.waitForSkinSiteSession(), + ]) + assert.equal(calls.filter((call) => call.path.endsWith('/refresh')).length, 1) +}) + +test('startup without a stored token does not make authenticated requests', async () => { + await session.initSkinSiteSession() + assert.equal(session.skinSiteStatus.value, 'signed-out') + assert.deepEqual(calls, []) +}) + +test('an expired token is cleared when renewal is rejected', async () => { + storage.set(tokenKey, 'expired-token') + respond = async () => Response.json({ errorMessage: 'Expired' }, { status: 401 }) + await session.initSkinSiteSession() + assert.equal(storage.has(tokenKey), false) + assert.equal(session.skinSiteStatus.value, 'signed-out') + assert.equal(calls.length, 1) +}) + +test('temporary renewal failure preserves the token and allows startup retry', async () => { + storage.set(tokenKey, 'old-token') + respond = async () => Response.json({ errorMessage: 'Unavailable' }, { status: 503 }) + await session.initSkinSiteSession() + assert.equal(session.skinSiteStatus.value, 'error') + assert.equal(storage.get(tokenKey), 'old-token') + respond = async (url) => + url.endsWith('/refresh') + ? ok({ jwt: 'fresh-token' }) + : url.endsWith('/user') + ? ok(user) + : ok([]) + await session.initSkinSiteSession() + assert.equal(session.skinSiteStatus.value, 'signed-in') +}) + +test('a late refresh cannot restore a session after sign-out', async () => { + storage.set(tokenKey, 'old-token') + let finish!: (response: Response) => void + respond = () => + new Promise((resolve) => { + finish = resolve + }) + const initializing = session.initSkinSiteSession() + await new Promise((resolve) => setImmediate(resolve)) + session.signOutSkinSite() + finish(ok({ username: 'Player', jwt: 'late-token' })) + await initializing + assert.equal(storage.has(tokenKey), false) + assert.equal(session.skinSiteStatus.value, 'signed-out') + assert.equal(session.skinSiteUser.value, null) +}) + +test('a malformed refresh response never replaces the saved token', async () => { + storage.set(tokenKey, 'old-token') + respond = async () => ok({ username: 'Player' }) + await session.initSkinSiteSession() + assert.equal(storage.get(tokenKey), 'old-token') + assert.equal(session.skinSiteStatus.value, 'error') +}) + +test('a failed sign-in attempt does not invalidate a pending startup renewal', async () => { + storage.set(tokenKey, 'old-token') + let finish!: (response: Response) => void + respond = async (url) => { + if (url.endsWith('/refresh')) + return new Promise((resolve) => { + finish = resolve + }) + if (url.endsWith('/login')) + return Response.json({ errorMessage: 'Invalid password' }, { status: 401 }) + return url.endsWith('/user') ? ok(user) : ok([]) + } + const initializing = session.initSkinSiteSession() + await new Promise((resolve) => setImmediate(resolve)) + await session.signInSkinSite({ username: 'Player', password: 'wrong', clientId: 'captcha' }) + finish(ok({ username: 'Player', jwt: 'fresh-token' })) + await initializing + assert.equal(session.skinSiteStatus.value, 'signed-in') + assert.equal(storage.get(tokenKey), 'fresh-token') +}) + +test('rejection of an old startup token does not discard a new successful login', async () => { + storage.set(tokenKey, 'expired-token') + let finishRefresh!: (response: Response) => void + let finishLogin!: (response: Response) => void + respond = async (url) => { + if (url.endsWith('/refresh')) + return new Promise((resolve) => { + finishRefresh = resolve + }) + if (url.endsWith('/login')) + return new Promise((resolve) => { + finishLogin = resolve + }) + return url.endsWith('/user') ? ok(user) : ok([]) + } + const initializing = session.initSkinSiteSession() + const signingIn = session.signInSkinSite({ + username: 'Player', + password: 'valid', + clientId: 'captcha', + }) + await new Promise((resolve) => setImmediate(resolve)) + finishRefresh(Response.json({ errorMessage: 'Expired' }, { status: 401 })) + await initializing + finishLogin(ok({ username: 'Player', jwt: 'login-token' })) + await signingIn + assert.equal(session.skinSiteStatus.value, 'signed-in') + assert.equal(storage.get(tokenKey), 'login-token') +}) + +test('explicit sign-out discards a late successful login', async () => { + let finishLogin!: (response: Response) => void + respond = () => + new Promise((resolve) => { + finishLogin = resolve + }) + const signingIn = session.signInSkinSite({ + username: 'Player', + password: 'valid', + clientId: 'captcha', + }) + await new Promise((resolve) => setImmediate(resolve)) + session.signOutSkinSite() + finishLogin(ok({ username: 'Player', jwt: 'late-login-token' })) + await signingIn + assert.equal(session.skinSiteStatus.value, 'signed-out') + assert.equal(storage.has(tokenKey), false) +}) diff --git a/apps/app-frontend/src/composables/skin-site-session.ts b/apps/app-frontend/src/composables/skin-site-session.ts index 51f3412..9cae783 100644 --- a/apps/app-frontend/src/composables/skin-site-session.ts +++ b/apps/app-frontend/src/composables/skin-site-session.ts @@ -65,13 +65,54 @@ function clearSessionState() { // --------------------------------------------------------------------------- let initialized: Promise | null = null +let sessionRevision = 0 +let loginRevision = 0 +let refreshRequest: { token: string; revision: number; promise: Promise } | null = null + +/** Share renewals and discard results from a session that has since changed. */ +function renewSkinSiteToken(): Promise { + const token = getSkinSiteToken() + if (!token) return Promise.resolve(false) + const revision = sessionRevision + if (refreshRequest?.token === token && refreshRequest.revision === revision) { + return refreshRequest.promise + } + const request = { + token, + revision, + promise: (async () => { + let result: SkinSiteResponse<{ username: string; jwt: string }> + try { + result = await skinSiteRefresh() + } catch { + return false + } + if (revision !== sessionRevision || getSkinSiteToken() !== token) return false + if (result.success) { + if (typeof result.payload?.jwt !== 'string' || !result.payload.jwt.trim()) return false + setSkinSiteToken(result.payload.jwt) + return true + } + if (result.errorCode === 401 || result.errorCode === 403) invalidateSkinSiteSession() + return false + })(), + } + refreshRequest = request + const clearRequest = () => { + if (refreshRequest === request) refreshRequest = null + } + void request.promise.then(clearRequest, clearRequest) + return request.promise +} /** - * Resolve the current session from the stored JWT. Safe to call repeatedly; + * Renew the saved JWT on each launcher startup before loading the user. + * Safe to call repeatedly; * the first call performs the actual work and later ones reuse it. */ export function initSkinSiteSession(): Promise { if (initialized) return initialized + const revision = sessionRevision initialized = (async () => { const token = getSkinSiteToken() if (!token) { @@ -79,7 +120,13 @@ export function initSkinSiteSession(): Promise { return } status.value = 'checking' + if (!(await renewSkinSiteToken())) { + if (revision === sessionRevision) status.value = 'error' + return + } + if (revision !== sessionRevision) return const result = await skinSiteCurrentUser() + if (revision !== sessionRevision) return if (result.success) { user.value = toUser(result.payload) status.value = 'signed-in' @@ -92,11 +139,18 @@ export function initSkinSiteSession(): Promise { status.value = 'error' } })() + .catch(() => { + if (revision === sessionRevision) status.value = 'error' + }) + .finally(() => { + if (revision === sessionRevision && status.value === 'error') initialized = null + }) return initialized } /** Force a re-check (e.g. after the user signs in from another view). */ export function reloadSkinSiteSession(): Promise { + sessionRevision++ initialized = null return initSkinSiteSession() } @@ -106,10 +160,14 @@ export async function signInSkinSite(payload: { password: string clientId: string }): Promise> { + const attemptRevision = ++loginRevision const result = await skinSiteLogin(payload) - if (result.success) { + if (result.success && attemptRevision === loginRevision) { + const revision = ++sessionRevision + initialized = null setSkinSiteToken(result.payload.jwt) const current = await skinSiteCurrentUser() + if (revision !== sessionRevision) return result if (current.success) { user.value = toUser(current.payload) status.value = 'signed-in' @@ -122,23 +180,25 @@ export async function signInSkinSite(payload: { return result } -export function signOutSkinSite() { +/** Reject an old credential without cancelling a newer explicit login attempt. */ +function invalidateSkinSiteSession() { + sessionRevision++ + refreshRequest = null clearSkinSiteToken() clearSessionState() status.value = 'signed-out' initialized = null } +export function signOutSkinSite() { + loginRevision++ + invalidateSkinSiteSession() +} + /** Refresh the JWT; returns true when a new token was stored. */ export async function refreshSkinSiteSession(): Promise { if (status.value !== 'signed-in') return false - const result = await skinSiteRefresh() - if (result.success) { - setSkinSiteToken(result.payload.jwt) - return true - } - if (result.errorCode === 401 || result.errorCode === 403) signOutSkinSite() - return false + return renewSkinSiteToken() } // --------------------------------------------------------------------------- @@ -157,6 +217,9 @@ export function receiveSkinSiteMessage(_event: MessageEvent, _frame: Window | nu export function openSkinSiteLogin() {} export function resetSkinSiteSession() { + loginRevision++ + sessionRevision++ + refreshRequest = null clearSessionState() status.value = getSkinSiteToken() ? 'checking' : 'signed-out' initialized = null diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index 9a3ae7f..13e7ee9 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -1,4 +1,203 @@ { + "app.skin-site.captcha.loading": { "message": "Loading captcha…" }, + "app.skin-site.captcha.submit": { "message": "Submit" }, + "app.skin-site.captcha.subtitle": { + "message": "Drag the slider to rotate the image upright." + }, + "app.skin-site.captcha.title": { "message": "Let us check you are human" }, + "app.skin-site.downloads.all": { "message": "All" }, + "app.skin-site.downloads.back": { "message": "Back to home" }, + "app.skin-site.downloads.empty": { "message": "Nothing to download yet." }, + "app.skin-site.downloads.failed": { "message": "Could not load downloads." }, + "app.skin-site.downloads.go": { "message": "Download" }, + "app.skin-site.downloads.loading": { "message": "Loading…" }, + "app.skin-site.downloads.status.legacy": { "message": "Legacy" }, + "app.skin-site.downloads.status.new": { "message": "New" }, + "app.skin-site.downloads.status.ok": { "message": "OK" }, + "app.skin-site.downloads.status.outdated": { "message": "Outdated" }, + "app.skin-site.downloads.status.removal": { "message": "Removal" }, + "app.skin-site.downloads.subtitle": { + "message": "Files shared by the server. Pick a category to narrow the list." + }, + "app.skin-site.downloads.title": { "message": "Downloads" }, + "app.skin-site.downloads.uploader": { "message": "by {name}" }, + "app.skin-site.home.announcements.empty": { + "message": "No announcements right now." + }, + "app.skin-site.home.announcements.failed": { + "message": "Could not load announcements." + }, + "app.skin-site.home.announcements.loading": { "message": "Loading…" }, + "app.skin-site.home.announcements.title": { "message": "Announcements" }, + "app.skin-site.home.donation.donors": { "message": "{count} donors" }, + "app.skin-site.home.donation.loading": { "message": "Loading…" }, + "app.skin-site.home.donation.title": { "message": "Donation thanks" }, + "app.skin-site.home.donation.total": { "message": "Total raised" }, + "app.skin-site.home.donation.unavailable": { + "message": "Donation summary unavailable." + }, + "app.skin-site.login.forgot": { "message": "Forgot password?" }, + "app.skin-site.login.password": { "message": "Password" }, + "app.skin-site.login.password-hint": { + "message": "6–50 characters with upper case, lower case and a digit." + }, + "app.skin-site.login.register": { "message": "Create account" }, + "app.skin-site.login.sign-in": { "message": "Sign in" }, + "app.skin-site.login.signing-in": { "message": "Signing in…" }, + "app.skin-site.login.success": { "message": "Signed in to StarLight." }, + "app.skin-site.login.title": { "message": "Sign in to StarLight" }, + "app.skin-site.login.username": { "message": "Username or email" }, + "app.skin-site.login.username-hint": { + "message": "3–16 characters, or a registered email address." + }, + "app.skin-site.map.back": { "message": "Back to home" }, + "app.skin-site.map.empty": { "message": "No maps available." }, + "app.skin-site.map.external": { "message": "External" }, + "app.skin-site.map.failed": { "message": "Could not load maps." }, + "app.skin-site.map.internal": { "message": "Internal" }, + "app.skin-site.map.loading": { "message": "Loading maps…" }, + "app.skin-site.map.map": { "message": "Map" }, + "app.skin-site.map.open": { "message": "Open in browser" }, + "app.skin-site.map.server": { "message": "Server" }, + "app.skin-site.map.title": { "message": "Server map" }, + "app.skin-site.profile-view.back": { "message": "Back to home" }, + "app.skin-site.profile-view.cape": { "message": "Cape" }, + "app.skin-site.profile-view.elytra": { "message": "Elytra" }, + "app.skin-site.profile-view.loading": { "message": "Loading players…" }, + "app.skin-site.profile-view.no-players": { "message": "No players yet." }, + "app.skin-site.profile-view.readonly": { + "message": "Mojang accounts are managed by Mojang; textures cannot be edited here." + }, + "app.skin-site.profile-view.remove": { "message": "Remove" }, + "app.skin-site.profile-view.removed": { "message": "Texture removed." }, + "app.skin-site.profile-view.select-hint": { + "message": "Select a player to preview and edit its textures." + }, + "app.skin-site.profile-view.signed-out": { + "message": "Sign in to manage your players." + }, + "app.skin-site.profile-view.skin": { "message": "Skin" }, + "app.skin-site.profile-view.title": { "message": "Your players" }, + "app.skin-site.profile-view.upload": { "message": "Upload" }, + "app.skin-site.profile-view.uploaded": { "message": "Texture updated." }, + "app.skin-site.profile.avatar": { "message": "Avatar" }, + "app.skin-site.profile.avatar-hint": { + "message": "Square images work best; the centre is cropped to 256×256." + }, + "app.skin-site.profile.avatar-uploaded": { "message": "Avatar updated." }, + "app.skin-site.profile.basic": { "message": "Profile" }, + "app.skin-site.profile.choose-avatar": { "message": "Choose image" }, + "app.skin-site.profile.minecraft-password": { + "message": "Minecraft password" + }, + "app.skin-site.profile.minecraft-password-changed": { + "message": "Minecraft password updated." + }, + "app.skin-site.profile.minecraft-password-hint": { + "message": "Used by the launcher; defaults to the website password." + }, + "app.skin-site.profile.mismatch": { "message": "Passwords do not match." }, + "app.skin-site.profile.new-password": { "message": "New password" }, + "app.skin-site.profile.old-password": { "message": "Current password" }, + "app.skin-site.profile.password-changed": { "message": "Password updated." }, + "app.skin-site.profile.password-hint": { + "message": "6–50 characters with upper case, lower case and a digit." + }, + "app.skin-site.profile.repeat-password": { "message": "Repeat new password" }, + "app.skin-site.profile.reset-default": { + "message": "Reset to website password" + }, + "app.skin-site.profile.save": { "message": "Save" }, + "app.skin-site.profile.saved": { "message": "Saved." }, + "app.skin-site.profile.security": { "message": "Security" }, + "app.skin-site.profile.site-password-hint": { + "message": "This is the password used to sign in to the skin site." + }, + "app.skin-site.profile.submit-password": { "message": "Update password" }, + "app.skin-site.profile.title": { "message": "Account settings" }, + "app.skin-site.profile.username": { "message": "Username" }, + "app.skin-site.profile.username-hint": { "message": "3–16 characters." }, + "app.skin-site.register.closed": { + "message": "Registration is currently closed on this server." + }, + "app.skin-site.register.code": { "message": "Verification code" }, + "app.skin-site.register.email": { "message": "Email" }, + "app.skin-site.register.invite-code": { "message": "Invite code" }, + "app.skin-site.register.invite-required": { + "message": "This server requires an invite code." + }, + "app.skin-site.register.mismatch": { "message": "Passwords do not match." }, + "app.skin-site.register.password": { "message": "Password" }, + "app.skin-site.register.password-hint": { + "message": "6–50 characters with upper case, lower case and a digit." + }, + "app.skin-site.register.qq": { "message": "QQ (optional)" }, + "app.skin-site.register.repeat-password": { "message": "Repeat password" }, + "app.skin-site.register.resend-in": { "message": "Sent ({seconds}s)" }, + "app.skin-site.register.send-code": { "message": "Send code" }, + "app.skin-site.register.sending": { "message": "Sending…" }, + "app.skin-site.register.submit": { "message": "Create account" }, + "app.skin-site.register.success": { + "message": "Account created and signed in." + }, + "app.skin-site.register.title": { "message": "Create a StarLight account" }, + "app.skin-site.reset.code": { "message": "Verification code" }, + "app.skin-site.reset.email": { "message": "Email" }, + "app.skin-site.reset.email-hint": { + "message": "Use the address you registered with." + }, + "app.skin-site.reset.mismatch": { "message": "Passwords do not match." }, + "app.skin-site.reset.new-password": { "message": "New password" }, + "app.skin-site.reset.password-hint": { + "message": "6–50 characters with upper case, lower case and a digit." + }, + "app.skin-site.reset.repeat-password": { "message": "Repeat password" }, + "app.skin-site.reset.resend-in": { "message": "Sent ({seconds}s)" }, + "app.skin-site.reset.send-code": { "message": "Send code" }, + "app.skin-site.reset.sending": { "message": "Sending…" }, + "app.skin-site.reset.submit": { "message": "Reset password" }, + "app.skin-site.reset.success": { + "message": "Password updated. You can sign in now." + }, + "app.skin-site.reset.title": { "message": "Reset password" }, + "app.skin-site.review.back": { "message": "Back to home" }, + "app.skin-site.review.back-to-list": { "message": "Back to forms" }, + "app.skin-site.review.email": { "message": "Your email" }, + "app.skin-site.review.email-hint": { + "message": "Results are sent to this address." + }, + "app.skin-site.review.empty": { + "message": "No application forms are available right now." + }, + "app.skin-site.review.failed": { + "message": "Could not load the review forms." + }, + "app.skin-site.review.invite-code": { "message": "Invite code" }, + "app.skin-site.review.loading": { "message": "Loading…" }, + "app.skin-site.review.optional": { "message": "Optional" }, + "app.skin-site.review.required": { "message": "Required" }, + "app.skin-site.review.result.passed": { "message": "Approved" }, + "app.skin-site.review.result.pending": { "message": "Under review" }, + "app.skin-site.review.result.rejected": { "message": "Rejected" }, + "app.skin-site.review.submit": { "message": "Submit application" }, + "app.skin-site.review.submitted": { "message": "Application submitted." }, + "app.skin-site.review.submitting": { "message": "Submitting…" }, + "app.skin-site.review.subtitle": { + "message": "Pick an application form and answer its questions." + }, + "app.skin-site.review.title": { "message": "Player review" }, + "app.skin-site.home.announcements.signed-out": { + "message": "Sign in to view announcements." + }, + "app.skin-site.chat.user-picker.email": { "message": "Email" }, + "app.skin-site.chat.user-picker.registered": { "message": "Registered" }, + "app.skin-site.chat.user-picker.banned-status": { "message": "Banned" }, + "app.skin-site.chat.user-picker.active-status": { "message": "Active" }, + "app.skin-site.admin.wiki.policy-public": { "message": "Everyone" }, + "app.skin-site.admin.wiki.policy-member": { "message": "Members" }, + "app.skin-site.admin.wiki.policy-admin": { + "message": "Website administrators" + }, "app.account.signed-in-as": { "message": "Signed in as" }, @@ -11555,884 +11754,1314 @@ "app.settings.logs.export.notification-text": { "message": "The log archive was saved to {path}" }, - "app.skins.armor.title": {"message": "Armour and trims"}, - "app.skins.armor.save": {"message": "Save"}, - "app.skins.armor.close": {"message": "Close"}, - "app.skins.armor.description": {"message": "Try armour and trims on your skin. Nothing is stored until you save it."}, - "app.skins.armor.unsaved": {"message": "Unsaved changes"}, - "app.skins.armor.reset-to-saved": {"message": "Reset to saved"}, - "app.skins.armor.restore-defaults": {"message": "Restore defaults"}, - "app.instances.batch-edit-groups.add": {"message": "Add"}, - "app.instances.batch-edit-groups.add-to-group-title": {"message": "Add instances to \"{groupName}\""}, - "app.instances.batch-edit-groups.added": {"message": "Added"}, - "app.instances.batch-edit-groups.cancel": {"message": "Cancel"}, - "app.instances.batch-edit-groups.create": {"message": "Create group"}, - "app.instances.batch-edit-groups.group-name": {"message": "Group name"}, - "app.instances.batch-edit-groups.group-name-placeholder": {"message": "Enter group name"}, - "app.instances.batch-edit-groups.no-instances-found": {"message": "No instances found"}, - "app.instances.batch-edit-groups.save-changes": {"message": "Save"}, - "app.instances.batch-edit-groups.search-instance": {"message": "Search instances"}, - "app.instances.group.add-to-group": {"message": "Add to group"}, - "app.instances.group.collapse": {"message": "Collapse group"}, - "app.instances.group.delete-description": {"message": "Instances in this group will be ungrouped."}, - "app.instances.group.delete-group": {"message": "Delete group"}, - "app.instances.group.edit-name": {"message": "Edit group name"}, - "app.instances.group.empty": {"message": "Drag and drop to add instances."}, - "app.instances.group.expand": {"message": "Expand group"}, - "app.instances.group.move-down": {"message": "Move group down"}, - "app.instances.group.move-up": {"message": "Move group up"}, - "app.instances.group.name-empty": {"message": "Group names cannot be empty."}, - "app.instances.group.name-too-long": {"message": "Group names cannot be longer than {maxLength} characters."}, - "app.instances.group.new-instance": {"message": "New instance"}, - "app.instances.group.pinned": {"message": "Pinned"}, - "app.instances.group.rename-group": {"message": "Rename group"}, - "app.instances.sort-by": {"message": "Sort by"}, - "app.instances.sort.direction.asc-alphabetical": {"message": "A–Z"}, - "app.instances.sort.direction.asc-date": {"message": "Oldest first"}, - "app.instances.sort.direction.asc-recency": {"message": "Least recent first"}, - "app.instances.sort.direction.asc-version": {"message": "Oldest version first"}, - "app.instances.sort.direction.desc-alphabetical": {"message": "Z–A"}, - "app.instances.sort.direction.desc-date": {"message": "Newest first"}, - "app.instances.sort.direction.desc-recency": {"message": "Most recent first"}, - "app.instances.sort.direction.desc-version": {"message": "Newest version first"}, - "app.library.context-menu.pin-instance": {"message": "Pin instance"}, - "app.library.context-menu.remove-from-group": {"message": "Remove from group"}, - "app.library.context-menu.unpin-instance": {"message": "Unpin instance"}, - "app.library.group.new": {"message": "New group"}, - "app.library.selection.new-group": {"message": "New group"}, - "app.library.selection.remove-from-group": {"message": "Remove from group"}, - "app.log-share.settings.title": {"message": "Log sharing & AI analysis"}, - "app.log-share.settings.description": {"message": "Share crash diagnostics with LogShare for a structured summary and optional AI analysis. LogShare is preferred; mclo.gs is used as a fallback."}, - "app.log-share.settings.share-provider": {"message": "Log sharing service"}, - "app.log-share.settings.share-provider-description": {"message": "Where the diagnostic link is uploaded when you share it."}, - "app.log-share.settings.ai-source": {"message": "AI analysis source"}, - "app.log-share.settings.ai-source-logshare": {"message": "LogShare AI"}, - "app.log-share.settings.ai-source-custom": {"message": "Custom AI provider"}, - "app.log-share.settings.ai-source-description": {"message": "Both sources send a sanitized and shortened crash context. AI output may be inaccurate."}, - "app.log-share.settings.auto-upload": {"message": "Upload to get a structured summary automatically"}, - "app.log-share.settings.auto-upload-description": {"message": "Only effective in LogShare AI mode."}, - "app.log-share.settings.multi-file": {"message": "Multi-file upload"}, - "app.log-share.settings.multi-file-description": {"message": "Upload latest.log, crash reports, hs_err and launcher logs together."}, - "app.log-share.settings.no-storage": {"message": "Analyze without storing logs"}, - "app.log-share.settings.no-storage-description": {"message": "Submit content directly for analysis and skip the stored upload. Disables the automatic summary."}, - "app.log-share.settings.show-progress": {"message": "Show AI analysis progress"}, - "app.log-share.settings.show-progress-description": {"message": "Display thinking, tool and limit status while LogShare AI is working."}, - "app.log-share.shared-logs.title": {"message": "Shared logs"}, - "app.log-share.shared-logs.description": {"message": "Every diagnostic you have shared is listed here. Delete an entry to remove it from your history and request its removal from the sharing service."}, - "app.log-share.shared-logs.empty": {"message": "No shared logs yet."}, - "app.log-share.shared-logs.delete": {"message": "Delete"}, - "app.log-share.shared-logs.deleting": {"message": "Deleting…"}, - "app.log-share.shared-logs.deleted": {"message": "Shared log removed."}, - "app.log-share.shared-logs.delete-failed": {"message": "Failed to delete the shared log."}, - "app.log-share.shared-logs.truncated": {"message": "Truncated"}, - "app.minecraft-crash.share-fallback": {"message": "LogShare is unavailable, so the diagnostic was shared via mclo.gs."}, - "app.log-share.ai.action": {"message": "Deep AI analysis"}, - "app.log-share.summary.title": {"message": "LogShare analysis"}, - "app.log-share.summary.loading": {"message": "Loading structured summary..."}, - "app.log-share.summary.empty": {"message": "LogShare did not return a structured summary for these logs."}, - "app.log-share.summary.no-storage": {"message": "Structured summary is unavailable while no-storage mode is enabled."}, - "app.log-share.summary.failed": {"message": "Could not load the LogShare summary: {message}"}, - "app.log-share.summary.problems": {"message": "Problems"}, - "app.log-share.summary.information": {"message": "Information"}, - "app.minecraft-crash.logs.loading": {"message": "Loading crash logs..."}, - "app.minecraft-crash.logs.empty": {"message": "No logs from this launch were found."}, - "app.minecraft-crash.logs.failed": {"message": "Could not load the crash logs: {message}"}, - "app.minecraft-crash.log-file.loading": {"message": "Loading log file..."}, - "app.minecraft-crash.log-file.failed": {"message": "Could not load this log file: {message}"}, - "app.log-share.ai.tab": {"message": "LogAgent detailed analysis"}, - "app.log-share.ai.root-cause": {"message": "Core root cause"}, - "app.log-share.ai.confidence": {"message": "Diagnostic confidence"}, - "app.log-share.ai.steps": {"message": "Recommended troubleshooting steps"}, - "app.log-share.ai.evidence": {"message": "Supporting evidence"}, - "app.log-share.ai.thinking": {"message": "Thinking…"}, - "app.log-share.ai.queued": {"message": "Queued, about {position} ahead"}, - "app.log-share.ai.queued-front": {"message": "Queued, waiting for a free slot"}, - "app.log-share.ai.tool": {"message": "Using tool: {name}"}, - "app.log-share.ai.unknown-tool": {"message": "tool"}, - "app.log-share.ai.tool-result": {"message": "Tool result received"}, - "app.log-share.ai.limit": {"message": "Reached tool loop limit"}, - "app.log-share.ai.working": {"message": "Analyzing with LogAgent…"}, - "app.log-share.ai.failed": {"message": "LogAgent analysis failed: {message}"}, - "app.shortcut-settings.change-shortcut": {"message": "Change the shortcut for {action}"}, - "app.shortcut-settings.enable": {"message": "Enable quick scrolling"}, - "app.shortcut-settings.end-action": {"message": "Scroll to the bottom"}, - "app.shortcut-settings.end-action-description": {"message": "Moves the page you are reading to the bottom."}, - "app.shortcut-settings.home-action": {"message": "Scroll to the top"}, - "app.shortcut-settings.home-action-description": {"message": "Moves the page you are reading to the top."}, - "app.shortcut-settings.nav-create": {"message": "Create new instance"}, - "app.shortcut-settings.nav-create-description": {"message": "Jump to creating a new instance."}, - "app.shortcut-settings.nav-description": {"message": "Jump to a menu item with its shortcut. Each shortcut is off until enabled."}, - "app.shortcut-settings.nav-discover": {"message": "Discover content"}, - "app.shortcut-settings.nav-discover-description": {"message": "Jump to browsing content."}, - "app.shortcut-settings.nav-downloads": {"message": "Downloads"}, - "app.shortcut-settings.nav-downloads-description": {"message": "Jump to downloads."}, - "app.shortcut-settings.nav-home": {"message": "Home"}, - "app.shortcut-settings.nav-home-description": {"message": "Jump to the Home page."}, - "app.shortcut-settings.nav-lab": {"message": "Lab"}, - "app.shortcut-settings.nav-lab-description": {"message": "Jump to the Lab."}, - "app.shortcut-settings.nav-library": {"message": "Library"}, - "app.shortcut-settings.nav-library-description": {"message": "Jump to your library."}, - "app.shortcut-settings.nav-multiplayer": {"message": "Multiplayer"}, - "app.shortcut-settings.nav-multiplayer-description": {"message": "Jump to multiplayer."}, - "app.shortcut-settings.nav-settings": {"message": "Settings"}, - "app.shortcut-settings.nav-settings-description": {"message": "Jump to the settings."}, - "app.shortcut-settings.nav-skins": {"message": "Skin selector"}, - "app.shortcut-settings.nav-skins-description": {"message": "Jump to the skin selector."}, - "app.shortcut-settings.nav-title": {"message": "Navigation shortcuts"}, - "app.shortcut-settings.nav-worlds": {"message": "Worlds"}, - "app.shortcut-settings.nav-worlds-description": {"message": "Jump to your worlds."}, - "app.shortcut-settings.page-down-action": {"message": "Scroll down one screen"}, - "app.shortcut-settings.page-down-action-description": {"message": "Moves the page you are reading down by one screen."}, - "app.shortcut-settings.page-up-action": {"message": "Scroll up one screen"}, - "app.shortcut-settings.page-up-action-description": {"message": "Moves the page you are reading up by one screen."}, - "app.shortcut-settings.scroll-description": {"message": "Move through long pages with the keyboard."}, - "app.shortcut-settings.scroll-title": {"message": "Quick scrolling"}, - "app.settings.tabs.shortcut-settings": {"message": "Keyboard shortcuts"}, - "app.skin-site.bugs.title": {"message": "Bugs & suggestions"}, - "app.skin-site.bugs.back": {"message": "Back to home"}, - "app.skin-site.bugs.subtitle": {"message": "Report issues and follow their progress."}, - "app.skin-site.bugs.loading": {"message": "Loading…"}, - "app.skin-site.bugs.failed": {"message": "Could not load issues."}, - "app.skin-site.bugs.empty": {"message": "No issues match these filters."}, - "app.skin-site.bugs.all": {"message": "All"}, - "app.skin-site.bugs.more": {"message": "Load more"}, - "app.skin-site.bugs.open-only": {"message": "Open only"}, - "app.skin-site.bugs.comments": {"message": "{count} comments"}, - "app.skin-site.bugs.type.bug": {"message": "Bug"}, - "app.skin-site.bugs.type.feature": {"message": "Feature"}, - "app.skin-site.bugs.type.improvement": {"message": "Improvement"}, - "app.skin-site.bugs.status.pending": {"message": "Pending"}, - "app.skin-site.bugs.status.confirmed": {"message": "Confirmed"}, - "app.skin-site.bugs.status.needs-info": {"message": "Needs info"}, - "app.skin-site.bugs.status.in-progress": {"message": "In progress"}, - "app.skin-site.bugs.status.verifying": {"message": "Verifying"}, - "app.skin-site.bugs.status.closed": {"message": "Closed"}, - "app.skin-site.bugs.priority.critical": {"message": "Critical"}, - "app.skin-site.bugs.priority.high": {"message": "High"}, - "app.skin-site.bugs.priority.normal": {"message": "Normal"}, - "app.skin-site.bugs.priority.low": {"message": "Low"}, - "app.skin-site.bugs.resolution.fixed": {"message": "Fixed"}, - "app.skin-site.bugs.resolution.wont-fix": {"message": "Won’t fix"}, - "app.skin-site.bugs.resolution.by-design": {"message": "By design"}, - "app.skin-site.bugs.resolution.duplicate": {"message": "Duplicate"}, - "app.skin-site.bugs.resolution.cannot-reproduce": {"message": "Cannot reproduce"}, - "app.skin-site.bugs.resolution.invalid": {"message": "Invalid"}, - "app.skin-site.bug-detail.back": {"message": "Back to list"}, - "app.skin-site.bug-detail.loading": {"message": "Loading…"}, - "app.skin-site.bug-detail.failed": {"message": "Could not load this issue."}, - "app.skin-site.bug-detail.issue-no": {"message": "Issue #{id}"}, - "app.skin-site.bug-detail.by": {"message": "by {name}"}, - "app.skin-site.bug-detail.assignee": {"message": "Assignee: {name}"}, - "app.skin-site.bug-detail.unassigned": {"message": "Unassigned"}, - "app.skin-site.bug-detail.description-empty": {"message": "No description provided."}, - "app.skin-site.bug-detail.resolution": {"message": "Resolution: {name}"}, - "app.skin-site.bug-detail.comments-title": {"message": "Comments"}, - "app.skin-site.bug-detail.comments-empty": {"message": "No comments yet. Be the first to reply."}, - "app.skin-site.bug-detail.comments-loading": {"message": "Loading comments…"}, - "app.skin-site.bug-detail.comment-placeholder": {"message": "Write a comment…"}, - "app.skin-site.bug-detail.comment-submit": {"message": "Comment"}, - "app.skin-site.bug-detail.comment-sending": {"message": "Sending…"}, - "app.skin-site.bug-detail.comment-empty-error": {"message": "Comment cannot be empty."}, - "app.skin-site.bug-detail.comment-posted": {"message": "Comment posted."}, - "app.skin-site.bug-detail.comment-deleted": {"message": "Comment deleted."}, - "app.skin-site.bug-detail.delete": {"message": "Delete"}, - "app.skin-site.bug-detail.deleting": {"message": "Deleting…"}, - "app.skin-site.bug-detail.system": {"message": "System"}, - "app.skin-site.bug-create.title": {"message": "New issue"}, - "app.skin-site.bug-create.type": {"message": "Type"}, - "app.skin-site.bug-create.title-field": {"message": "Title"}, - "app.skin-site.bug-create.title-placeholder": {"message": "A short summary of the problem"}, - "app.skin-site.bug-create.description": {"message": "Description"}, - "app.skin-site.bug-create.description-placeholder": {"message": "What happened, and how can it be reproduced?"}, - "app.skin-site.bug-create.private": {"message": "Private issue"}, - "app.skin-site.bug-create.private-hint": {"message": "Only you and the maintainers can see it."}, - "app.skin-site.bug-create.images": {"message": "Images"}, - "app.skin-site.bug-create.images-hint": {"message": "Optional, up to 5 images."}, - "app.skin-site.bug-create.submit": {"message": "Create issue"}, - "app.skin-site.bug-create.submitting": {"message": "Creating…"}, - "app.skin-site.bug-create.created": {"message": "Issue created."}, - "app.skin-site.bug-create.cancel": {"message": "Cancel"}, - "app.skin-site.bug-notifications.title": {"message": "Notifications"}, - "app.skin-site.bug-notifications.back": {"message": "Back to home"}, - "app.skin-site.bug-notifications.subtitle": {"message": "Activity on issues you reported or follow."}, - "app.skin-site.bug-notifications.loading": {"message": "Loading…"}, - "app.skin-site.bug-notifications.failed": {"message": "Could not load notifications."}, - "app.skin-site.bug-notifications.empty": {"message": "No new notifications."}, - "app.skin-site.bug-notifications.item": {"message": "{name} updated issue #{id}"}, - "app.skin-site.bug-notifications.mark-all": {"message": "Mark all as read"}, - "app.skin-site.bug-notifications.marked-all": {"message": "All notifications marked as read."}, - "app.skin-site.bugs.create": {"message": "New issue"}, - "app.skin-site.bugs.notifications": {"message": "Notifications"}, - "app.skin-site.home.title": {"message": "StarLight Skin Site"}, - "app.skin-site.home.subtitle": {"message": "The wardrobe for StarLight Server players."}, - "app.skin-site.home.address": {"message": "Skin site address"}, - "app.skin-site.home.copied": {"message": "Address copied."}, - "app.skin-site.home.copy": {"message": "Copy"}, - "app.skin-site.admin.title": {"message": "Admin console"}, - "app.skin-site.admin.back": {"message": "Back to home"}, - "app.skin-site.admin.no-access": {"message": "You do not have access."}, - "app.skin-site.admin.no-access-hint": {"message": "Sign in with an administrator account to use this console."}, - "app.skin-site.admin.group.security": {"message": "Security"}, - "app.skin-site.admin.group.users": {"message": "Users"}, - "app.skin-site.admin.group.community": {"message": "Community"}, - "app.skin-site.admin.group.resources": {"message": "Resources"}, - "app.skin-site.admin.item.servers": {"message": "Servers"}, - "app.skin-site.admin.item.summary": {"message": "Summary"}, - "app.skin-site.admin.item.access-audit": {"message": "Access audit"}, - "app.skin-site.admin.item.users": {"message": "Users"}, - "app.skin-site.admin.item.players": {"message": "Players"}, - "app.skin-site.admin.item.review": {"message": "Player review"}, - "app.skin-site.admin.item.broadcast": {"message": "Broadcast"}, - "app.skin-site.admin.item.chat": {"message": "Chat rooms"}, - "app.skin-site.admin.item.wiki": {"message": "Wiki"}, - "app.skin-site.admin.item.donations": {"message": "Donations"}, - "app.skin-site.admin.item.mods": {"message": "Mods"}, - "app.skin-site.admin.item.launcher": {"message": "Launcher"}, - "app.skin-site.admin.item.downloads": {"message": "Downloads"}, - "app.skin-site.home.admin": {"message": "Admin"}, - "app.skin-site.home.settings": {"message": "Account"}, - "app.skin-site.home.sign-in": {"message": "Sign in"}, - "app.skin-site.home.players": {"message": "Players"}, - "app.skin-site.home.my-players": {"message": "My players"}, - "app.skin-site.home.no-players": {"message": "No players linked yet."}, - "app.skin-site.home.loading": {"message": "Loading…"}, - "app.skin-site.home.error": {"message": "Could not load."}, - "app.skin-site.home.luck": {"message": "Luck"}, - "app.skin-site.home.luck-unavailable": {"message": "Unavailable"}, - "app.skin-site.home.signed-out-title": {"message": "Not signed in"}, - "app.skin-site.home.signed-out-body": {"message": "Sign in to manage your account and textures."}, - "app.skin-site.admin.summary.title": {"message": "System overview"}, - "app.skin-site.admin.summary.loading": {"message": "Loading…"}, - "app.skin-site.admin.summary.failed": {"message": "Could not load status."}, - "app.skin-site.admin.summary.uptime": {"message": "Uptime"}, - "app.skin-site.admin.summary.memory": {"message": "Memory usage"}, - "app.skin-site.admin.summary.users": {"message": "Users"}, - "app.skin-site.admin.summary.players": {"message": "Players"}, - "app.skin-site.admin.summary.realtime-hint": {"message": "Live CPU and GC charts require the admin WebSocket, which is not connected yet."}, - "app.skin-site.admin.broadcast.title": {"message": "Announcements"}, - "app.skin-site.admin.broadcast.loading": {"message": "Loading…"}, - "app.skin-site.admin.broadcast.failed": {"message": "Could not load."}, - "app.skin-site.admin.broadcast.empty": {"message": "No announcements yet."}, - "app.skin-site.admin.broadcast.create": {"message": "New announcement"}, - "app.skin-site.admin.broadcast.save": {"message": "Save"}, - "app.skin-site.admin.broadcast.cancel": {"message": "Cancel"}, - "app.skin-site.admin.broadcast.edit": {"message": "Edit"}, - "app.skin-site.admin.broadcast.remove": {"message": "Delete"}, - "app.skin-site.admin.broadcast.title-field": {"message": "Title"}, - "app.skin-site.admin.broadcast.subtitle-field": {"message": "Subtitle"}, - "app.skin-site.admin.broadcast.content-field": {"message": "Content"}, - "app.skin-site.admin.broadcast.expire-at": {"message": "Expires at"}, - "app.skin-site.admin.broadcast.star": {"message": "Highlight"}, - "app.skin-site.admin.broadcast.level": {"message": "Importance"}, - "app.skin-site.admin.broadcast.image": {"message": "Image"}, - "app.skin-site.admin.broadcast.remove-image": {"message": "Remove image"}, - "app.skin-site.admin.broadcast.created": {"message": "Announcement created."}, - "app.skin-site.admin.broadcast.updated": {"message": "Announcement updated."}, - "app.skin-site.admin.broadcast.deleted": {"message": "Announcement deleted."}, - "app.skin-site.admin.broadcast.confirm-delete": {"message": "Delete “{title}”? This cannot be undone."}, - "app.skin-site.admin.broadcast.uploader": {"message": "by {name}"}, - "app.skin-site.admin.players.title": {"message": "Players"}, - "app.skin-site.admin.players.loading": {"message": "Loading…"}, - "app.skin-site.admin.players.failed": {"message": "Could not load players."}, - "app.skin-site.admin.players.empty": {"message": "No players found."}, - "app.skin-site.admin.players.search": {"message": "Search"}, - "app.skin-site.admin.players.search-name": {"message": "Name"}, - "app.skin-site.admin.players.search-owner": {"message": "Owner"}, - "app.skin-site.admin.players.search-uuid": {"message": "UUID"}, - "app.skin-site.admin.players.total": {"message": "{count} players"}, - "app.skin-site.admin.players.prev": {"message": "Previous"}, - "app.skin-site.admin.players.next": {"message": "Next"}, - "app.skin-site.admin.players.page": {"message": "Page {page}"}, - "app.skin-site.admin.players.name": {"message": "Name"}, - "app.skin-site.admin.players.owner": {"message": "Owner"}, - "app.skin-site.admin.players.source": {"message": "Source"}, - "app.skin-site.admin.players.legacy": {"message": "Legacy adapted"}, - "app.skin-site.admin.players.actions": {"message": "Actions"}, - "app.skin-site.admin.players.edit": {"message": "Edit"}, - "app.skin-site.admin.players.save": {"message": "Save"}, - "app.skin-site.admin.players.cancel": {"message": "Cancel"}, - "app.skin-site.admin.players.remove": {"message": "Delete"}, - "app.skin-site.admin.players.updated": {"message": "Player updated."}, - "app.skin-site.admin.players.deleted": {"message": "Player deleted."}, - "app.skin-site.admin.players.confirm-delete": {"message": "Delete player “{name}”? This cannot be undone."}, - "app.skin-site.admin.players.mojang": {"message": "Mojang"}, - "app.skin-site.admin.players.custom": {"message": "Skin site"}, - "app.skin-site.admin.users.title": {"message": "Users"}, - "app.skin-site.admin.users.loading": {"message": "Loading…"}, - "app.skin-site.admin.users.failed": {"message": "Could not load users."}, - "app.skin-site.admin.users.empty": {"message": "No users found."}, - "app.skin-site.admin.users.search": {"message": "Search"}, - "app.skin-site.admin.users.search-name": {"message": "Username"}, - "app.skin-site.admin.users.search-email": {"message": "Email"}, - "app.skin-site.admin.users.search-uuid": {"message": "UUID"}, - "app.skin-site.admin.users.total": {"message": "{count} users"}, - "app.skin-site.admin.users.prev": {"message": "Previous"}, - "app.skin-site.admin.users.next": {"message": "Next"}, - "app.skin-site.admin.users.page": {"message": "Page {page}"}, - "app.skin-site.admin.users.edit": {"message": "Edit"}, - "app.skin-site.admin.users.save": {"message": "Save"}, - "app.skin-site.admin.users.cancel": {"message": "Cancel"}, - "app.skin-site.admin.users.remove": {"message": "Delete"}, - "app.skin-site.admin.users.players": {"message": "Players"}, - "app.skin-site.admin.users.updated": {"message": "User updated."}, - "app.skin-site.admin.users.deleted": {"message": "User deleted."}, - "app.skin-site.admin.users.confirm-delete": {"message": "Delete user “{name}”? This cannot be undone."}, - "app.skin-site.admin.users.username": {"message": "Username"}, - "app.skin-site.admin.users.email": {"message": "Email"}, - "app.skin-site.admin.users.qq": {"message": "QQ"}, - "app.skin-site.admin.users.password": {"message": "New password"}, - "app.skin-site.admin.users.password-hint": {"message": "Leave empty to keep the current password."}, - "app.skin-site.admin.users.banned": {"message": "Banned"}, - "app.skin-site.admin.users.permissions": {"message": "Permissions"}, - "app.skin-site.admin.users.permissions-hint": {"message": "Comma separated, e.g. WEB_ADMIN, SERVER_ADMIN."}, - "app.skin-site.admin.users.no-players": {"message": "No linked players."}, - "app.skin-site.admin.users.registered": {"message": "Registered"}, - "app.skin-site.admin.review.title": {"message": "Player review"}, - "app.skin-site.admin.review.loading": {"message": "Loading…"}, - "app.skin-site.admin.review.failed": {"message": "Could not load submissions."}, - "app.skin-site.admin.review.empty": {"message": "No submissions found."}, - "app.skin-site.admin.review.total": {"message": "{count} submissions"}, - "app.skin-site.admin.review.filter-all": {"message": "All"}, - "app.skin-site.admin.review.filter-pending": {"message": "Pending"}, - "app.skin-site.admin.review.filter-passed": {"message": "Passed"}, - "app.skin-site.admin.review.filter-rejected": {"message": "Rejected"}, - "app.skin-site.admin.review.result-pending": {"message": "Pending"}, - "app.skin-site.admin.review.result-passed": {"message": "Passed"}, - "app.skin-site.admin.review.result-rejected": {"message": "Rejected"}, - "app.skin-site.admin.review.mode-auto": {"message": "Auto"}, - "app.skin-site.admin.review.mode-manual": {"message": "Manual"}, - "app.skin-site.admin.review.applicant": {"message": "Applicant"}, - "app.skin-site.admin.review.submitted-at": {"message": "Submitted"}, - "app.skin-site.admin.review.score": {"message": "Score"}, - "app.skin-site.admin.review.reviewer": {"message": "Reviewer"}, - "app.skin-site.admin.review.details": {"message": "Details"}, - "app.skin-site.admin.review.hide": {"message": "Hide"}, - "app.skin-site.admin.review.answers": {"message": "Answers"}, - "app.skin-site.admin.review.no-answers": {"message": "No answers."}, - "app.skin-site.admin.review.auto-checks": {"message": "Automatic checks"}, - "app.skin-site.admin.review.context": {"message": "Submission context"}, - "app.skin-site.admin.review.ip": {"message": "IP"}, - "app.skin-site.admin.review.user-agent": {"message": "User agent"}, - "app.skin-site.admin.review.activity": {"message": "Activity"}, - "app.skin-site.admin.review.duration": {"message": "Duration"}, - "app.skin-site.admin.review.hidden-time": {"message": "Hidden time"}, - "app.skin-site.admin.review.screen-switches": {"message": "Screen switches"}, - "app.skin-site.admin.review.history": {"message": "Decision history"}, - "app.skin-site.admin.review.accept": {"message": "Accept"}, - "app.skin-site.admin.review.reject": {"message": "Reject"}, - "app.skin-site.admin.review.reason": {"message": "Reason"}, - "app.skin-site.admin.review.accept-done": {"message": "Submission accepted."}, - "app.skin-site.admin.review.reject-done": {"message": "Submission rejected."}, - "app.skin-site.admin.review.regen-invite": {"message": "Regenerate invite"}, - "app.skin-site.admin.review.invite-regenerated": {"message": "Invite code regenerated."}, - "app.skin-site.admin.review.invite-sent": {"message": "Email sent."}, - "app.skin-site.admin.review.invite-not-sent": {"message": "Email not sent."}, - "app.skin-site.admin.review.prev": {"message": "Previous"}, - "app.skin-site.admin.review.next": {"message": "Next"}, - "app.skin-site.admin.review.page": {"message": "Page {page}"}, - "app.skin-site.admin.launcher.title": {"message": "Launcher releases"}, - "app.skin-site.admin.launcher.loading": {"message": "Loading…"}, - "app.skin-site.admin.launcher.failed": {"message": "Could not load releases."}, - "app.skin-site.admin.launcher.empty": {"message": "No releases uploaded yet."}, - "app.skin-site.admin.launcher.latest-url": {"message": "Latest URL"}, - "app.skin-site.admin.launcher.version": {"message": "Version"}, - "app.skin-site.admin.launcher.channel": {"message": "Channel"}, - "app.skin-site.admin.launcher.uploader": {"message": "Uploaded by"}, - "app.skin-site.admin.launcher.uploaded-at": {"message": "Uploaded"}, - "app.skin-site.admin.launcher.published-at": {"message": "Published"}, - "app.skin-site.admin.launcher.force-update": {"message": "Forced"}, - "app.skin-site.admin.launcher.active": {"message": "Active"}, - "app.skin-site.admin.launcher.activate": {"message": "Activate"}, - "app.skin-site.admin.launcher.activated": {"message": "Release activated."}, - "app.skin-site.admin.launcher.upload": {"message": "Upload release"}, - "app.skin-site.admin.launcher.upload-hint": {"message": "Choose a release ZIP (max 4 GiB)."}, - "app.skin-site.admin.launcher.uploading": {"message": "Uploading…"}, - "app.skin-site.admin.launcher.upload-done": {"message": "Release uploaded."}, - "app.skin-site.admin.launcher.force-flag": {"message": "Force update"}, - "app.skin-site.admin.launcher.prev": {"message": "Previous"}, - "app.skin-site.admin.launcher.next": {"message": "Next"}, - "app.skin-site.admin.launcher.page": {"message": "Page {page}"}, - "app.skin-site.admin.launcher.total": {"message": "{count} releases"}, - "app.skin-site.admin.downloads.title": {"message": "Downloads"}, - "app.skin-site.admin.downloads.loading": {"message": "Loading…"}, - "app.skin-site.admin.downloads.failed": {"message": "Could not load downloads."}, - "app.skin-site.admin.downloads.empty": {"message": "No download items."}, - "app.skin-site.admin.downloads.categories": {"message": "Categories"}, - "app.skin-site.admin.downloads.all": {"message": "All items"}, - "app.skin-site.admin.downloads.add-category": {"message": "Add"}, - "app.skin-site.admin.downloads.category-name": {"message": "Category name"}, - "app.skin-site.admin.downloads.remove-category": {"message": "Remove"}, - "app.skin-site.admin.downloads.total": {"message": "{count} items"}, - "app.skin-site.admin.downloads.add-item": {"message": "New item"}, - "app.skin-site.admin.downloads.save": {"message": "Save"}, - "app.skin-site.admin.downloads.cancel": {"message": "Cancel"}, - "app.skin-site.admin.downloads.edit": {"message": "Edit"}, - "app.skin-site.admin.downloads.remove": {"message": "Delete"}, - "app.skin-site.admin.downloads.filename": {"message": "File name"}, - "app.skin-site.admin.downloads.description": {"message": "Description"}, - "app.skin-site.admin.downloads.routes": {"message": "Download routes"}, - "app.skin-site.admin.downloads.route-name": {"message": "Name"}, - "app.skin-site.admin.downloads.route-url": {"message": "URL"}, - "app.skin-site.admin.downloads.add-route": {"message": "Add route"}, - "app.skin-site.admin.downloads.remove-route": {"message": "Remove"}, - "app.skin-site.admin.downloads.statuses": {"message": "Statuses"}, - "app.skin-site.admin.downloads.bind-category": {"message": "Categories"}, - "app.skin-site.admin.downloads.uploader": {"message": "by {name}"}, - "app.skin-site.admin.downloads.created": {"message": "Item created."}, - "app.skin-site.admin.downloads.updated": {"message": "Item updated."}, - "app.skin-site.admin.downloads.deleted": {"message": "Item deleted."}, - "app.skin-site.admin.downloads.category-added": {"message": "Category added."}, - "app.skin-site.admin.downloads.category-deleted": {"message": "Category deleted."}, - "app.skin-site.admin.downloads.confirm-delete-item": {"message": "Delete “{name}”? This cannot be undone."}, - "app.skin-site.admin.downloads.confirm-delete-category": {"message": "Delete category “{name}”?"}, - "app.skin-site.admin.servers.title": {"message": "Map servers"}, - "app.skin-site.admin.servers.loading": {"message": "Loading…"}, - "app.skin-site.admin.servers.failed": {"message": "Could not load servers."}, - "app.skin-site.admin.servers.empty": {"message": "No map servers configured."}, - "app.skin-site.admin.servers.server-name": {"message": "Server name"}, - "app.skin-site.admin.servers.internal": {"message": "Internal"}, - "app.skin-site.admin.servers.maps": {"message": "Maps"}, - "app.skin-site.admin.servers.map-name": {"message": "Map name"}, - "app.skin-site.admin.servers.map-url": {"message": "Map URL"}, - "app.skin-site.admin.servers.add-map": {"message": "Add map"}, - "app.skin-site.admin.servers.remove-map": {"message": "Remove"}, - "app.skin-site.admin.servers.add-server": {"message": "Add server"}, - "app.skin-site.admin.servers.remove-server": {"message": "Remove server"}, - "app.skin-site.admin.servers.save": {"message": "Save changes"}, - "app.skin-site.admin.servers.saving": {"message": "Saving…"}, - "app.skin-site.admin.servers.saved": {"message": "Map servers saved."}, - "app.skin-site.admin.audit.title": {"message": "Access audit"}, - "app.skin-site.admin.audit.loading": {"message": "Loading…"}, - "app.skin-site.admin.audit.failed": {"message": "Could not load audit log."}, - "app.skin-site.admin.audit.empty": {"message": "No audit entries."}, - "app.skin-site.admin.audit.subject": {"message": "Subject"}, - "app.skin-site.admin.audit.action": {"message": "Action"}, - "app.skin-site.admin.audit.source-ip": {"message": "Source IP"}, - "app.skin-site.admin.audit.reported-ip": {"message": "Reported player IP"}, - "app.skin-site.admin.audit.count": {"message": "Count"}, - "app.skin-site.admin.audit.first-at": {"message": "First seen"}, - "app.skin-site.admin.audit.last-at": {"message": "Last seen"}, - "app.skin-site.admin.audit.proxy": {"message": "Proxy"}, - "app.skin-site.admin.audit.internal": {"message": "Internal"}, - "app.skin-site.admin.audit.search-subject": {"message": "Subject"}, - "app.skin-site.admin.audit.search-ip": {"message": "IP"}, - "app.skin-site.admin.audit.search-action": {"message": "Action"}, - "app.skin-site.admin.audit.search": {"message": "Search"}, - "app.skin-site.admin.audit.clear": {"message": "Clear"}, - "app.skin-site.admin.audit.load-more": {"message": "Load more"}, - "app.skin-site.admin.audit.retention": {"message": "Retention (days)"}, - "app.skin-site.admin.audit.record-limit": {"message": "Record limit"}, - "app.skin-site.admin.audit.save-retention": {"message": "Save"}, - "app.skin-site.admin.audit.retention-saved": {"message": "Retention updated."}, - "app.skin-site.admin.donations.title": {"message": "Donations"}, - "app.skin-site.admin.donations.loading": {"message": "Loading…"}, - "app.skin-site.admin.donations.failed": {"message": "Could not load donations."}, - "app.skin-site.admin.donations.empty": {"message": "No donations yet."}, - "app.skin-site.admin.donations.total-amount": {"message": "Total"}, - "app.skin-site.admin.donations.donor-count": {"message": "Donors"}, - "app.skin-site.admin.donations.donor": {"message": "Donor"}, - "app.skin-site.admin.donations.amount": {"message": "Amount"}, - "app.skin-site.admin.donations.updated-at": {"message": "Updated"}, - "app.skin-site.admin.donations.external": {"message": "External"}, - "app.skin-site.admin.donations.linked": {"message": "Linked"}, - "app.skin-site.admin.donations.create": {"message": "Add donation"}, - "app.skin-site.admin.donations.save": {"message": "Save"}, - "app.skin-site.admin.donations.cancel": {"message": "Cancel"}, - "app.skin-site.admin.donations.edit": {"message": "Edit"}, - "app.skin-site.admin.donations.remove": {"message": "Delete"}, - "app.skin-site.admin.donations.amount-field": {"message": "Amount"}, - "app.skin-site.admin.donations.display-name": {"message": "Display name"}, - "app.skin-site.admin.donations.user-uuid": {"message": "User UUID"}, - "app.skin-site.admin.donations.qq": {"message": "QQ"}, - "app.skin-site.admin.donations.append-mode": {"message": "Append to existing amount"}, - "app.skin-site.admin.donations.created": {"message": "Donation created."}, - "app.skin-site.admin.donations.updated": {"message": "Donation updated."}, - "app.skin-site.admin.donations.deleted": {"message": "Donation deleted."}, - "app.skin-site.admin.donations.confirm-delete": {"message": "Delete the donation from “{name}”? This cannot be undone."}, - "app.skin-site.admin.donations.load-more": {"message": "Load more"}, - "app.skin-site.admin.mods.title": {"message": "Mods"}, - "app.skin-site.admin.mods.tab-mods": {"message": "Mod files"}, - "app.skin-site.admin.mods.tab-tags": {"message": "Rule tags"}, - "app.skin-site.admin.mods.tab-runtime": {"message": "Runtime"}, - "app.skin-site.admin.mods.loading": {"message": "Loading…"}, - "app.skin-site.admin.mods.failed": {"message": "Could not load mods."}, - "app.skin-site.admin.mods.empty": {"message": "No mods found."}, - "app.skin-site.admin.mods.total": {"message": "{count} mods"}, - "app.skin-site.admin.mods.search": {"message": "Search by name"}, - "app.skin-site.admin.mods.filter-all": {"message": "All tags"}, - "app.skin-site.admin.mods.prev": {"message": "Previous"}, - "app.skin-site.admin.mods.next": {"message": "Next"}, - "app.skin-site.admin.mods.page": {"message": "Page {page}"}, - "app.skin-site.admin.mods.name": {"message": "Name"}, - "app.skin-site.admin.mods.version": {"message": "Version"}, - "app.skin-site.admin.mods.mod-id": {"message": "Mod ID"}, - "app.skin-site.admin.mods.size": {"message": "Size"}, - "app.skin-site.admin.mods.loaders": {"message": "Loaders"}, - "app.skin-site.admin.mods.domain-rules": {"message": "Domain rules"}, - "app.skin-site.admin.mods.edit": {"message": "Edit"}, - "app.skin-site.admin.mods.save": {"message": "Save"}, - "app.skin-site.admin.mods.cancel": {"message": "Cancel"}, - "app.skin-site.admin.mods.remove": {"message": "Delete"}, - "app.skin-site.admin.mods.updated": {"message": "Mod updated."}, - "app.skin-site.admin.mods.deleted": {"message": "Mod deleted."}, - "app.skin-site.admin.mods.confirm-delete": {"message": "Delete mod “{name}”? This cannot be undone."}, - "app.skin-site.admin.mods.rule-tags": {"message": "Rule tags"}, - "app.skin-site.admin.mods.tag-name": {"message": "Tag name"}, - "app.skin-site.admin.mods.tag-rules": {"message": "Rules"}, - "app.skin-site.admin.mods.tag-rules-hint": {"message": "One rule per line."}, - "app.skin-site.admin.mods.new-tag": {"message": "New tag"}, - "app.skin-site.admin.mods.tag-created": {"message": "Tag created."}, - "app.skin-site.admin.mods.tag-updated": {"message": "Tag updated."}, - "app.skin-site.admin.mods.tag-deleted": {"message": "Tag deleted."}, - "app.skin-site.admin.mods.confirm-delete-tag": {"message": "Delete tag “{name}”?"}, - "app.skin-site.admin.mods.runtime": {"message": "Runtime releases"}, - "app.skin-site.admin.mods.manifest-url": {"message": "Manifest URL"}, - "app.skin-site.admin.mods.upload-runtime": {"message": "Upload runtime"}, - "app.skin-site.admin.mods.runtime-version": {"message": "Version"}, - "app.skin-site.admin.mods.uploading": {"message": "Uploading…"}, - "app.skin-site.admin.mods.runtime-uploaded": {"message": "Runtime uploaded."}, - "app.skin-site.admin.mods.activate": {"message": "Activate"}, - "app.skin-site.admin.mods.activated": {"message": "Runtime activated."}, - "app.skin-site.admin.mods.available": {"message": "Available"}, - "app.skin-site.admin.mods.unavailable": {"message": "Unavailable"}, - "app.skin-site.admin.mods.active": {"message": "Active"}, - "app.skin-site.admin.mods.uploader": {"message": "by {name}"}, - "app.skin-site.home.nav.title": {"message": "Skin site"}, - "app.skin-site.home.nav.download": {"message": "Downloads"}, - "app.skin-site.home.nav.download-desc": {"message": "Browse files shared by the server."}, - "app.skin-site.home.nav.review": {"message": "Player review"}, - "app.skin-site.home.nav.review-desc": {"message": "Submit and review player applications."}, - "app.skin-site.home.nav.wiki": {"message": "Server wiki"}, - "app.skin-site.home.nav.wiki-desc": {"message": "Guides and documentation for the server."}, - "app.skin-site.home.nav.bugs": {"message": "Bug tracker"}, - "app.skin-site.home.nav.bugs-desc": {"message": "Report issues and follow their progress."}, - "app.skin-site.home.nav.map": {"message": "Server map"}, - "app.skin-site.home.nav.map-desc": {"message": "Explore the live world map."}, - "app.skin-site.home.nav.profile": {"message": "Profile"}, - "app.skin-site.home.nav.profile-desc": {"message": "Manage your account and textures."}, - "app.skin-site.admin.wiki.title": {"message": "Wiki"}, - "app.skin-site.admin.wiki.tab-page": {"message": "Page"}, - "app.skin-site.admin.wiki.tab-audit": {"message": "Audit log"}, - "app.skin-site.admin.wiki.tab-settings": {"message": "Settings"}, - "app.skin-site.admin.wiki.loading": {"message": "Loading…"}, - "app.skin-site.admin.wiki.page-path": {"message": "Page path"}, - "app.skin-site.admin.wiki.load-page": {"message": "Load page"}, - "app.skin-site.admin.wiki.page-not-found": {"message": "Page not found."}, - "app.skin-site.admin.wiki.revision": {"message": "Revision"}, - "app.skin-site.admin.wiki.author": {"message": "Author"}, - "app.skin-site.admin.wiki.updated-at": {"message": "Updated"}, - "app.skin-site.admin.wiki.categories": {"message": "Categories"}, - "app.skin-site.admin.wiki.read-policy": {"message": "Read policy"}, - "app.skin-site.admin.wiki.edit-policy": {"message": "Edit policy"}, - "app.skin-site.admin.wiki.save-protect": {"message": "Save policies"}, - "app.skin-site.admin.wiki.protect-saved": {"message": "Page policies updated."}, - "app.skin-site.admin.wiki.set-home": {"message": "Set as home page"}, - "app.skin-site.admin.wiki.home-set": {"message": "Home page updated."}, - "app.skin-site.admin.wiki.set-deleted": {"message": "Mark as deleted"}, - "app.skin-site.admin.wiki.set-restored": {"message": "Restore page"}, - "app.skin-site.admin.wiki.deleted-updated": {"message": "Page deletion state updated."}, - "app.skin-site.admin.wiki.audit-empty": {"message": "No audit entries."}, - "app.skin-site.admin.wiki.audit-target": {"message": "Target"}, - "app.skin-site.admin.wiki.load-more": {"message": "Load more"}, - "app.skin-site.admin.wiki.settings-hint": {"message": "Comma separated domains."}, - "app.skin-site.admin.wiki.domains": {"message": "Domains"}, - "app.skin-site.admin.wiki.home-page": {"message": "Home page"}, - "app.skin-site.admin.wiki.default-skin": {"message": "Default skin"}, - "app.skin-site.admin.wiki.max-page-bytes": {"message": "Max page bytes"}, - "app.skin-site.admin.wiki.max-attachment-bytes": {"message": "Max attachment bytes"}, - "app.skin-site.admin.wiki.save-settings": {"message": "Save settings"}, - "app.skin-site.admin.wiki.settings-saved": {"message": "Settings saved."}, - "app.skin-site.admin.review.tab-list": {"message": "Submissions"}, - "app.skin-site.admin.review.tab-papers": {"message": "Papers"}, - "app.skin-site.admin.review.papers-empty": {"message": "No review papers yet."}, - "app.skin-site.admin.review.new-paper": {"message": "New paper"}, - "app.skin-site.admin.review.edit-paper": {"message": "Edit"}, - "app.skin-site.admin.review.paper-title": {"message": "Title"}, - "app.skin-site.admin.review.paper-annotation": {"message": "Description"}, - "app.skin-site.admin.review.paper-enabled": {"message": "Enabled"}, - "app.skin-site.admin.review.paper-mode": {"message": "Mode"}, - "app.skin-site.admin.review.paper-pass-score": {"message": "Pass score"}, - "app.skin-site.admin.review.paper-target-permissions": {"message": "Target permissions"}, - "app.skin-site.admin.review.paper-questions": {"message": "Questions"}, - "app.skin-site.admin.review.add-question": {"message": "Add question"}, - "app.skin-site.admin.review.question-title": {"message": "Question title"}, - "app.skin-site.admin.review.question-type": {"message": "Type"}, - "app.skin-site.admin.review.question-score": {"message": "Score"}, - "app.skin-site.admin.review.question-annotation": {"message": "Annotation"}, - "app.skin-site.admin.review.add-option": {"message": "Add option"}, - "app.skin-site.admin.review.option-description": {"message": "Option text"}, - "app.skin-site.admin.review.option-correct": {"message": "Correct"}, - "app.skin-site.admin.review.remove-question": {"message": "Remove"}, - "app.skin-site.admin.review.question-no": {"message": "Question {n}"}, - "app.skin-site.admin.review.paper-created": {"message": "Paper created."}, - "app.skin-site.admin.review.paper-saved": {"message": "Paper saved."}, - "app.skin-site.admin.review.mail-title": {"message": "Pass mail"}, - "app.skin-site.admin.review.mail-enabled": {"message": "Send mail on pass"}, - "app.skin-site.admin.review.mail-subject": {"message": "Subject"}, - "app.skin-site.admin.review.mail-message": {"message": "Body"}, - "app.skin-site.admin.review.mail-extra": {"message": "Extra text"}, - "app.skin-site.admin.review.mail-include-invite": {"message": "Include invite code"}, - "app.skin-site.admin.review.prevent-duplicate": {"message": "Reject duplicate submissions"}, - "app.skin-site.admin.review.copy-link": {"message": "Copy link"}, - "app.skin-site.admin.review.link-copied": {"message": "Link copied."}, - "app.skin-site.admin.review.move-up": {"message": "Move up"}, - "app.skin-site.admin.review.move-down": {"message": "Move down"}, - "app.skin-site.admin.review.mail-subject-required": {"message": "Subject is required when mail is enabled."}, - "app.skin-site.admin.review.mail-message-required": {"message": "Body is required when mail is enabled."}, - "app.skin-site.admin.users.actions": {"message": "Actions"}, - "app.skin-site.admin.users.audit": {"message": "Audit"}, - "app.skin-site.admin.users.audit-empty": {"message": "No audit records yet."}, - "app.skin-site.admin.users.audit-more": {"message": "Load earlier"}, - "app.skin-site.admin.users.audit-title": {"message": "User management audit"}, - "app.skin-site.admin.users.bot": {"message": "Bot"}, - "app.skin-site.admin.users.bot-active": {"message": "Token valid"}, - "app.skin-site.admin.users.bot-ban": {"message": "Ban bot creation"}, - "app.skin-site.admin.users.bot-creation-banned": {"message": "Token creation banned"}, - "app.skin-site.admin.users.bot-exists": {"message": "Bot created"}, - "app.skin-site.admin.users.bot-inactive": {"message": "Token invalidated"}, - "app.skin-site.admin.users.bot-invalidate": {"message": "Invalidate token"}, - "app.skin-site.admin.users.bot-not-exists": {"message": "No bot yet"}, - "app.skin-site.admin.users.bot-save-username": {"message": "Save bot username"}, - "app.skin-site.admin.users.bot-title": {"message": "Bot management"}, - "app.skin-site.admin.users.bot-token-hint": {"message": "Token hint"}, - "app.skin-site.admin.users.bot-unban": {"message": "Unban bot creation"}, - "app.skin-site.admin.users.bot-username": {"message": "Bot username"}, - "app.skin-site.admin.users.bound-players-title": {"message": "Linked players"}, - "app.skin-site.admin.users.frequency": {"message": "Rate limit"}, - "app.skin-site.admin.users.frequency-clear": {"message": "Clear limit"}, - "app.skin-site.admin.users.frequency-current": {"message": "Currently {percent}%"}, - "app.skin-site.admin.users.frequency-hint": {"message": "Packets from this user will be dropped at the given rate."}, - "app.skin-site.admin.users.frequency-none": {"message": "Not enabled"}, - "app.skin-site.admin.users.frequency-range": {"message": "Only integer values from 10% to 100% are allowed."}, - "app.skin-site.admin.users.frequency-save": {"message": "Save limit"}, - "app.skin-site.admin.users.frequency-title": {"message": "Rate limit"}, - "app.skin-site.admin.users.invite": {"message": "Invite user"}, - "app.skin-site.admin.users.invite-copied": {"message": "Invite code copied."}, - "app.skin-site.admin.users.invite-copy": {"message": "Copy"}, - "app.skin-site.admin.users.invite-email": {"message": "Email"}, - "app.skin-site.admin.users.invite-email-invalid": {"message": "Enter a valid email address."}, - "app.skin-site.admin.users.invite-generate": {"message": "Generate invite code"}, - "app.skin-site.admin.users.invite-title": {"message": "Invite user"}, - "app.skin-site.admin.users.legacy-adapted": {"message": "Legacy internal adapted"}, - "app.skin-site.admin.users.registered-at": {"message": "Registered at"}, - "app.skin-site.admin.users.status": {"message": "Status"}, - "app.skin-site.admin.users.status-banned": {"message": "Banned"}, - "app.skin-site.admin.users.status-normal": {"message": "Normal"}, - "app.skin-site.admin.mods.upload": {"message": "Upload"}, - "app.skin-site.admin.mods.select-mode": {"message": "Select multiple"}, - "app.skin-site.admin.mods.exit-select": {"message": "Done selecting"}, - "app.skin-site.admin.mods.select-all": {"message": "Select all"}, - "app.skin-site.admin.mods.clear-selection": {"message": "Clear selection"}, - "app.skin-site.admin.mods.selected-count": {"message": "{count} selected"}, - "app.skin-site.admin.mods.delete-selected": {"message": "Delete selected"}, - "app.skin-site.admin.mods.confirm-delete-multi": {"message": "Delete {count} selected mods? This cannot be undone."}, - "app.skin-site.admin.mods.deleted-multi": {"message": "Selected mods deleted."}, - "app.skin-site.admin.mods.server-address": {"message": "Server address"}, - "app.skin-site.admin.mods.server-address-hint": {"message": "Filter by server address"}, - "app.skin-site.admin.mods.apply": {"message": "Apply"}, - "app.skin-site.admin.mods.clear": {"message": "Clear"}, - "app.skin-site.admin.mods.upload-title": {"message": "Upload mods"}, - "app.skin-site.admin.mods.upload-drop": {"message": "Drag .jar files here, or click to choose"}, - "app.skin-site.admin.mods.upload-sub": {"message": "Multiple files supported, up to {size} MB each"}, - "app.skin-site.admin.mods.upload-tags": {"message": "Attach rule tags"}, - "app.skin-site.admin.mods.upload-queue": {"message": "Files"}, - "app.skin-site.admin.mods.upload-pending": {"message": "Pending"}, - "app.skin-site.admin.mods.upload-checking": {"message": "Checking…"}, - "app.skin-site.admin.mods.upload-uploading": {"message": "Uploading…"}, - "app.skin-site.admin.mods.upload-conflict": {"message": "Name exists"}, - "app.skin-site.admin.mods.upload-success": {"message": "Done"}, - "app.skin-site.admin.mods.upload-error": {"message": "Failed"}, - "app.skin-site.admin.mods.upload-exists": {"message": "This file already exists on the server."}, - "app.skin-site.admin.mods.upload-name-conflict": {"message": "A mod with the same file name already exists"}, - "app.skin-site.admin.mods.upload-name-conflict-hint": {"message": "Overwrite the existing mod, or skip this file."}, - "app.skin-site.admin.mods.upload-overwrite": {"message": "Overwrite"}, - "app.skin-site.admin.mods.upload-skip": {"message": "Skip"}, - "app.skin-site.admin.mods.upload-close": {"message": "Close"}, - "app.skin-site.admin.mods.upload-all-done": {"message": "All files processed."}, - "app.skin-site.admin.mods.upload-too-large": {"message": "File too large: {name}"}, - "app.skin-site.chat.avatar.label": {"message": "Room avatar"}, - "app.skin-site.chat.avatar.upload": {"message": "Upload avatar"}, - "app.skin-site.chat.avatar.replace": {"message": "Replace avatar"}, - "app.skin-site.chat.avatar.remove": {"message": "Remove avatar"}, - "app.skin-site.chat.avatar.hint": {"message": "PNG, JPEG or WebP, up to 8192 px per side and 5 MB."}, - "app.skin-site.chat.avatar.bad-format": {"message": "Only PNG, JPEG and WebP are supported."}, - "app.skin-site.chat.avatar.too-large": {"message": "Avatar must not exceed 5 MB."}, - "app.skin-site.chat.avatar.not-image": {"message": "Avatar must be an image."}, - "app.skin-site.chat.admin.title": {"message": "Chat rooms"}, - "app.skin-site.chat.admin.subtitle": {"message": "Create public or private rooms and hand daily moderation to trusted members."}, - "app.skin-site.chat.admin.create-room": {"message": "Create room"}, - "app.skin-site.chat.admin.loading": {"message": "Loading…"}, - "app.skin-site.chat.admin.failed": {"message": "Could not load rooms."}, - "app.skin-site.chat.admin.empty": {"message": "No chat rooms yet."}, - "app.skin-site.chat.admin.edit": {"message": "Edit"}, - "app.skin-site.chat.admin.remove": {"message": "Delete"}, - "app.skin-site.chat.admin.save": {"message": "Save"}, - "app.skin-site.chat.admin.cancel": {"message": "Cancel"}, - "app.skin-site.chat.admin.name": {"message": "Name"}, - "app.skin-site.chat.admin.description": {"message": "Description"}, - "app.skin-site.chat.admin.visibility": {"message": "Visibility"}, - "app.skin-site.chat.admin.visibility-public": {"message": "Public"}, - "app.skin-site.chat.admin.visibility-private": {"message": "Private"}, - "app.skin-site.chat.admin.kind": {"message": "Kind"}, - "app.skin-site.chat.admin.kind-standard": {"message": "Standard"}, - "app.skin-site.chat.admin.kind-settlement": {"message": "Settlement"}, - "app.skin-site.chat.admin.prerequisite-mode": {"message": "Prerequisite mode"}, - "app.skin-site.chat.admin.prerequisite-any": {"message": "Any"}, - "app.skin-site.chat.admin.prerequisite-all": {"message": "All"}, - "app.skin-site.chat.admin.prerequisite-permissions": {"message": "Required permissions"}, - "app.skin-site.chat.admin.prerequisite-hint": {"message": "Comma separated. Leave empty for no restriction."}, - "app.skin-site.chat.admin.created": {"message": "Room created."}, - "app.skin-site.chat.admin.updated": {"message": "Room updated."}, - "app.skin-site.chat.admin.deleted": {"message": "Room deleted."}, - "app.skin-site.chat.admin.confirm-delete": {"message": "Permanently delete room “{name}” and all its messages?"}, - "app.skin-site.chat.admin.manager-badge": {"message": "Manager"}, - "app.skin-site.chat.admin.voice-badge": {"message": "Voice"}, - "app.skin-site.chat.user-picker.label": {"message": "Pick a user"}, - "app.skin-site.chat.user-picker.placeholder": {"message": "Search username, email or account ID"}, - "app.skin-site.chat.user-picker.by-user": {"message": "By user"}, - "app.skin-site.chat.user-picker.by-player": {"message": "By player"}, - "app.skin-site.chat.user-picker.hint": {"message": "Fuzzy search on username and email, or search by account ID."}, - "app.skin-site.chat.user-picker.player-hint": {"message": "Enter a player name to find the bound user."}, - "app.skin-site.chat.user-picker.empty": {"message": "No matching user."}, - "app.skin-site.chat.user-picker.banned": {"message": "This account is banned."}, - "app.skin-site.chat.user-picker.guest": {"message": "Guests cannot become a manager or settlement owner."}, - "app.skin-site.chat.user-picker.already-in": {"message": "This user is already in the room."}, - "app.skin-site.chat.user-picker.not-allowed": {"message": "This user is outside the allowed range."}, - "app.skin-site.chat.user-picker.select": {"message": "Select"}, - "app.skin-site.chat.user-picker.selected": {"message": "Selected"}, - "app.skin-site.chat.user-picker.id": {"message": "ID"}, - "app.skin-site.chat.admin.managers": {"message": "Managers"}, - "app.skin-site.chat.admin.manager-title": {"message": "Room manager"}, - "app.skin-site.chat.admin.manager-hint": {"message": "Managers can moderate members, messages and files in this room."}, - "app.skin-site.chat.admin.grant": {"message": "Grant"}, - "app.skin-site.chat.admin.revoke": {"message": "Revoke"}, - "app.skin-site.chat.admin.manager-granted": {"message": "Manager granted."}, - "app.skin-site.chat.admin.manager-revoked": {"message": "Manager revoked."}, - "app.skin-site.chat.admin.owner-title": {"message": "Settlement owner"}, - "app.skin-site.chat.admin.owner-hint": {"message": "Assign the first settlement owner for this room."}, - "app.skin-site.chat.admin.owner-assigned": {"message": "Settlement owner assigned."}, - "app.skin-site.chat.admin.owner-current": {"message": "Current owner: {name}"}, - "app.skin-site.chat.admin.owner-none": {"message": "No settlement owner yet."}, - "app.skin-site.chat.admin.audit": {"message": "Audit"}, - "app.skin-site.chat.admin.audit-title": {"message": "Room audit"}, - "app.skin-site.chat.admin.audit-empty": {"message": "No audit records yet."}, - "app.skin-site.chat.admin.audit-target": {"message": "Target: {name}"}, - "app.skin-site.chat.admin.close": {"message": "Close"}, - "app.skin-site.chat.voice-runtime-title": {"message": "Realtime voice runtime"}, - "app.skin-site.chat.voice-logs": {"message": "View logs"}, - "app.skin-site.chat.voice-config": {"message": "Configure runtime"}, - "app.skin-site.chat.voice-retry": {"message": "Retry deployment"}, - "app.skin-site.chat.voice-config-title": {"message": "Voice runtime configuration"}, - "app.skin-site.chat.voice-logs-title": {"message": "LiveKit logs"}, - "app.skin-site.chat.voice-capacity-title": {"message": "Voice capacity"}, - "app.skin-site.chat.voice-capacity": {"message": "Voice capacity"}, - "app.skin-site.chat.participant-limit": {"message": "Participant limit"}, - "app.skin-site.chat.listener-limit": {"message": "Listener limit"}, - "app.skin-site.chat.voice-capacity-updated": {"message": "Voice capacity updated."}, - "app.skin-site.chat.voice-enabled": {"message": "Enable realtime voice"}, - "app.skin-site.chat.voice-runtime-path": {"message": "Runtime directory"}, - "app.skin-site.chat.voice-mirror": {"message": "Mirror base URL"}, - "app.skin-site.chat.voice-public-url": {"message": "Public URL"}, - "app.skin-site.chat.voice-bind": {"message": "Bind address"}, - "app.skin-site.chat.voice-signal-port": {"message": "Signal port"}, - "app.skin-site.chat.voice-rtc-tcp-port": {"message": "Media TCP port"}, - "app.skin-site.chat.voice-rtc-udp-port": {"message": "Media UDP port"}, - "app.skin-site.chat.voice-startup-timeout": {"message": "Startup timeout (s)"}, - "app.skin-site.chat.voice-use-external-ip": {"message": "Use external IP"}, - "app.skin-site.chat.voice-node-ip": {"message": "Node IP"}, - "app.skin-site.chat.voice-saved": {"message": "Voice configuration saved."}, - "app.skin-site.chat.voice-logs-auto-refresh": {"message": "Auto refresh"}, - "app.skin-site.chat.voice-logs-refresh": {"message": "Refresh"}, - "app.skin-site.chat.voice-logs-empty": {"message": "No logs yet."}, - "app.skin-site.chat.admin.voice-runtime-title": {"message": "Realtime voice runtime"}, - "app.skin-site.chat.admin.voice-logs": {"message": "View logs"}, - "app.skin-site.chat.admin.voice-config": {"message": "Configure runtime"}, - "app.skin-site.chat.admin.voice-retry": {"message": "Retry deployment"}, - "app.skin-site.chat.admin.voice-config-title": {"message": "Voice runtime configuration"}, - "app.skin-site.chat.admin.voice-logs-title": {"message": "LiveKit logs"}, - "app.skin-site.chat.admin.voice-capacity-title": {"message": "Voice capacity"}, - "app.skin-site.chat.admin.voice-capacity": {"message": "Voice capacity"}, - "app.skin-site.chat.admin.participant-limit": {"message": "Participant limit"}, - "app.skin-site.chat.admin.listener-limit": {"message": "Listener limit"}, - "app.skin-site.chat.admin.voice-capacity-updated": {"message": "Voice capacity updated."}, - "app.skin-site.chat.admin.voice-enabled": {"message": "Enable realtime voice"}, - "app.skin-site.chat.admin.voice-runtime-path": {"message": "Runtime directory"}, - "app.skin-site.chat.admin.voice-mirror": {"message": "Mirror base URL"}, - "app.skin-site.chat.admin.voice-public-url": {"message": "Public URL"}, - "app.skin-site.chat.admin.voice-bind": {"message": "Bind address"}, - "app.skin-site.chat.admin.voice-signal-port": {"message": "Signal port"}, - "app.skin-site.chat.admin.voice-rtc-tcp-port": {"message": "Media TCP port"}, - "app.skin-site.chat.admin.voice-rtc-udp-port": {"message": "Media UDP port"}, - "app.skin-site.chat.admin.voice-startup-timeout": {"message": "Startup timeout (s)"}, - "app.skin-site.chat.admin.voice-use-external-ip": {"message": "Use external IP"}, - "app.skin-site.chat.admin.voice-node-ip": {"message": "Node IP"}, - "app.skin-site.chat.admin.voice-saved": {"message": "Voice configuration saved."}, - "app.skin-site.chat.admin.voice-logs-auto-refresh": {"message": "Auto refresh"}, - "app.skin-site.chat.admin.voice-logs-refresh": {"message": "Refresh"}, - "app.skin-site.chat.admin.voice-logs-empty": {"message": "No logs yet."}, - "app.skin-site.admin.wiki.tab-migration": {"message": "Migration"}, - "app.skin-site.admin.wiki.migration-title": {"message": "MediaWiki migration"}, - "app.skin-site.admin.wiki.migration-hint": {"message": "Migrated content lands in a hidden staging area and is published in one batch after every page is reviewed."}, - "app.skin-site.admin.wiki.auto-title": {"message": "Read from MediaWiki API"}, - "app.skin-site.admin.wiki.api-url": {"message": "api.php URL"}, - "app.skin-site.admin.wiki.bot-username": {"message": "Bot username (optional)"}, - "app.skin-site.admin.wiki.bot-password": {"message": "Bot password (optional)"}, - "app.skin-site.admin.wiki.credentials-hint": {"message": "Credentials stay in memory for this job only. HTTPS is required when supplied."}, - "app.skin-site.admin.wiki.allow-private": {"message": "Allow private network source"}, - "app.skin-site.admin.wiki.allow-private-hint": {"message": "Private sources may reach services inside the server network."}, - "app.skin-site.admin.wiki.start-auto": {"message": "Create automatic migration job"}, - "app.skin-site.admin.wiki.xml-title": {"message": "Import MediaWiki XML"}, - "app.skin-site.admin.wiki.xml-file": {"message": "Export file"}, - "app.skin-site.admin.wiki.start-xml": {"message": "Upload and stage"}, - "app.skin-site.admin.wiki.stale-rules": {"message": "Stale page detection rules"}, - "app.skin-site.admin.wiki.stale-days": {"message": "Days without update"}, - "app.skin-site.admin.wiki.maintainer-window": {"message": "Maintainer window (days)"}, - "app.skin-site.admin.wiki.max-maintainers": {"message": "Max maintainers"}, - "app.skin-site.admin.wiki.stale-markers": {"message": "Stale categories, templates or text"}, - "app.skin-site.admin.wiki.stale-markers-hint": {"message": "Comma separated"}, - "app.skin-site.admin.wiki.jobs": {"message": "Migration jobs"}, - "app.skin-site.admin.wiki.refresh-jobs": {"message": "Refresh"}, - "app.skin-site.admin.wiki.jobs-empty": {"message": "No migration jobs yet."}, - "app.skin-site.admin.wiki.reviewed-count": {"message": "Reviewed {reviewed} / {total}"}, - "app.skin-site.admin.wiki.imported-excluded": {"message": "Imported {imported} · Excluded {excluded}"}, - "app.skin-site.admin.wiki.view-job": {"message": "View job"}, - "app.skin-site.admin.wiki.delete-job": {"message": "Delete job record"}, - "app.skin-site.admin.wiki.confirm-delete-job": {"message": "Delete migration job #{id}? This cannot be undone."}, - "app.skin-site.admin.wiki.candidates": {"message": "Candidate pages"}, - "app.skin-site.admin.wiki.candidates-empty": {"message": "Select a job to review its pages."}, - "app.skin-site.admin.wiki.last-updated": {"message": "Updated {date}"}, - "app.skin-site.admin.wiki.maintainer-count": {"message": "{count} maintainers"}, - "app.skin-site.admin.wiki.conflict": {"message": "Title conflict"}, - "app.skin-site.admin.wiki.target-title": {"message": "Target title"}, - "app.skin-site.admin.wiki.overwrite": {"message": "Overwrite"}, - "app.skin-site.admin.wiki.import": {"message": "Import"}, - "app.skin-site.admin.wiki.exclude": {"message": "Exclude"}, - "app.skin-site.admin.wiki.publish": {"message": "Publish batch"}, - "app.skin-site.admin.wiki.rollback": {"message": "Roll back"}, - "app.skin-site.admin.wiki.confirm-rollback": {"message": "Roll back this migration batch? Published pages will be reverted."}, - "app.skin-site.admin.wiki.state-importing": {"message": "Importing"}, - "app.skin-site.admin.wiki.state-review": {"message": "Waiting review"}, - "app.skin-site.admin.wiki.state-publishing": {"message": "Publishing"}, - "app.skin-site.admin.wiki.state-published": {"message": "Published"}, - "app.skin-site.admin.wiki.state-failed": {"message": "Failed"}, - "app.skin-site.admin.wiki.state-rolled-back": {"message": "Rolled back"}, - "app.skin-site.admin.wiki.decision-pending": {"message": "Pending"}, - "app.skin-site.admin.wiki.decision-import": {"message": "Import"}, - "app.skin-site.admin.wiki.decision-exclude": {"message": "Exclude"}, - "app.skin-site.admin.wiki.migration-started": {"message": "Migration job created."}, - "app.skin-site.admin.wiki.xml-staged": {"message": "XML parsed and staged for review."}, - "app.skin-site.admin.wiki.published": {"message": "Published {imported} pages, overwrote {overwritten}."}, - "app.skin-site.admin.wiki.rolled-back": {"message": "Migration batch rolled back."}, - "app.skin-site.admin.wiki.job-deleted": {"message": "Migration record deleted."}, - "app.skin-site.admin.wiki.select-xml": {"message": "Choose a MediaWiki XML file first."}, - "app.skin-site.wiki.title": {"message": "StarLight Wiki"}, - "app.skin-site.wiki.search-placeholder": {"message": "Enter a page title"}, - "app.skin-site.wiki.open": {"message": "Open"}, - "app.skin-site.wiki.loading": {"message": "Loading…"}, - "app.skin-site.wiki.not-found": {"message": "Page not found."}, - "app.skin-site.wiki.empty": {"message": "Enter a page title above to start reading."}, - "app.skin-site.wiki.updated-at": {"message": "Last edited {date} by {author}"}, - "app.skin-site.wiki.revision": {"message": "Revision #{id}"}, - "app.skin-site.wiki.categories": {"message": "Categories"}, - "app.skin-site.wiki.back": {"message": "Back"} + "app.skins.armor.title": { "message": "Armour and trims" }, + "app.skins.armor.save": { "message": "Save" }, + "app.skins.armor.close": { "message": "Close" }, + "app.skins.armor.description": { + "message": "Try armour and trims on your skin. Nothing is stored until you save it." + }, + "app.skins.armor.unsaved": { "message": "Unsaved changes" }, + "app.skins.armor.reset-to-saved": { "message": "Reset to saved" }, + "app.skins.armor.restore-defaults": { "message": "Restore defaults" }, + "app.instances.batch-edit-groups.add": { "message": "Add" }, + "app.instances.batch-edit-groups.add-to-group-title": { + "message": "Add instances to \"{groupName}\"" + }, + "app.instances.batch-edit-groups.added": { "message": "Added" }, + "app.instances.batch-edit-groups.cancel": { "message": "Cancel" }, + "app.instances.batch-edit-groups.create": { "message": "Create group" }, + "app.instances.batch-edit-groups.group-name": { "message": "Group name" }, + "app.instances.batch-edit-groups.group-name-placeholder": { + "message": "Enter group name" + }, + "app.instances.batch-edit-groups.no-instances-found": { + "message": "No instances found" + }, + "app.instances.batch-edit-groups.save-changes": { "message": "Save" }, + "app.instances.batch-edit-groups.search-instance": { + "message": "Search instances" + }, + "app.instances.group.add-to-group": { "message": "Add to group" }, + "app.instances.group.collapse": { "message": "Collapse group" }, + "app.instances.group.delete-description": { + "message": "Instances in this group will be ungrouped." + }, + "app.instances.group.delete-group": { "message": "Delete group" }, + "app.instances.group.edit-name": { "message": "Edit group name" }, + "app.instances.group.empty": { "message": "Drag and drop to add instances." }, + "app.instances.group.expand": { "message": "Expand group" }, + "app.instances.group.move-down": { "message": "Move group down" }, + "app.instances.group.move-up": { "message": "Move group up" }, + "app.instances.group.name-empty": { + "message": "Group names cannot be empty." + }, + "app.instances.group.name-too-long": { + "message": "Group names cannot be longer than {maxLength} characters." + }, + "app.instances.group.new-instance": { "message": "New instance" }, + "app.instances.group.pinned": { "message": "Pinned" }, + "app.instances.group.rename-group": { "message": "Rename group" }, + "app.instances.sort-by": { "message": "Sort by" }, + "app.instances.sort.direction.asc-alphabetical": { "message": "A–Z" }, + "app.instances.sort.direction.asc-date": { "message": "Oldest first" }, + "app.instances.sort.direction.asc-recency": { + "message": "Least recent first" + }, + "app.instances.sort.direction.asc-version": { + "message": "Oldest version first" + }, + "app.instances.sort.direction.desc-alphabetical": { "message": "Z–A" }, + "app.instances.sort.direction.desc-date": { "message": "Newest first" }, + "app.instances.sort.direction.desc-recency": { + "message": "Most recent first" + }, + "app.instances.sort.direction.desc-version": { + "message": "Newest version first" + }, + "app.library.context-menu.pin-instance": { "message": "Pin instance" }, + "app.library.context-menu.remove-from-group": { + "message": "Remove from group" + }, + "app.library.context-menu.unpin-instance": { "message": "Unpin instance" }, + "app.library.group.new": { "message": "New group" }, + "app.library.selection.new-group": { "message": "New group" }, + "app.library.selection.remove-from-group": { "message": "Remove from group" }, + "app.log-share.settings.title": { "message": "Log sharing & AI analysis" }, + "app.log-share.settings.description": { + "message": "Share crash diagnostics with LogShare for a structured summary and optional AI analysis. LogShare is preferred; mclo.gs is used as a fallback." + }, + "app.log-share.settings.share-provider": { "message": "Log sharing service" }, + "app.log-share.settings.share-provider-description": { + "message": "Where the diagnostic link is uploaded when you share it." + }, + "app.log-share.settings.ai-source": { "message": "AI analysis source" }, + "app.log-share.settings.ai-source-logshare": { "message": "LogShare AI" }, + "app.log-share.settings.ai-source-custom": { + "message": "Custom AI provider" + }, + "app.log-share.settings.ai-source-description": { + "message": "Both sources send a sanitized and shortened crash context. AI output may be inaccurate." + }, + "app.log-share.settings.auto-upload": { + "message": "Upload to get a structured summary automatically" + }, + "app.log-share.settings.auto-upload-description": { + "message": "Only effective in LogShare AI mode." + }, + "app.log-share.settings.multi-file": { "message": "Multi-file upload" }, + "app.log-share.settings.multi-file-description": { + "message": "Upload latest.log, crash reports, hs_err and launcher logs together." + }, + "app.log-share.settings.no-storage": { + "message": "Analyze without storing logs" + }, + "app.log-share.settings.no-storage-description": { + "message": "Submit content directly for analysis and skip the stored upload. Disables the automatic summary." + }, + "app.log-share.settings.show-progress": { + "message": "Show AI analysis progress" + }, + "app.log-share.settings.show-progress-description": { + "message": "Display thinking, tool and limit status while LogShare AI is working." + }, + "app.log-share.shared-logs.title": { "message": "Shared logs" }, + "app.log-share.shared-logs.description": { + "message": "Every diagnostic you have shared is listed here. Delete an entry to remove it from your history and request its removal from the sharing service." + }, + "app.log-share.shared-logs.empty": { "message": "No shared logs yet." }, + "app.log-share.shared-logs.delete": { "message": "Delete" }, + "app.log-share.shared-logs.deleting": { "message": "Deleting…" }, + "app.log-share.shared-logs.deleted": { "message": "Shared log removed." }, + "app.log-share.shared-logs.delete-failed": { + "message": "Failed to delete the shared log." + }, + "app.log-share.shared-logs.truncated": { "message": "Truncated" }, + "app.minecraft-crash.share-fallback": { + "message": "LogShare is unavailable, so the diagnostic was shared via mclo.gs." + }, + "app.log-share.ai.action": { "message": "Deep AI analysis" }, + "app.log-share.summary.title": { "message": "LogShare analysis" }, + "app.log-share.summary.loading": { + "message": "Loading structured summary..." + }, + "app.log-share.summary.empty": { + "message": "LogShare did not return a structured summary for these logs." + }, + "app.log-share.summary.no-storage": { + "message": "Structured summary is unavailable while no-storage mode is enabled." + }, + "app.log-share.summary.failed": { + "message": "Could not load the LogShare summary: {message}" + }, + "app.log-share.summary.problems": { "message": "Problems" }, + "app.log-share.summary.information": { "message": "Information" }, + "app.minecraft-crash.logs.loading": { "message": "Loading crash logs..." }, + "app.minecraft-crash.logs.empty": { + "message": "No logs from this launch were found." + }, + "app.minecraft-crash.logs.failed": { + "message": "Could not load the crash logs: {message}" + }, + "app.minecraft-crash.log-file.loading": { "message": "Loading log file..." }, + "app.minecraft-crash.log-file.failed": { + "message": "Could not load this log file: {message}" + }, + "app.log-share.ai.tab": { "message": "LogAgent detailed analysis" }, + "app.log-share.ai.root-cause": { "message": "Core root cause" }, + "app.log-share.ai.confidence": { "message": "Diagnostic confidence" }, + "app.log-share.ai.steps": { "message": "Recommended troubleshooting steps" }, + "app.log-share.ai.evidence": { "message": "Supporting evidence" }, + "app.log-share.ai.thinking": { "message": "Thinking…" }, + "app.log-share.ai.queued": { "message": "Queued, about {position} ahead" }, + "app.log-share.ai.queued-front": { + "message": "Queued, waiting for a free slot" + }, + "app.log-share.ai.tool": { "message": "Using tool: {name}" }, + "app.log-share.ai.unknown-tool": { "message": "tool" }, + "app.log-share.ai.tool-result": { "message": "Tool result received" }, + "app.log-share.ai.limit": { "message": "Reached tool loop limit" }, + "app.log-share.ai.working": { "message": "Analyzing with LogAgent…" }, + "app.log-share.ai.failed": { + "message": "LogAgent analysis failed: {message}" + }, + "app.shortcut-settings.change-shortcut": { + "message": "Change the shortcut for {action}" + }, + "app.shortcut-settings.enable": { "message": "Enable quick scrolling" }, + "app.shortcut-settings.end-action": { "message": "Scroll to the bottom" }, + "app.shortcut-settings.end-action-description": { + "message": "Moves the page you are reading to the bottom." + }, + "app.shortcut-settings.home-action": { "message": "Scroll to the top" }, + "app.shortcut-settings.home-action-description": { + "message": "Moves the page you are reading to the top." + }, + "app.shortcut-settings.nav-create": { "message": "Create new instance" }, + "app.shortcut-settings.nav-create-description": { + "message": "Jump to creating a new instance." + }, + "app.shortcut-settings.nav-description": { + "message": "Jump to a menu item with its shortcut. Each shortcut is off until enabled." + }, + "app.shortcut-settings.nav-discover": { "message": "Discover content" }, + "app.shortcut-settings.nav-discover-description": { + "message": "Jump to browsing content." + }, + "app.shortcut-settings.nav-downloads": { "message": "Downloads" }, + "app.shortcut-settings.nav-downloads-description": { + "message": "Jump to downloads." + }, + "app.shortcut-settings.nav-home": { "message": "Home" }, + "app.shortcut-settings.nav-home-description": { + "message": "Jump to the Home page." + }, + "app.shortcut-settings.nav-lab": { "message": "Lab" }, + "app.shortcut-settings.nav-lab-description": { + "message": "Jump to the Lab." + }, + "app.shortcut-settings.nav-library": { "message": "Library" }, + "app.shortcut-settings.nav-library-description": { + "message": "Jump to your library." + }, + "app.shortcut-settings.nav-multiplayer": { "message": "Multiplayer" }, + "app.shortcut-settings.nav-multiplayer-description": { + "message": "Jump to multiplayer." + }, + "app.shortcut-settings.nav-settings": { "message": "Settings" }, + "app.shortcut-settings.nav-settings-description": { + "message": "Jump to the settings." + }, + "app.shortcut-settings.nav-skins": { "message": "Skin selector" }, + "app.shortcut-settings.nav-skins-description": { + "message": "Jump to the skin selector." + }, + "app.shortcut-settings.nav-title": { "message": "Navigation shortcuts" }, + "app.shortcut-settings.nav-worlds": { "message": "Worlds" }, + "app.shortcut-settings.nav-worlds-description": { + "message": "Jump to your worlds." + }, + "app.shortcut-settings.page-down-action": { + "message": "Scroll down one screen" + }, + "app.shortcut-settings.page-down-action-description": { + "message": "Moves the page you are reading down by one screen." + }, + "app.shortcut-settings.page-up-action": { "message": "Scroll up one screen" }, + "app.shortcut-settings.page-up-action-description": { + "message": "Moves the page you are reading up by one screen." + }, + "app.shortcut-settings.scroll-description": { + "message": "Move through long pages with the keyboard." + }, + "app.shortcut-settings.scroll-title": { "message": "Quick scrolling" }, + "app.settings.tabs.shortcut-settings": { "message": "Keyboard shortcuts" }, + "app.skin-site.bugs.title": { "message": "Bugs & suggestions" }, + "app.skin-site.bugs.back": { "message": "Back to home" }, + "app.skin-site.bugs.subtitle": { + "message": "Report issues and follow their progress." + }, + "app.skin-site.bugs.loading": { "message": "Loading…" }, + "app.skin-site.bugs.failed": { "message": "Could not load issues." }, + "app.skin-site.bugs.empty": { "message": "No issues match these filters." }, + "app.skin-site.bugs.all": { "message": "All" }, + "app.skin-site.bugs.more": { "message": "Load more" }, + "app.skin-site.bugs.open-only": { "message": "Open only" }, + "app.skin-site.bugs.comments": { "message": "{count} comments" }, + "app.skin-site.bugs.type.bug": { "message": "Bug" }, + "app.skin-site.bugs.type.feature": { "message": "Feature" }, + "app.skin-site.bugs.type.improvement": { "message": "Improvement" }, + "app.skin-site.bugs.status.pending": { "message": "Pending" }, + "app.skin-site.bugs.status.confirmed": { "message": "Confirmed" }, + "app.skin-site.bugs.status.needs-info": { "message": "Needs info" }, + "app.skin-site.bugs.status.in-progress": { "message": "In progress" }, + "app.skin-site.bugs.status.verifying": { "message": "Verifying" }, + "app.skin-site.bugs.status.closed": { "message": "Closed" }, + "app.skin-site.bugs.priority.critical": { "message": "Critical" }, + "app.skin-site.bugs.priority.high": { "message": "High" }, + "app.skin-site.bugs.priority.normal": { "message": "Normal" }, + "app.skin-site.bugs.priority.low": { "message": "Low" }, + "app.skin-site.bugs.resolution.fixed": { "message": "Fixed" }, + "app.skin-site.bugs.resolution.wont-fix": { "message": "Won’t fix" }, + "app.skin-site.bugs.resolution.by-design": { "message": "By design" }, + "app.skin-site.bugs.resolution.duplicate": { "message": "Duplicate" }, + "app.skin-site.bugs.resolution.cannot-reproduce": { + "message": "Cannot reproduce" + }, + "app.skin-site.bugs.resolution.invalid": { "message": "Invalid" }, + "app.skin-site.bug-detail.back": { "message": "Back to list" }, + "app.skin-site.bug-detail.loading": { "message": "Loading…" }, + "app.skin-site.bug-detail.failed": { + "message": "Could not load this issue." + }, + "app.skin-site.bug-detail.issue-no": { "message": "Issue #{id}" }, + "app.skin-site.bug-detail.by": { "message": "by {name}" }, + "app.skin-site.bug-detail.assignee": { "message": "Assignee: {name}" }, + "app.skin-site.bug-detail.unassigned": { "message": "Unassigned" }, + "app.skin-site.bug-detail.description-empty": { + "message": "No description provided." + }, + "app.skin-site.bug-detail.resolution": { "message": "Resolution: {name}" }, + "app.skin-site.bug-detail.comments-title": { "message": "Comments" }, + "app.skin-site.bug-detail.comments-empty": { + "message": "No comments yet. Be the first to reply." + }, + "app.skin-site.bug-detail.comments-loading": { + "message": "Loading comments…" + }, + "app.skin-site.bug-detail.comment-placeholder": { + "message": "Write a comment…" + }, + "app.skin-site.bug-detail.comment-submit": { "message": "Comment" }, + "app.skin-site.bug-detail.comment-sending": { "message": "Sending…" }, + "app.skin-site.bug-detail.comment-empty-error": { + "message": "Comment cannot be empty." + }, + "app.skin-site.bug-detail.comment-posted": { "message": "Comment posted." }, + "app.skin-site.bug-detail.comment-deleted": { "message": "Comment deleted." }, + "app.skin-site.bug-detail.delete": { "message": "Delete" }, + "app.skin-site.bug-detail.deleting": { "message": "Deleting…" }, + "app.skin-site.bug-detail.system": { "message": "System" }, + "app.skin-site.bug-create.title": { "message": "New issue" }, + "app.skin-site.bug-create.type": { "message": "Type" }, + "app.skin-site.bug-create.title-field": { "message": "Title" }, + "app.skin-site.bug-create.title-placeholder": { + "message": "A short summary of the problem" + }, + "app.skin-site.bug-create.description": { "message": "Description" }, + "app.skin-site.bug-create.description-placeholder": { + "message": "What happened, and how can it be reproduced?" + }, + "app.skin-site.bug-create.private": { "message": "Private issue" }, + "app.skin-site.bug-create.private-hint": { + "message": "Only you and the maintainers can see it." + }, + "app.skin-site.bug-create.images": { "message": "Images" }, + "app.skin-site.bug-create.images-hint": { + "message": "Optional, up to 5 images." + }, + "app.skin-site.bug-create.submit": { "message": "Create issue" }, + "app.skin-site.bug-create.submitting": { "message": "Creating…" }, + "app.skin-site.bug-create.created": { "message": "Issue created." }, + "app.skin-site.bug-create.cancel": { "message": "Cancel" }, + "app.skin-site.bug-notifications.title": { "message": "Notifications" }, + "app.skin-site.bug-notifications.back": { "message": "Back to home" }, + "app.skin-site.bug-notifications.subtitle": { + "message": "Activity on issues you reported or follow." + }, + "app.skin-site.bug-notifications.loading": { "message": "Loading…" }, + "app.skin-site.bug-notifications.failed": { + "message": "Could not load notifications." + }, + "app.skin-site.bug-notifications.empty": { + "message": "No new notifications." + }, + "app.skin-site.bug-notifications.item": { + "message": "{name} updated issue #{id}" + }, + "app.skin-site.bug-notifications.mark-all": { "message": "Mark all as read" }, + "app.skin-site.bug-notifications.marked-all": { + "message": "All notifications marked as read." + }, + "app.skin-site.bugs.create": { "message": "New issue" }, + "app.skin-site.bugs.notifications": { "message": "Notifications" }, + "app.skin-site.home.title": { "message": "StarLight Skin Site" }, + "app.skin-site.home.subtitle": { + "message": "The wardrobe for StarLight Server players." + }, + "app.skin-site.home.address": { "message": "Skin site address" }, + "app.skin-site.home.copied": { "message": "Address copied." }, + "app.skin-site.home.copy": { "message": "Copy" }, + "app.skin-site.admin.title": { "message": "Admin console" }, + "app.skin-site.admin.back": { "message": "Back to home" }, + "app.skin-site.admin.no-access": { "message": "You do not have access." }, + "app.skin-site.admin.no-access-hint": { + "message": "Sign in with an administrator account to use this console." + }, + "app.skin-site.admin.group.security": { "message": "Security" }, + "app.skin-site.admin.group.users": { "message": "Users" }, + "app.skin-site.admin.group.community": { "message": "Community" }, + "app.skin-site.admin.group.resources": { "message": "Resources" }, + "app.skin-site.admin.item.servers": { "message": "Servers" }, + "app.skin-site.admin.item.summary": { "message": "Summary" }, + "app.skin-site.admin.item.access-audit": { "message": "Access audit" }, + "app.skin-site.admin.item.users": { "message": "Users" }, + "app.skin-site.admin.item.players": { "message": "Players" }, + "app.skin-site.admin.item.review": { "message": "Player review" }, + "app.skin-site.admin.item.broadcast": { "message": "Broadcast" }, + "app.skin-site.admin.item.chat": { "message": "Chat rooms" }, + "app.skin-site.admin.item.wiki": { "message": "Wiki" }, + "app.skin-site.admin.item.donations": { "message": "Donations" }, + "app.skin-site.admin.item.mods": { "message": "Mods" }, + "app.skin-site.admin.item.launcher": { "message": "Launcher" }, + "app.skin-site.admin.item.downloads": { "message": "Downloads" }, + "app.skin-site.home.admin": { "message": "Admin" }, + "app.skin-site.home.settings": { "message": "Account" }, + "app.skin-site.home.sign-in": { "message": "Sign in" }, + "app.skin-site.home.players": { "message": "Players" }, + "app.skin-site.home.my-players": { "message": "My players" }, + "app.skin-site.home.no-players": { "message": "No players linked yet." }, + "app.skin-site.home.loading": { "message": "Loading…" }, + "app.skin-site.home.error": { "message": "Could not load." }, + "app.skin-site.home.luck": { "message": "Luck" }, + "app.skin-site.home.luck-unavailable": { "message": "Unavailable" }, + "app.skin-site.home.signed-out-title": { "message": "Not signed in" }, + "app.skin-site.home.signed-out-body": { + "message": "Sign in to manage your account and textures." + }, + "app.skin-site.admin.summary.title": { "message": "System overview" }, + "app.skin-site.admin.summary.loading": { "message": "Loading…" }, + "app.skin-site.admin.summary.failed": { "message": "Could not load status." }, + "app.skin-site.admin.summary.uptime": { "message": "Uptime" }, + "app.skin-site.admin.summary.memory": { "message": "Memory usage" }, + "app.skin-site.admin.summary.users": { "message": "Users" }, + "app.skin-site.admin.summary.players": { "message": "Players" }, + "app.skin-site.admin.summary.realtime-hint": { + "message": "Live CPU and GC charts require the admin WebSocket, which is not connected yet." + }, + "app.skin-site.admin.broadcast.title": { "message": "Announcements" }, + "app.skin-site.admin.broadcast.loading": { "message": "Loading…" }, + "app.skin-site.admin.broadcast.failed": { "message": "Could not load." }, + "app.skin-site.admin.broadcast.empty": { "message": "No announcements yet." }, + "app.skin-site.admin.broadcast.create": { "message": "New announcement" }, + "app.skin-site.admin.broadcast.save": { "message": "Save" }, + "app.skin-site.admin.broadcast.cancel": { "message": "Cancel" }, + "app.skin-site.admin.broadcast.edit": { "message": "Edit" }, + "app.skin-site.admin.broadcast.remove": { "message": "Delete" }, + "app.skin-site.admin.broadcast.title-field": { "message": "Title" }, + "app.skin-site.admin.broadcast.subtitle-field": { "message": "Subtitle" }, + "app.skin-site.admin.broadcast.content-field": { "message": "Content" }, + "app.skin-site.admin.broadcast.expire-at": { "message": "Expires at" }, + "app.skin-site.admin.broadcast.star": { "message": "Highlight" }, + "app.skin-site.admin.broadcast.level": { "message": "Importance" }, + "app.skin-site.admin.broadcast.image": { "message": "Image" }, + "app.skin-site.admin.broadcast.remove-image": { "message": "Remove image" }, + "app.skin-site.admin.broadcast.created": { + "message": "Announcement created." + }, + "app.skin-site.admin.broadcast.updated": { + "message": "Announcement updated." + }, + "app.skin-site.admin.broadcast.deleted": { + "message": "Announcement deleted." + }, + "app.skin-site.admin.broadcast.confirm-delete": { + "message": "Delete “{title}”? This cannot be undone." + }, + "app.skin-site.admin.broadcast.uploader": { "message": "by {name}" }, + "app.skin-site.admin.players.title": { "message": "Players" }, + "app.skin-site.admin.players.loading": { "message": "Loading…" }, + "app.skin-site.admin.players.failed": { + "message": "Could not load players." + }, + "app.skin-site.admin.players.empty": { "message": "No players found." }, + "app.skin-site.admin.players.search": { "message": "Search" }, + "app.skin-site.admin.players.search-name": { "message": "Name" }, + "app.skin-site.admin.players.search-owner": { "message": "Owner" }, + "app.skin-site.admin.players.search-uuid": { "message": "UUID" }, + "app.skin-site.admin.players.total": { "message": "{count} players" }, + "app.skin-site.admin.players.prev": { "message": "Previous" }, + "app.skin-site.admin.players.next": { "message": "Next" }, + "app.skin-site.admin.players.page": { "message": "Page {page}" }, + "app.skin-site.admin.players.name": { "message": "Name" }, + "app.skin-site.admin.players.owner": { "message": "Owner" }, + "app.skin-site.admin.players.source": { "message": "Source" }, + "app.skin-site.admin.players.legacy": { "message": "Legacy adapted" }, + "app.skin-site.admin.players.actions": { "message": "Actions" }, + "app.skin-site.admin.players.edit": { "message": "Edit" }, + "app.skin-site.admin.players.save": { "message": "Save" }, + "app.skin-site.admin.players.cancel": { "message": "Cancel" }, + "app.skin-site.admin.players.remove": { "message": "Delete" }, + "app.skin-site.admin.players.updated": { "message": "Player updated." }, + "app.skin-site.admin.players.deleted": { "message": "Player deleted." }, + "app.skin-site.admin.players.confirm-delete": { + "message": "Delete player “{name}”? This cannot be undone." + }, + "app.skin-site.admin.players.mojang": { "message": "Mojang" }, + "app.skin-site.admin.players.custom": { "message": "Skin site" }, + "app.skin-site.admin.users.title": { "message": "Users" }, + "app.skin-site.admin.users.loading": { "message": "Loading…" }, + "app.skin-site.admin.users.failed": { "message": "Could not load users." }, + "app.skin-site.admin.users.empty": { "message": "No users found." }, + "app.skin-site.admin.users.search": { "message": "Search" }, + "app.skin-site.admin.users.search-name": { "message": "Username" }, + "app.skin-site.admin.users.search-email": { "message": "Email" }, + "app.skin-site.admin.users.search-uuid": { "message": "UUID" }, + "app.skin-site.admin.users.total": { "message": "{count} users" }, + "app.skin-site.admin.users.prev": { "message": "Previous" }, + "app.skin-site.admin.users.next": { "message": "Next" }, + "app.skin-site.admin.users.page": { "message": "Page {page}" }, + "app.skin-site.admin.users.edit": { "message": "Edit" }, + "app.skin-site.admin.users.save": { "message": "Save" }, + "app.skin-site.admin.users.cancel": { "message": "Cancel" }, + "app.skin-site.admin.users.remove": { "message": "Delete" }, + "app.skin-site.admin.users.players": { "message": "Players" }, + "app.skin-site.admin.users.updated": { "message": "User updated." }, + "app.skin-site.admin.users.deleted": { "message": "User deleted." }, + "app.skin-site.admin.users.confirm-delete": { + "message": "Delete user “{name}”? This cannot be undone." + }, + "app.skin-site.admin.users.username": { "message": "Username" }, + "app.skin-site.admin.users.email": { "message": "Email" }, + "app.skin-site.admin.users.qq": { "message": "QQ" }, + "app.skin-site.admin.users.password": { "message": "New password" }, + "app.skin-site.admin.users.password-hint": { + "message": "Leave empty to keep the current password." + }, + "app.skin-site.admin.users.banned": { "message": "Banned" }, + "app.skin-site.admin.users.permissions": { "message": "Permissions" }, + "app.skin-site.admin.users.permissions-hint": { + "message": "Comma separated, e.g. WEB_ADMIN, SERVER_ADMIN." + }, + "app.skin-site.admin.users.no-players": { "message": "No linked players." }, + "app.skin-site.admin.users.registered": { "message": "Registered" }, + "app.skin-site.admin.review.title": { "message": "Player review" }, + "app.skin-site.admin.review.loading": { "message": "Loading…" }, + "app.skin-site.admin.review.failed": { + "message": "Could not load submissions." + }, + "app.skin-site.admin.review.empty": { "message": "No submissions found." }, + "app.skin-site.admin.review.total": { "message": "{count} submissions" }, + "app.skin-site.admin.review.filter-all": { "message": "All" }, + "app.skin-site.admin.review.filter-pending": { "message": "Pending" }, + "app.skin-site.admin.review.filter-passed": { "message": "Passed" }, + "app.skin-site.admin.review.filter-rejected": { "message": "Rejected" }, + "app.skin-site.admin.review.result-pending": { "message": "Pending" }, + "app.skin-site.admin.review.result-passed": { "message": "Passed" }, + "app.skin-site.admin.review.result-rejected": { "message": "Rejected" }, + "app.skin-site.admin.review.mode-auto": { "message": "Auto" }, + "app.skin-site.admin.review.mode-manual": { "message": "Manual" }, + "app.skin-site.admin.review.applicant": { "message": "Applicant" }, + "app.skin-site.admin.review.submitted-at": { "message": "Submitted" }, + "app.skin-site.admin.review.score": { "message": "Score" }, + "app.skin-site.admin.review.reviewer": { "message": "Reviewer" }, + "app.skin-site.admin.review.details": { "message": "Details" }, + "app.skin-site.admin.review.hide": { "message": "Hide" }, + "app.skin-site.admin.review.answers": { "message": "Answers" }, + "app.skin-site.admin.review.no-answers": { "message": "No answers." }, + "app.skin-site.admin.review.auto-checks": { "message": "Automatic checks" }, + "app.skin-site.admin.review.context": { "message": "Submission context" }, + "app.skin-site.admin.review.ip": { "message": "IP" }, + "app.skin-site.admin.review.user-agent": { "message": "User agent" }, + "app.skin-site.admin.review.activity": { "message": "Activity" }, + "app.skin-site.admin.review.duration": { "message": "Duration" }, + "app.skin-site.admin.review.hidden-time": { "message": "Hidden time" }, + "app.skin-site.admin.review.screen-switches": { + "message": "Screen switches" + }, + "app.skin-site.admin.review.history": { "message": "Decision history" }, + "app.skin-site.admin.review.accept": { "message": "Accept" }, + "app.skin-site.admin.review.reject": { "message": "Reject" }, + "app.skin-site.admin.review.reason": { "message": "Reason" }, + "app.skin-site.admin.review.accept-done": { + "message": "Submission accepted." + }, + "app.skin-site.admin.review.reject-done": { + "message": "Submission rejected." + }, + "app.skin-site.admin.review.regen-invite": { "message": "Regenerate invite" }, + "app.skin-site.admin.review.invite-regenerated": { + "message": "Invite code regenerated." + }, + "app.skin-site.admin.review.invite-sent": { "message": "Email sent." }, + "app.skin-site.admin.review.invite-not-sent": { + "message": "Email not sent." + }, + "app.skin-site.admin.review.prev": { "message": "Previous" }, + "app.skin-site.admin.review.next": { "message": "Next" }, + "app.skin-site.admin.review.page": { "message": "Page {page}" }, + "app.skin-site.admin.launcher.title": { "message": "Launcher releases" }, + "app.skin-site.admin.launcher.loading": { "message": "Loading…" }, + "app.skin-site.admin.launcher.failed": { + "message": "Could not load releases." + }, + "app.skin-site.admin.launcher.empty": { + "message": "No releases uploaded yet." + }, + "app.skin-site.admin.launcher.latest-url": { "message": "Latest URL" }, + "app.skin-site.admin.launcher.version": { "message": "Version" }, + "app.skin-site.admin.launcher.channel": { "message": "Channel" }, + "app.skin-site.admin.launcher.uploader": { "message": "Uploaded by" }, + "app.skin-site.admin.launcher.uploaded-at": { "message": "Uploaded" }, + "app.skin-site.admin.launcher.published-at": { "message": "Published" }, + "app.skin-site.admin.launcher.force-update": { "message": "Forced" }, + "app.skin-site.admin.launcher.active": { "message": "Active" }, + "app.skin-site.admin.launcher.activate": { "message": "Activate" }, + "app.skin-site.admin.launcher.activated": { "message": "Release activated." }, + "app.skin-site.admin.launcher.upload": { "message": "Upload release" }, + "app.skin-site.admin.launcher.upload-hint": { + "message": "Choose a release ZIP (max 4 GiB)." + }, + "app.skin-site.admin.launcher.uploading": { "message": "Uploading…" }, + "app.skin-site.admin.launcher.upload-done": { + "message": "Release uploaded." + }, + "app.skin-site.admin.launcher.force-flag": { "message": "Force update" }, + "app.skin-site.admin.launcher.prev": { "message": "Previous" }, + "app.skin-site.admin.launcher.next": { "message": "Next" }, + "app.skin-site.admin.launcher.page": { "message": "Page {page}" }, + "app.skin-site.admin.launcher.total": { "message": "{count} releases" }, + "app.skin-site.admin.downloads.title": { "message": "Downloads" }, + "app.skin-site.admin.downloads.loading": { "message": "Loading…" }, + "app.skin-site.admin.downloads.failed": { + "message": "Could not load downloads." + }, + "app.skin-site.admin.downloads.empty": { "message": "No download items." }, + "app.skin-site.admin.downloads.categories": { "message": "Categories" }, + "app.skin-site.admin.downloads.all": { "message": "All items" }, + "app.skin-site.admin.downloads.add-category": { "message": "Add" }, + "app.skin-site.admin.downloads.category-name": { "message": "Category name" }, + "app.skin-site.admin.downloads.remove-category": { "message": "Remove" }, + "app.skin-site.admin.downloads.total": { "message": "{count} items" }, + "app.skin-site.admin.downloads.add-item": { "message": "New item" }, + "app.skin-site.admin.downloads.save": { "message": "Save" }, + "app.skin-site.admin.downloads.cancel": { "message": "Cancel" }, + "app.skin-site.admin.downloads.edit": { "message": "Edit" }, + "app.skin-site.admin.downloads.remove": { "message": "Delete" }, + "app.skin-site.admin.downloads.filename": { "message": "File name" }, + "app.skin-site.admin.downloads.description": { "message": "Description" }, + "app.skin-site.admin.downloads.routes": { "message": "Download routes" }, + "app.skin-site.admin.downloads.route-name": { "message": "Name" }, + "app.skin-site.admin.downloads.route-url": { "message": "URL" }, + "app.skin-site.admin.downloads.add-route": { "message": "Add route" }, + "app.skin-site.admin.downloads.remove-route": { "message": "Remove" }, + "app.skin-site.admin.downloads.statuses": { "message": "Statuses" }, + "app.skin-site.admin.downloads.bind-category": { "message": "Categories" }, + "app.skin-site.admin.downloads.uploader": { "message": "by {name}" }, + "app.skin-site.admin.downloads.created": { "message": "Item created." }, + "app.skin-site.admin.downloads.updated": { "message": "Item updated." }, + "app.skin-site.admin.downloads.deleted": { "message": "Item deleted." }, + "app.skin-site.admin.downloads.category-added": { + "message": "Category added." + }, + "app.skin-site.admin.downloads.category-deleted": { + "message": "Category deleted." + }, + "app.skin-site.admin.downloads.confirm-delete-item": { + "message": "Delete “{name}”? This cannot be undone." + }, + "app.skin-site.admin.downloads.confirm-delete-category": { + "message": "Delete category “{name}”?" + }, + "app.skin-site.admin.servers.title": { "message": "Map servers" }, + "app.skin-site.admin.servers.loading": { "message": "Loading…" }, + "app.skin-site.admin.servers.failed": { + "message": "Could not load servers." + }, + "app.skin-site.admin.servers.empty": { + "message": "No map servers configured." + }, + "app.skin-site.admin.servers.server-name": { "message": "Server name" }, + "app.skin-site.admin.servers.internal": { "message": "Internal" }, + "app.skin-site.admin.servers.maps": { "message": "Maps" }, + "app.skin-site.admin.servers.map-name": { "message": "Map name" }, + "app.skin-site.admin.servers.map-url": { "message": "Map URL" }, + "app.skin-site.admin.servers.add-map": { "message": "Add map" }, + "app.skin-site.admin.servers.remove-map": { "message": "Remove" }, + "app.skin-site.admin.servers.add-server": { "message": "Add server" }, + "app.skin-site.admin.servers.remove-server": { "message": "Remove server" }, + "app.skin-site.admin.servers.save": { "message": "Save changes" }, + "app.skin-site.admin.servers.saving": { "message": "Saving…" }, + "app.skin-site.admin.servers.saved": { "message": "Map servers saved." }, + "app.skin-site.admin.audit.title": { "message": "Access audit" }, + "app.skin-site.admin.audit.loading": { "message": "Loading…" }, + "app.skin-site.admin.audit.failed": { + "message": "Could not load audit log." + }, + "app.skin-site.admin.audit.empty": { "message": "No audit entries." }, + "app.skin-site.admin.audit.subject": { "message": "Subject" }, + "app.skin-site.admin.audit.action": { "message": "Action" }, + "app.skin-site.admin.audit.source-ip": { "message": "Source IP" }, + "app.skin-site.admin.audit.reported-ip": { "message": "Reported player IP" }, + "app.skin-site.admin.audit.count": { "message": "Count" }, + "app.skin-site.admin.audit.first-at": { "message": "First seen" }, + "app.skin-site.admin.audit.last-at": { "message": "Last seen" }, + "app.skin-site.admin.audit.proxy": { "message": "Proxy" }, + "app.skin-site.admin.audit.internal": { "message": "Internal" }, + "app.skin-site.admin.audit.search-subject": { "message": "Subject" }, + "app.skin-site.admin.audit.search-ip": { "message": "IP" }, + "app.skin-site.admin.audit.search-action": { "message": "Action" }, + "app.skin-site.admin.audit.search": { "message": "Search" }, + "app.skin-site.admin.audit.clear": { "message": "Clear" }, + "app.skin-site.admin.audit.load-more": { "message": "Load more" }, + "app.skin-site.admin.audit.retention": { "message": "Retention (days)" }, + "app.skin-site.admin.audit.record-limit": { "message": "Record limit" }, + "app.skin-site.admin.audit.save-retention": { "message": "Save" }, + "app.skin-site.admin.audit.retention-saved": { + "message": "Retention updated." + }, + "app.skin-site.admin.donations.title": { "message": "Donations" }, + "app.skin-site.admin.donations.loading": { "message": "Loading…" }, + "app.skin-site.admin.donations.failed": { + "message": "Could not load donations." + }, + "app.skin-site.admin.donations.empty": { "message": "No donations yet." }, + "app.skin-site.admin.donations.total-amount": { "message": "Total" }, + "app.skin-site.admin.donations.donor-count": { "message": "Donors" }, + "app.skin-site.admin.donations.donor": { "message": "Donor" }, + "app.skin-site.admin.donations.amount": { "message": "Amount" }, + "app.skin-site.admin.donations.updated-at": { "message": "Updated" }, + "app.skin-site.admin.donations.external": { "message": "External" }, + "app.skin-site.admin.donations.linked": { "message": "Linked" }, + "app.skin-site.admin.donations.create": { "message": "Add donation" }, + "app.skin-site.admin.donations.save": { "message": "Save" }, + "app.skin-site.admin.donations.cancel": { "message": "Cancel" }, + "app.skin-site.admin.donations.edit": { "message": "Edit" }, + "app.skin-site.admin.donations.remove": { "message": "Delete" }, + "app.skin-site.admin.donations.amount-field": { "message": "Amount" }, + "app.skin-site.admin.donations.display-name": { "message": "Display name" }, + "app.skin-site.admin.donations.user-uuid": { "message": "User UUID" }, + "app.skin-site.admin.donations.qq": { "message": "QQ" }, + "app.skin-site.admin.donations.append-mode": { + "message": "Append to existing amount" + }, + "app.skin-site.admin.donations.created": { "message": "Donation created." }, + "app.skin-site.admin.donations.updated": { "message": "Donation updated." }, + "app.skin-site.admin.donations.deleted": { "message": "Donation deleted." }, + "app.skin-site.admin.donations.confirm-delete": { + "message": "Delete the donation from “{name}”? This cannot be undone." + }, + "app.skin-site.admin.donations.load-more": { "message": "Load more" }, + "app.skin-site.admin.mods.title": { "message": "Mods" }, + "app.skin-site.admin.mods.tab-mods": { "message": "Mod files" }, + "app.skin-site.admin.mods.tab-tags": { "message": "Rule tags" }, + "app.skin-site.admin.mods.tab-runtime": { "message": "Runtime" }, + "app.skin-site.admin.mods.loading": { "message": "Loading…" }, + "app.skin-site.admin.mods.failed": { "message": "Could not load mods." }, + "app.skin-site.admin.mods.empty": { "message": "No mods found." }, + "app.skin-site.admin.mods.total": { "message": "{count} mods" }, + "app.skin-site.admin.mods.search": { "message": "Search by name" }, + "app.skin-site.admin.mods.filter-all": { "message": "All tags" }, + "app.skin-site.admin.mods.prev": { "message": "Previous" }, + "app.skin-site.admin.mods.next": { "message": "Next" }, + "app.skin-site.admin.mods.page": { "message": "Page {page}" }, + "app.skin-site.admin.mods.name": { "message": "Name" }, + "app.skin-site.admin.mods.version": { "message": "Version" }, + "app.skin-site.admin.mods.mod-id": { "message": "Mod ID" }, + "app.skin-site.admin.mods.size": { "message": "Size" }, + "app.skin-site.admin.mods.loaders": { "message": "Loaders" }, + "app.skin-site.admin.mods.domain-rules": { "message": "Domain rules" }, + "app.skin-site.admin.mods.edit": { "message": "Edit" }, + "app.skin-site.admin.mods.save": { "message": "Save" }, + "app.skin-site.admin.mods.cancel": { "message": "Cancel" }, + "app.skin-site.admin.mods.remove": { "message": "Delete" }, + "app.skin-site.admin.mods.updated": { "message": "Mod updated." }, + "app.skin-site.admin.mods.deleted": { "message": "Mod deleted." }, + "app.skin-site.admin.mods.confirm-delete": { + "message": "Delete mod “{name}”? This cannot be undone." + }, + "app.skin-site.admin.mods.rule-tags": { "message": "Rule tags" }, + "app.skin-site.admin.mods.tag-name": { "message": "Tag name" }, + "app.skin-site.admin.mods.tag-rules": { "message": "Rules" }, + "app.skin-site.admin.mods.tag-rules-hint": { + "message": "One rule per line." + }, + "app.skin-site.admin.mods.new-tag": { "message": "New tag" }, + "app.skin-site.admin.mods.tag-created": { "message": "Tag created." }, + "app.skin-site.admin.mods.tag-updated": { "message": "Tag updated." }, + "app.skin-site.admin.mods.tag-deleted": { "message": "Tag deleted." }, + "app.skin-site.admin.mods.confirm-delete-tag": { + "message": "Delete tag “{name}”?" + }, + "app.skin-site.admin.mods.runtime": { "message": "Runtime releases" }, + "app.skin-site.admin.mods.manifest-url": { "message": "Manifest URL" }, + "app.skin-site.admin.mods.upload-runtime": { "message": "Upload runtime" }, + "app.skin-site.admin.mods.runtime-version": { "message": "Version" }, + "app.skin-site.admin.mods.uploading": { "message": "Uploading…" }, + "app.skin-site.admin.mods.runtime-uploaded": { + "message": "Runtime uploaded." + }, + "app.skin-site.admin.mods.activate": { "message": "Activate" }, + "app.skin-site.admin.mods.activated": { "message": "Runtime activated." }, + "app.skin-site.admin.mods.available": { "message": "Available" }, + "app.skin-site.admin.mods.unavailable": { "message": "Unavailable" }, + "app.skin-site.admin.mods.active": { "message": "Active" }, + "app.skin-site.admin.mods.uploader": { "message": "by {name}" }, + "app.skin-site.home.nav.title": { "message": "Skin site" }, + "app.skin-site.home.nav.download": { "message": "Downloads" }, + "app.skin-site.home.nav.download-desc": { + "message": "Browse files shared by the server." + }, + "app.skin-site.home.nav.review": { "message": "Player review" }, + "app.skin-site.home.nav.review-desc": { + "message": "Submit and review player applications." + }, + "app.skin-site.home.nav.wiki": { "message": "Server wiki" }, + "app.skin-site.home.nav.wiki-desc": { + "message": "Guides and documentation for the server." + }, + "app.skin-site.home.nav.bugs": { "message": "Bug tracker" }, + "app.skin-site.home.nav.bugs-desc": { + "message": "Report issues and follow their progress." + }, + "app.skin-site.home.nav.map": { "message": "Server map" }, + "app.skin-site.home.nav.map-desc": { + "message": "Explore the live world map." + }, + "app.skin-site.home.nav.profile": { "message": "Profile" }, + "app.skin-site.home.nav.profile-desc": { + "message": "Manage your account and textures." + }, + "app.skin-site.admin.wiki.title": { "message": "Wiki" }, + "app.skin-site.admin.wiki.tab-page": { "message": "Page" }, + "app.skin-site.admin.wiki.tab-audit": { "message": "Audit log" }, + "app.skin-site.admin.wiki.tab-settings": { "message": "Settings" }, + "app.skin-site.admin.wiki.loading": { "message": "Loading…" }, + "app.skin-site.admin.wiki.page-path": { "message": "Page path" }, + "app.skin-site.admin.wiki.load-page": { "message": "Load page" }, + "app.skin-site.admin.wiki.page-not-found": { "message": "Page not found." }, + "app.skin-site.admin.wiki.revision": { "message": "Revision" }, + "app.skin-site.admin.wiki.author": { "message": "Author" }, + "app.skin-site.admin.wiki.updated-at": { "message": "Updated" }, + "app.skin-site.admin.wiki.categories": { "message": "Categories" }, + "app.skin-site.admin.wiki.read-policy": { "message": "Read policy" }, + "app.skin-site.admin.wiki.edit-policy": { "message": "Edit policy" }, + "app.skin-site.admin.wiki.save-protect": { "message": "Save policies" }, + "app.skin-site.admin.wiki.protect-saved": { + "message": "Page policies updated." + }, + "app.skin-site.admin.wiki.set-home": { "message": "Set as home page" }, + "app.skin-site.admin.wiki.home-set": { "message": "Home page updated." }, + "app.skin-site.admin.wiki.set-deleted": { "message": "Mark as deleted" }, + "app.skin-site.admin.wiki.set-restored": { "message": "Restore page" }, + "app.skin-site.admin.wiki.deleted-updated": { + "message": "Page deletion state updated." + }, + "app.skin-site.admin.wiki.audit-empty": { "message": "No audit entries." }, + "app.skin-site.admin.wiki.audit-target": { "message": "Target" }, + "app.skin-site.admin.wiki.load-more": { "message": "Load more" }, + "app.skin-site.admin.wiki.settings-hint": { + "message": "Comma separated domains." + }, + "app.skin-site.admin.wiki.domains": { "message": "Domains" }, + "app.skin-site.admin.wiki.home-page": { "message": "Home page" }, + "app.skin-site.admin.wiki.default-skin": { "message": "Default skin" }, + "app.skin-site.admin.wiki.max-page-bytes": { "message": "Max page bytes" }, + "app.skin-site.admin.wiki.max-attachment-bytes": { + "message": "Max attachment bytes" + }, + "app.skin-site.admin.wiki.save-settings": { "message": "Save settings" }, + "app.skin-site.admin.wiki.settings-saved": { "message": "Settings saved." }, + "app.skin-site.admin.review.tab-list": { "message": "Submissions" }, + "app.skin-site.admin.review.tab-papers": { "message": "Papers" }, + "app.skin-site.admin.review.papers-empty": { + "message": "No review papers yet." + }, + "app.skin-site.admin.review.new-paper": { "message": "New paper" }, + "app.skin-site.admin.review.edit-paper": { "message": "Edit" }, + "app.skin-site.admin.review.paper-title": { "message": "Title" }, + "app.skin-site.admin.review.paper-annotation": { "message": "Description" }, + "app.skin-site.admin.review.paper-enabled": { "message": "Enabled" }, + "app.skin-site.admin.review.paper-mode": { "message": "Mode" }, + "app.skin-site.admin.review.paper-pass-score": { "message": "Pass score" }, + "app.skin-site.admin.review.paper-target-permissions": { + "message": "Target permissions" + }, + "app.skin-site.admin.review.paper-questions": { "message": "Questions" }, + "app.skin-site.admin.review.add-question": { "message": "Add question" }, + "app.skin-site.admin.review.question-title": { "message": "Question title" }, + "app.skin-site.admin.review.question-type": { "message": "Type" }, + "app.skin-site.admin.review.question-score": { "message": "Score" }, + "app.skin-site.admin.review.question-annotation": { "message": "Annotation" }, + "app.skin-site.admin.review.add-option": { "message": "Add option" }, + "app.skin-site.admin.review.option-description": { "message": "Option text" }, + "app.skin-site.admin.review.option-correct": { "message": "Correct" }, + "app.skin-site.admin.review.remove-question": { "message": "Remove" }, + "app.skin-site.admin.review.question-no": { "message": "Question {n}" }, + "app.skin-site.admin.review.paper-created": { "message": "Paper created." }, + "app.skin-site.admin.review.paper-saved": { "message": "Paper saved." }, + "app.skin-site.admin.review.mail-title": { "message": "Pass mail" }, + "app.skin-site.admin.review.mail-enabled": { "message": "Send mail on pass" }, + "app.skin-site.admin.review.mail-subject": { "message": "Subject" }, + "app.skin-site.admin.review.mail-message": { "message": "Body" }, + "app.skin-site.admin.review.mail-extra": { "message": "Extra text" }, + "app.skin-site.admin.review.mail-include-invite": { + "message": "Include invite code" + }, + "app.skin-site.admin.review.prevent-duplicate": { + "message": "Reject duplicate submissions" + }, + "app.skin-site.admin.review.copy-link": { "message": "Copy link" }, + "app.skin-site.admin.review.link-copied": { "message": "Link copied." }, + "app.skin-site.admin.review.move-up": { "message": "Move up" }, + "app.skin-site.admin.review.move-down": { "message": "Move down" }, + "app.skin-site.admin.review.mail-subject-required": { + "message": "Subject is required when mail is enabled." + }, + "app.skin-site.admin.review.mail-message-required": { + "message": "Body is required when mail is enabled." + }, + "app.skin-site.admin.users.actions": { "message": "Actions" }, + "app.skin-site.admin.users.audit": { "message": "Audit" }, + "app.skin-site.admin.users.audit-empty": { + "message": "No audit records yet." + }, + "app.skin-site.admin.users.audit-more": { "message": "Load earlier" }, + "app.skin-site.admin.users.audit-title": { + "message": "User management audit" + }, + "app.skin-site.admin.users.bot": { "message": "Bot" }, + "app.skin-site.admin.users.bot-active": { "message": "Token valid" }, + "app.skin-site.admin.users.bot-ban": { "message": "Ban bot creation" }, + "app.skin-site.admin.users.bot-creation-banned": { + "message": "Token creation banned" + }, + "app.skin-site.admin.users.bot-exists": { "message": "Bot created" }, + "app.skin-site.admin.users.bot-inactive": { "message": "Token invalidated" }, + "app.skin-site.admin.users.bot-invalidate": { "message": "Invalidate token" }, + "app.skin-site.admin.users.bot-not-exists": { "message": "No bot yet" }, + "app.skin-site.admin.users.bot-save-username": { + "message": "Save bot username" + }, + "app.skin-site.admin.users.bot-title": { "message": "Bot management" }, + "app.skin-site.admin.users.bot-token-hint": { "message": "Token hint" }, + "app.skin-site.admin.users.bot-unban": { "message": "Unban bot creation" }, + "app.skin-site.admin.users.bot-username": { "message": "Bot username" }, + "app.skin-site.admin.users.bound-players-title": { + "message": "Linked players" + }, + "app.skin-site.admin.users.frequency": { "message": "Rate limit" }, + "app.skin-site.admin.users.frequency-clear": { "message": "Clear limit" }, + "app.skin-site.admin.users.frequency-current": { + "message": "Currently {percent}%" + }, + "app.skin-site.admin.users.frequency-hint": { + "message": "Packets from this user will be dropped at the given rate." + }, + "app.skin-site.admin.users.frequency-none": { "message": "Not enabled" }, + "app.skin-site.admin.users.frequency-range": { + "message": "Only integer values from 10% to 100% are allowed." + }, + "app.skin-site.admin.users.frequency-save": { "message": "Save limit" }, + "app.skin-site.admin.users.frequency-title": { "message": "Rate limit" }, + "app.skin-site.admin.users.invite": { "message": "Invite user" }, + "app.skin-site.admin.users.invite-copied": { + "message": "Invite code copied." + }, + "app.skin-site.admin.users.invite-copy": { "message": "Copy" }, + "app.skin-site.admin.users.invite-email": { "message": "Email" }, + "app.skin-site.admin.users.invite-email-invalid": { + "message": "Enter a valid email address." + }, + "app.skin-site.admin.users.invite-generate": { + "message": "Generate invite code" + }, + "app.skin-site.admin.users.invite-title": { "message": "Invite user" }, + "app.skin-site.admin.users.legacy-adapted": { + "message": "Legacy internal adapted" + }, + "app.skin-site.admin.users.registered-at": { "message": "Registered at" }, + "app.skin-site.admin.users.status": { "message": "Status" }, + "app.skin-site.admin.users.status-banned": { "message": "Banned" }, + "app.skin-site.admin.users.status-normal": { "message": "Normal" }, + "app.skin-site.admin.mods.upload": { "message": "Upload" }, + "app.skin-site.admin.mods.select-mode": { "message": "Select multiple" }, + "app.skin-site.admin.mods.exit-select": { "message": "Done selecting" }, + "app.skin-site.admin.mods.select-all": { "message": "Select all" }, + "app.skin-site.admin.mods.clear-selection": { "message": "Clear selection" }, + "app.skin-site.admin.mods.selected-count": { "message": "{count} selected" }, + "app.skin-site.admin.mods.delete-selected": { "message": "Delete selected" }, + "app.skin-site.admin.mods.confirm-delete-multi": { + "message": "Delete {count} selected mods? This cannot be undone." + }, + "app.skin-site.admin.mods.deleted-multi": { + "message": "Selected mods deleted." + }, + "app.skin-site.admin.mods.server-address": { "message": "Server address" }, + "app.skin-site.admin.mods.server-address-hint": { + "message": "Filter by server address" + }, + "app.skin-site.admin.mods.apply": { "message": "Apply" }, + "app.skin-site.admin.mods.clear": { "message": "Clear" }, + "app.skin-site.admin.mods.upload-title": { "message": "Upload mods" }, + "app.skin-site.admin.mods.upload-drop": { + "message": "Drag .jar files here, or click to choose" + }, + "app.skin-site.admin.mods.upload-sub": { + "message": "Multiple files supported, up to {size} MB each" + }, + "app.skin-site.admin.mods.upload-tags": { "message": "Attach rule tags" }, + "app.skin-site.admin.mods.upload-queue": { "message": "Files" }, + "app.skin-site.admin.mods.upload-pending": { "message": "Pending" }, + "app.skin-site.admin.mods.upload-checking": { "message": "Checking…" }, + "app.skin-site.admin.mods.upload-uploading": { "message": "Uploading…" }, + "app.skin-site.admin.mods.upload-conflict": { "message": "Name exists" }, + "app.skin-site.admin.mods.upload-success": { "message": "Done" }, + "app.skin-site.admin.mods.upload-error": { "message": "Failed" }, + "app.skin-site.admin.mods.upload-exists": { + "message": "This file already exists on the server." + }, + "app.skin-site.admin.mods.upload-name-conflict": { + "message": "A mod with the same file name already exists" + }, + "app.skin-site.admin.mods.upload-name-conflict-hint": { + "message": "Overwrite the existing mod, or skip this file." + }, + "app.skin-site.admin.mods.upload-overwrite": { "message": "Overwrite" }, + "app.skin-site.admin.mods.upload-skip": { "message": "Skip" }, + "app.skin-site.admin.mods.upload-close": { "message": "Close" }, + "app.skin-site.admin.mods.upload-all-done": { + "message": "All files processed." + }, + "app.skin-site.admin.mods.upload-too-large": { + "message": "File too large: {name}" + }, + "app.skin-site.chat.avatar.label": { "message": "Room avatar" }, + "app.skin-site.chat.avatar.upload": { "message": "Upload avatar" }, + "app.skin-site.chat.avatar.replace": { "message": "Replace avatar" }, + "app.skin-site.chat.avatar.remove": { "message": "Remove avatar" }, + "app.skin-site.chat.avatar.hint": { + "message": "PNG, JPEG or WebP, up to 8192 px per side and 5 MB." + }, + "app.skin-site.chat.avatar.bad-format": { + "message": "Only PNG, JPEG and WebP are supported." + }, + "app.skin-site.chat.avatar.too-large": { + "message": "Avatar must not exceed 5 MB." + }, + "app.skin-site.chat.avatar.not-image": { + "message": "Avatar must be an image." + }, + "app.skin-site.chat.admin.title": { "message": "Chat rooms" }, + "app.skin-site.chat.admin.subtitle": { + "message": "Create public or private rooms and hand daily moderation to trusted members." + }, + "app.skin-site.chat.admin.create-room": { "message": "Create room" }, + "app.skin-site.chat.admin.loading": { "message": "Loading…" }, + "app.skin-site.chat.admin.failed": { "message": "Could not load rooms." }, + "app.skin-site.chat.admin.empty": { "message": "No chat rooms yet." }, + "app.skin-site.chat.admin.edit": { "message": "Edit" }, + "app.skin-site.chat.admin.remove": { "message": "Delete" }, + "app.skin-site.chat.admin.save": { "message": "Save" }, + "app.skin-site.chat.admin.cancel": { "message": "Cancel" }, + "app.skin-site.chat.admin.name": { "message": "Name" }, + "app.skin-site.chat.admin.description": { "message": "Description" }, + "app.skin-site.chat.admin.visibility": { "message": "Visibility" }, + "app.skin-site.chat.admin.visibility-public": { "message": "Public" }, + "app.skin-site.chat.admin.visibility-private": { "message": "Private" }, + "app.skin-site.chat.admin.kind": { "message": "Kind" }, + "app.skin-site.chat.admin.kind-standard": { "message": "Standard" }, + "app.skin-site.chat.admin.kind-settlement": { "message": "Settlement" }, + "app.skin-site.chat.admin.prerequisite-mode": { + "message": "Prerequisite mode" + }, + "app.skin-site.chat.admin.prerequisite-any": { "message": "Any" }, + "app.skin-site.chat.admin.prerequisite-all": { "message": "All" }, + "app.skin-site.chat.admin.prerequisite-permissions": { + "message": "Required permissions" + }, + "app.skin-site.chat.admin.prerequisite-hint": { + "message": "Comma separated. Leave empty for no restriction." + }, + "app.skin-site.chat.admin.created": { "message": "Room created." }, + "app.skin-site.chat.admin.updated": { "message": "Room updated." }, + "app.skin-site.chat.admin.deleted": { "message": "Room deleted." }, + "app.skin-site.chat.admin.confirm-delete": { + "message": "Permanently delete room “{name}” and all its messages?" + }, + "app.skin-site.chat.admin.manager-badge": { "message": "Manager" }, + "app.skin-site.chat.admin.voice-badge": { "message": "Voice" }, + "app.skin-site.chat.user-picker.label": { "message": "Pick a user" }, + "app.skin-site.chat.user-picker.placeholder": { + "message": "Search username, email or account ID" + }, + "app.skin-site.chat.user-picker.by-user": { "message": "By user" }, + "app.skin-site.chat.user-picker.by-player": { "message": "By player" }, + "app.skin-site.chat.user-picker.hint": { + "message": "Fuzzy search on username and email, or search by account ID." + }, + "app.skin-site.chat.user-picker.player-hint": { + "message": "Enter a player name to find the bound user." + }, + "app.skin-site.chat.user-picker.empty": { "message": "No matching user." }, + "app.skin-site.chat.user-picker.banned": { + "message": "This account is banned." + }, + "app.skin-site.chat.user-picker.guest": { + "message": "Guests cannot become a manager or settlement owner." + }, + "app.skin-site.chat.user-picker.already-in": { + "message": "This user is already in the room." + }, + "app.skin-site.chat.user-picker.not-allowed": { + "message": "This user is outside the allowed range." + }, + "app.skin-site.chat.user-picker.select": { "message": "Select" }, + "app.skin-site.chat.user-picker.selected": { "message": "Selected" }, + "app.skin-site.chat.user-picker.id": { "message": "ID" }, + "app.skin-site.chat.admin.managers": { "message": "Managers" }, + "app.skin-site.chat.admin.manager-title": { "message": "Room manager" }, + "app.skin-site.chat.admin.manager-hint": { + "message": "Managers can moderate members, messages and files in this room." + }, + "app.skin-site.chat.admin.grant": { "message": "Grant" }, + "app.skin-site.chat.admin.revoke": { "message": "Revoke" }, + "app.skin-site.chat.admin.manager-granted": { "message": "Manager granted." }, + "app.skin-site.chat.admin.manager-revoked": { "message": "Manager revoked." }, + "app.skin-site.chat.admin.owner-title": { "message": "Settlement owner" }, + "app.skin-site.chat.admin.owner-hint": { + "message": "Assign the first settlement owner for this room." + }, + "app.skin-site.chat.admin.owner-assigned": { + "message": "Settlement owner assigned." + }, + "app.skin-site.chat.admin.owner-current": { + "message": "Current owner: {name}" + }, + "app.skin-site.chat.admin.owner-none": { + "message": "No settlement owner yet." + }, + "app.skin-site.chat.admin.audit": { "message": "Audit" }, + "app.skin-site.chat.admin.audit-title": { "message": "Room audit" }, + "app.skin-site.chat.admin.audit-empty": { + "message": "No audit records yet." + }, + "app.skin-site.chat.admin.audit-target": { "message": "Target: {name}" }, + "app.skin-site.chat.admin.close": { "message": "Close" }, + "app.skin-site.chat.voice-runtime-title": { + "message": "Realtime voice runtime" + }, + "app.skin-site.chat.voice-logs": { "message": "View logs" }, + "app.skin-site.chat.voice-config": { "message": "Configure runtime" }, + "app.skin-site.chat.voice-retry": { "message": "Retry deployment" }, + "app.skin-site.chat.voice-config-title": { + "message": "Voice runtime configuration" + }, + "app.skin-site.chat.voice-logs-title": { "message": "LiveKit logs" }, + "app.skin-site.chat.voice-capacity-title": { "message": "Voice capacity" }, + "app.skin-site.chat.voice-capacity": { "message": "Voice capacity" }, + "app.skin-site.chat.participant-limit": { "message": "Participant limit" }, + "app.skin-site.chat.listener-limit": { "message": "Listener limit" }, + "app.skin-site.chat.voice-capacity-updated": { + "message": "Voice capacity updated." + }, + "app.skin-site.chat.voice-enabled": { "message": "Enable realtime voice" }, + "app.skin-site.chat.voice-runtime-path": { "message": "Runtime directory" }, + "app.skin-site.chat.voice-mirror": { "message": "Mirror base URL" }, + "app.skin-site.chat.voice-public-url": { "message": "Public URL" }, + "app.skin-site.chat.voice-bind": { "message": "Bind address" }, + "app.skin-site.chat.voice-signal-port": { "message": "Signal port" }, + "app.skin-site.chat.voice-rtc-tcp-port": { "message": "Media TCP port" }, + "app.skin-site.chat.voice-rtc-udp-port": { "message": "Media UDP port" }, + "app.skin-site.chat.voice-startup-timeout": { + "message": "Startup timeout (s)" + }, + "app.skin-site.chat.voice-use-external-ip": { "message": "Use external IP" }, + "app.skin-site.chat.voice-node-ip": { "message": "Node IP" }, + "app.skin-site.chat.voice-saved": { "message": "Voice configuration saved." }, + "app.skin-site.chat.voice-logs-auto-refresh": { "message": "Auto refresh" }, + "app.skin-site.chat.voice-logs-refresh": { "message": "Refresh" }, + "app.skin-site.chat.voice-logs-empty": { "message": "No logs yet." }, + "app.skin-site.chat.admin.voice-runtime-title": { + "message": "Realtime voice runtime" + }, + "app.skin-site.chat.admin.voice-logs": { "message": "View logs" }, + "app.skin-site.chat.admin.voice-config": { "message": "Configure runtime" }, + "app.skin-site.chat.admin.voice-retry": { "message": "Retry deployment" }, + "app.skin-site.chat.admin.voice-config-title": { + "message": "Voice runtime configuration" + }, + "app.skin-site.chat.admin.voice-logs-title": { "message": "LiveKit logs" }, + "app.skin-site.chat.admin.voice-capacity-title": { + "message": "Voice capacity" + }, + "app.skin-site.chat.admin.voice-capacity": { "message": "Voice capacity" }, + "app.skin-site.chat.admin.participant-limit": { + "message": "Participant limit" + }, + "app.skin-site.chat.admin.listener-limit": { "message": "Listener limit" }, + "app.skin-site.chat.admin.voice-capacity-updated": { + "message": "Voice capacity updated." + }, + "app.skin-site.chat.admin.voice-enabled": { + "message": "Enable realtime voice" + }, + "app.skin-site.chat.admin.voice-runtime-path": { + "message": "Runtime directory" + }, + "app.skin-site.chat.admin.voice-mirror": { "message": "Mirror base URL" }, + "app.skin-site.chat.admin.voice-public-url": { "message": "Public URL" }, + "app.skin-site.chat.admin.voice-bind": { "message": "Bind address" }, + "app.skin-site.chat.admin.voice-signal-port": { "message": "Signal port" }, + "app.skin-site.chat.admin.voice-rtc-tcp-port": { + "message": "Media TCP port" + }, + "app.skin-site.chat.admin.voice-rtc-udp-port": { + "message": "Media UDP port" + }, + "app.skin-site.chat.admin.voice-startup-timeout": { + "message": "Startup timeout (s)" + }, + "app.skin-site.chat.admin.voice-use-external-ip": { + "message": "Use external IP" + }, + "app.skin-site.chat.admin.voice-node-ip": { "message": "Node IP" }, + "app.skin-site.chat.admin.voice-saved": { + "message": "Voice configuration saved." + }, + "app.skin-site.chat.admin.voice-logs-auto-refresh": { + "message": "Auto refresh" + }, + "app.skin-site.chat.admin.voice-logs-refresh": { "message": "Refresh" }, + "app.skin-site.chat.admin.voice-logs-empty": { "message": "No logs yet." }, + "app.skin-site.admin.wiki.tab-migration": { "message": "Migration" }, + "app.skin-site.admin.wiki.migration-title": { + "message": "MediaWiki migration" + }, + "app.skin-site.admin.wiki.migration-hint": { + "message": "Migrated content lands in a hidden staging area and is published in one batch after every page is reviewed." + }, + "app.skin-site.admin.wiki.auto-title": { + "message": "Read from MediaWiki API" + }, + "app.skin-site.admin.wiki.api-url": { "message": "api.php URL" }, + "app.skin-site.admin.wiki.bot-username": { + "message": "Bot username (optional)" + }, + "app.skin-site.admin.wiki.bot-password": { + "message": "Bot password (optional)" + }, + "app.skin-site.admin.wiki.credentials-hint": { + "message": "Credentials stay in memory for this job only. HTTPS is required when supplied." + }, + "app.skin-site.admin.wiki.allow-private": { + "message": "Allow private network source" + }, + "app.skin-site.admin.wiki.allow-private-hint": { + "message": "Private sources may reach services inside the server network." + }, + "app.skin-site.admin.wiki.start-auto": { + "message": "Create automatic migration job" + }, + "app.skin-site.admin.wiki.xml-title": { "message": "Import MediaWiki XML" }, + "app.skin-site.admin.wiki.xml-file": { "message": "Export file" }, + "app.skin-site.admin.wiki.start-xml": { "message": "Upload and stage" }, + "app.skin-site.admin.wiki.stale-rules": { + "message": "Stale page detection rules" + }, + "app.skin-site.admin.wiki.stale-days": { "message": "Days without update" }, + "app.skin-site.admin.wiki.maintainer-window": { + "message": "Maintainer window (days)" + }, + "app.skin-site.admin.wiki.max-maintainers": { "message": "Max maintainers" }, + "app.skin-site.admin.wiki.stale-markers": { + "message": "Stale categories, templates or text" + }, + "app.skin-site.admin.wiki.stale-markers-hint": { + "message": "Comma separated" + }, + "app.skin-site.admin.wiki.jobs": { "message": "Migration jobs" }, + "app.skin-site.admin.wiki.refresh-jobs": { "message": "Refresh" }, + "app.skin-site.admin.wiki.jobs-empty": { + "message": "No migration jobs yet." + }, + "app.skin-site.admin.wiki.reviewed-count": { + "message": "Reviewed {reviewed} / {total}" + }, + "app.skin-site.admin.wiki.imported-excluded": { + "message": "Imported {imported} · Excluded {excluded}" + }, + "app.skin-site.admin.wiki.view-job": { "message": "View job" }, + "app.skin-site.admin.wiki.delete-job": { "message": "Delete job record" }, + "app.skin-site.admin.wiki.confirm-delete-job": { + "message": "Delete migration job #{id}? This cannot be undone." + }, + "app.skin-site.admin.wiki.candidates": { "message": "Candidate pages" }, + "app.skin-site.admin.wiki.candidates-empty": { + "message": "Select a job to review its pages." + }, + "app.skin-site.admin.wiki.last-updated": { "message": "Updated {date}" }, + "app.skin-site.admin.wiki.maintainer-count": { + "message": "{count} maintainers" + }, + "app.skin-site.admin.wiki.conflict": { "message": "Title conflict" }, + "app.skin-site.admin.wiki.target-title": { "message": "Target title" }, + "app.skin-site.admin.wiki.overwrite": { "message": "Overwrite" }, + "app.skin-site.admin.wiki.import": { "message": "Import" }, + "app.skin-site.admin.wiki.exclude": { "message": "Exclude" }, + "app.skin-site.admin.wiki.publish": { "message": "Publish batch" }, + "app.skin-site.admin.wiki.rollback": { "message": "Roll back" }, + "app.skin-site.admin.wiki.confirm-rollback": { + "message": "Roll back this migration batch? Published pages will be reverted." + }, + "app.skin-site.admin.wiki.state-importing": { "message": "Importing" }, + "app.skin-site.admin.wiki.state-review": { "message": "Waiting review" }, + "app.skin-site.admin.wiki.state-publishing": { "message": "Publishing" }, + "app.skin-site.admin.wiki.state-published": { "message": "Published" }, + "app.skin-site.admin.wiki.state-failed": { "message": "Failed" }, + "app.skin-site.admin.wiki.state-rolled-back": { "message": "Rolled back" }, + "app.skin-site.admin.wiki.decision-pending": { "message": "Pending" }, + "app.skin-site.admin.wiki.decision-import": { "message": "Import" }, + "app.skin-site.admin.wiki.decision-exclude": { "message": "Exclude" }, + "app.skin-site.admin.wiki.migration-started": { + "message": "Migration job created." + }, + "app.skin-site.admin.wiki.xml-staged": { + "message": "XML parsed and staged for review." + }, + "app.skin-site.admin.wiki.published": { + "message": "Published {imported} pages, overwrote {overwritten}." + }, + "app.skin-site.admin.wiki.rolled-back": { + "message": "Migration batch rolled back." + }, + "app.skin-site.admin.wiki.job-deleted": { + "message": "Migration record deleted." + }, + "app.skin-site.admin.wiki.select-xml": { + "message": "Choose a MediaWiki XML file first." + }, + "app.skin-site.wiki.title": { "message": "StarLight Wiki" }, + "app.skin-site.wiki.search-placeholder": { "message": "Enter a page title" }, + "app.skin-site.wiki.open": { "message": "Open" }, + "app.skin-site.wiki.loading": { "message": "Loading…" }, + "app.skin-site.wiki.not-found": { "message": "Page not found." }, + "app.skin-site.wiki.empty": { + "message": "Enter a page title above to start reading." + }, + "app.skin-site.wiki.updated-at": { + "message": "Last edited {date} by {author}" + }, + "app.skin-site.wiki.revision": { "message": "Revision #{id}" }, + "app.skin-site.wiki.categories": { "message": "Categories" }, + "app.skin-site.wiki.back": { "message": "Back" } } diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json index 0e33964..0ea5f84 100644 --- a/apps/app-frontend/src/locales/zh-CN/index.json +++ b/apps/app-frontend/src/locales/zh-CN/index.json @@ -1,23 +1,208 @@ { - "app.instance-player.title": { "message": "选择实例玩家" }, - "app.instance-player.remember": { "message": "选择后,此实例将一直使用该玩家。可在实例设置中切换。" }, - "app.instance-player.restore": { "message": "请重新登录 {name};要更换玩家,请前往实例设置。" }, - "app.instance-player.loading": { "message": "正在读取已登录的玩家…" }, - "app.instance-player.licensed": { "message": "正版玩家" }, - "app.instance-player.skin": { "message": "皮肤站玩家" }, - "app.instance-player.offline": { "message": "离线玩家" }, - "app.instance-player.empty": { "message": "当前账号没有可用的皮肤站玩家,可在皮肤站创建玩家。" }, - "app.instance-player.sign-in": { "message": "请登录账号后选择玩家。" }, - "app.instance-player.retry": { "message": "重试" }, - "app.instance-player.skin-login": { "message": "登录皮肤站" }, - "app.instance-player.or": { "message": "或者" }, - "app.instance-player.use-microsoft": { "message": "使用正版账号" }, - "app.instance-player.saving": { "message": "正在保存实例玩家…" }, - "app.instance-player.setting": { "message": "实例玩家" }, - "app.instance-player.first-launch": { "message": "首次启动时选择玩家" }, - "app.instance-player.change": { "message": "切换实例玩家" }, + "app.skin-site.captcha.loading": { "message": "正在加载验证码…" }, + "app.skin-site.captcha.submit": { "message": "验证" }, + "app.skin-site.captcha.subtitle": { + "message": "拖动滑块,将图片旋转至正向。" + }, + "app.skin-site.captcha.title": { "message": "请完成人机验证" }, + "app.skin-site.downloads.all": { "message": "全部" }, + "app.skin-site.downloads.back": { "message": "返回主页" }, + "app.skin-site.downloads.empty": { "message": "暂无可下载的资源。" }, + "app.skin-site.downloads.failed": { "message": "加载下载资源失败。" }, + "app.skin-site.downloads.go": { "message": "下载" }, + "app.skin-site.downloads.loading": { "message": "正在加载…" }, + "app.skin-site.downloads.status.legacy": { "message": "旧版" }, + "app.skin-site.downloads.status.new": { "message": "新增" }, + "app.skin-site.downloads.status.ok": { "message": "可用" }, + "app.skin-site.downloads.status.outdated": { "message": "已过时" }, + "app.skin-site.downloads.status.removal": { "message": "待移除" }, + "app.skin-site.downloads.subtitle": { + "message": "服务器分享的文件,可选择分类筛选。" + }, + "app.skin-site.downloads.title": { "message": "资源下载" }, + "app.skin-site.downloads.uploader": { "message": "上传者:{name}" }, + "app.skin-site.home.announcements.empty": { "message": "暂无公告。" }, + "app.skin-site.home.announcements.failed": { "message": "加载公告失败。" }, + "app.skin-site.home.announcements.loading": { "message": "正在加载…" }, + "app.skin-site.home.announcements.title": { "message": "公告" }, + "app.skin-site.home.donation.donors": { "message": "{count} 位捐赠者" }, + "app.skin-site.home.donation.loading": { "message": "正在加载…" }, + "app.skin-site.home.donation.title": { "message": "鸣谢捐赠" }, + "app.skin-site.home.donation.total": { "message": "累计捐赠" }, + "app.skin-site.home.donation.unavailable": { + "message": "暂时无法获取捐赠统计。" + }, + "app.skin-site.login.forgot": { "message": "忘记密码?" }, + "app.skin-site.login.password": { "message": "密码" }, + "app.skin-site.login.password-hint": { + "message": "6~50 个字符,须包含大写字母、小写字母和数字。" + }, + "app.skin-site.login.register": { "message": "注册账号" }, + "app.skin-site.login.sign-in": { "message": "登录" }, + "app.skin-site.login.signing-in": { "message": "正在登录…" }, + "app.skin-site.login.success": { "message": "已登录 StarLight。" }, + "app.skin-site.login.title": { "message": "登录 StarLight" }, + "app.skin-site.login.username": { "message": "用户名或邮箱" }, + "app.skin-site.login.username-hint": { + "message": "3~16 个字符,或已注册的邮箱地址。" + }, + "app.skin-site.map.back": { "message": "返回主页" }, + "app.skin-site.map.empty": { "message": "暂无可用地图。" }, + "app.skin-site.map.external": { "message": "外部地图" }, + "app.skin-site.map.failed": { "message": "加载地图失败。" }, + "app.skin-site.map.internal": { "message": "内部地图" }, + "app.skin-site.map.loading": { "message": "正在加载地图…" }, + "app.skin-site.map.map": { "message": "地图" }, + "app.skin-site.map.open": { "message": "在浏览器中打开" }, + "app.skin-site.map.server": { "message": "服务器" }, + "app.skin-site.map.title": { "message": "服务器地图" }, + "app.skin-site.profile-view.back": { "message": "返回主页" }, + "app.skin-site.profile-view.cape": { "message": "披风" }, + "app.skin-site.profile-view.elytra": { "message": "鞘翅" }, + "app.skin-site.profile-view.loading": { "message": "正在加载角色…" }, + "app.skin-site.profile-view.no-players": { "message": "暂无角色。" }, + "app.skin-site.profile-view.readonly": { + "message": "Mojang 账号由 Mojang 管理,无法在此修改材质。" + }, + "app.skin-site.profile-view.remove": { "message": "移除" }, + "app.skin-site.profile-view.removed": { "message": "材质已移除。" }, + "app.skin-site.profile-view.select-hint": { + "message": "选择角色以预览和修改其材质。" + }, + "app.skin-site.profile-view.signed-out": { + "message": "登录后管理你的角色。" + }, + "app.skin-site.profile-view.skin": { "message": "皮肤" }, + "app.skin-site.profile-view.title": { "message": "我的角色" }, + "app.skin-site.profile-view.upload": { "message": "上传" }, + "app.skin-site.profile-view.uploaded": { "message": "材质已更新。" }, + "app.skin-site.profile.avatar": { "message": "头像" }, + "app.skin-site.profile.avatar-hint": { + "message": "建议使用正方形图片,系统会将中心区域裁剪为 256×256。" + }, + "app.skin-site.profile.avatar-uploaded": { "message": "头像已更新。" }, + "app.skin-site.profile.basic": { "message": "个人资料" }, + "app.skin-site.profile.choose-avatar": { "message": "选择图片" }, + "app.skin-site.profile.minecraft-password": { "message": "Minecraft 密码" }, + "app.skin-site.profile.minecraft-password-changed": { + "message": "Minecraft 密码已更新。" + }, + "app.skin-site.profile.minecraft-password-hint": { + "message": "用于启动器登录,默认与网站密码相同。" + }, + "app.skin-site.profile.mismatch": { "message": "两次输入的密码不一致。" }, + "app.skin-site.profile.new-password": { "message": "新密码" }, + "app.skin-site.profile.old-password": { "message": "当前密码" }, + "app.skin-site.profile.password-changed": { "message": "密码已更新。" }, + "app.skin-site.profile.password-hint": { + "message": "6~50 个字符,须包含大写字母、小写字母和数字。" + }, + "app.skin-site.profile.repeat-password": { "message": "再次输入新密码" }, + "app.skin-site.profile.reset-default": { "message": "重置为网站密码" }, + "app.skin-site.profile.save": { "message": "保存" }, + "app.skin-site.profile.saved": { "message": "已保存。" }, + "app.skin-site.profile.security": { "message": "安全设置" }, + "app.skin-site.profile.site-password-hint": { + "message": "用于登录皮肤站的密码。" + }, + "app.skin-site.profile.submit-password": { "message": "修改密码" }, + "app.skin-site.profile.title": { "message": "账号设置" }, + "app.skin-site.profile.username": { "message": "用户名" }, + "app.skin-site.profile.username-hint": { "message": "3~16 个字符。" }, + "app.skin-site.register.closed": { "message": "当前服务器暂未开放注册。" }, + "app.skin-site.register.code": { "message": "验证码" }, + "app.skin-site.register.email": { "message": "邮箱" }, + "app.skin-site.register.invite-code": { "message": "邀请码" }, + "app.skin-site.register.invite-required": { + "message": "此服务器需要邀请码才能注册。" + }, + "app.skin-site.register.mismatch": { "message": "两次输入的密码不一致。" }, + "app.skin-site.register.password": { "message": "密码" }, + "app.skin-site.register.password-hint": { + "message": "6~50 个字符,须包含大写字母、小写字母和数字。" + }, + "app.skin-site.register.qq": { "message": "QQ(选填)" }, + "app.skin-site.register.repeat-password": { "message": "再次输入密码" }, + "app.skin-site.register.resend-in": { "message": "已发送({seconds} 秒)" }, + "app.skin-site.register.send-code": { "message": "发送验证码" }, + "app.skin-site.register.sending": { "message": "正在发送…" }, + "app.skin-site.register.submit": { "message": "注册账号" }, + "app.skin-site.register.success": { "message": "账号已创建并登录。" }, + "app.skin-site.register.title": { "message": "注册 StarLight 账号" }, + "app.skin-site.reset.code": { "message": "验证码" }, + "app.skin-site.reset.email": { "message": "邮箱" }, + "app.skin-site.reset.email-hint": { + "message": "请输入注册时使用的邮箱地址。" + }, + "app.skin-site.reset.mismatch": { "message": "两次输入的密码不一致。" }, + "app.skin-site.reset.new-password": { "message": "新密码" }, + "app.skin-site.reset.password-hint": { + "message": "6~50 个字符,须包含大写字母、小写字母和数字。" + }, + "app.skin-site.reset.repeat-password": { "message": "再次输入密码" }, + "app.skin-site.reset.resend-in": { "message": "已发送({seconds} 秒)" }, + "app.skin-site.reset.send-code": { "message": "发送验证码" }, + "app.skin-site.reset.sending": { "message": "正在发送…" }, + "app.skin-site.reset.submit": { "message": "重置密码" }, + "app.skin-site.reset.success": { "message": "密码已更新,现在可以登录。" }, + "app.skin-site.reset.title": { "message": "重置密码" }, + "app.skin-site.review.back": { "message": "返回主页" }, + "app.skin-site.review.back-to-list": { "message": "返回申请列表" }, + "app.skin-site.review.email": { "message": "你的邮箱" }, + "app.skin-site.review.email-hint": { "message": "审核结果将发送至此邮箱。" }, + "app.skin-site.review.empty": { "message": "暂无开放的申请表。" }, + "app.skin-site.review.failed": { "message": "加载审核申请表失败。" }, + "app.skin-site.review.invite-code": { "message": "邀请码" }, + "app.skin-site.review.loading": { "message": "正在加载…" }, + "app.skin-site.review.optional": { "message": "选填" }, + "app.skin-site.review.required": { "message": "必填" }, + "app.skin-site.review.result.passed": { "message": "已通过" }, + "app.skin-site.review.result.pending": { "message": "审核中" }, + "app.skin-site.review.result.rejected": { "message": "未通过" }, + "app.skin-site.review.submit": { "message": "提交申请" }, + "app.skin-site.review.submitted": { "message": "申请已提交。" }, + "app.skin-site.review.submitting": { "message": "正在提交…" }, + "app.skin-site.review.subtitle": { + "message": "选择申请表并回答其中的问题。" + }, + "app.skin-site.review.title": { "message": "玩家审核" }, + "app.skin-site.home.announcements.signed-out": { + "message": "登录后查看公告。" + }, + "app.skin-site.chat.user-picker.email": { "message": "邮箱" }, + "app.skin-site.chat.user-picker.registered": { "message": "注册时间" }, + "app.skin-site.chat.user-picker.banned-status": { "message": "已封禁" }, + "app.skin-site.chat.user-picker.active-status": { "message": "正常" }, + "app.skin-site.admin.wiki.policy-public": { "message": "所有人" }, + "app.skin-site.admin.wiki.policy-member": { "message": "已登录成员" }, + "app.skin-site.admin.wiki.policy-admin": { "message": "网站管理员" }, + "app.instance-player.title": { "message": "选择实例玩家" }, + "app.instance-player.remember": { + "message": "选择后,此实例将一直使用该玩家。可在实例设置中切换。" + }, + "app.instance-player.restore": { + "message": "请重新登录 {name};要更换玩家,请前往实例设置。" + }, + "app.instance-player.loading": { "message": "正在读取已登录的玩家…" }, + "app.instance-player.licensed": { "message": "正版玩家" }, + "app.instance-player.skin": { "message": "皮肤站玩家" }, + "app.instance-player.offline": { "message": "离线玩家" }, + "app.instance-player.empty": { + "message": "当前账号没有可用的皮肤站玩家,可在皮肤站创建玩家。" + }, + "app.instance-player.sign-in": { "message": "请登录账号后选择玩家。" }, + "app.instance-player.retry": { "message": "重试" }, + "app.instance-player.skin-login": { "message": "登录皮肤站" }, + "app.instance-player.or": { "message": "或者" }, + "app.instance-player.use-microsoft": { "message": "使用正版账号" }, + "app.instance-player.saving": { "message": "正在保存实例玩家…" }, + "app.instance-player.setting": { "message": "实例玩家" }, + "app.instance-player.first-launch": { "message": "首次启动时选择玩家" }, + "app.instance-player.change": { "message": "切换实例玩家" }, "app.hosted-mods.title": { "message": "正在更新服务器 Mod" }, - "app.hosted-mods.description": { "message": "所有更新安装完成后将继续启动游戏。" }, + "app.hosted-mods.description": { + "message": "所有更新安装完成后将继续启动游戏。" + }, "app.hosted-mods.complete": { "message": "下载完成" }, "app.hosted-mods.downloads": { "message": "查看下载" }, "app.hosted-mods.close": { "message": "关闭" }, @@ -26,44 +211,68 @@ "app.hosted-packs.progress.downloads": { "message": "查看下载" }, "app.hosted-packs.open-instance": { "message": "打开已安装实例" }, "app.hosted-packs.retry-install": { "message": "重试安装" }, - "app.hosted-packs.auto-installing": { "message": "正在下载并安装服务器整合包…" }, - "app.hosted-packs.auto-description": { "message": "从StarLight服务器获取整合包并自动安装,可一键游玩StarLight服务器" }, + "app.hosted-packs.auto-installing": { + "message": "正在下载并安装服务器整合包…" + }, + "app.hosted-packs.auto-description": { + "message": "从StarLight服务器获取整合包并自动安装,可一键游玩StarLight服务器" + }, "app.hosted-packs.auto-install": { "message": "安装 StarLight 官方整合包" }, "app.hosted-install.game-dir.header": { "message": "选择游戏目录" }, - "app.hosted-install.game-dir.description": { "message": "StarLight 实例的游戏数据(mods、存档、配置、资源包)会存放在外部游戏目录中。请选择一个根目录,整合包会在其中单独建一个子文件夹。" }, + "app.hosted-install.game-dir.description": { + "message": "StarLight 实例的游戏数据(mods、存档、配置、资源包)会存放在外部游戏目录中。请选择一个根目录,整合包会在其中单独建一个子文件夹。" + }, "app.hosted-install.game-dir.label": { "message": "游戏目录根路径" }, "app.hosted-install.game-dir.browse": { "message": "浏览" }, "app.hosted-install.game-dir.no-selection": { "message": "尚未选择文件夹" }, "app.hosted-install.game-dir.reset-default": { "message": "使用默认位置" }, - "app.hosted-install.game-dir.preview": { "message": "游戏文件将安装到:{path}" }, + "app.hosted-install.game-dir.preview": { + "message": "游戏文件将安装到:{path}" + }, "app.hosted-install.game-dir.confirm": { "message": "安装" }, "app.onboarding.instance-mode.title": { "message": "选择实例类型" }, - "app.onboarding.instance-mode.description": { "message": "StarLight 实例自动安装管理员指定的整合包和版本,每次启动先检查并完成更新,需要登录 StarLight 账号。本地实例可自选版本和整合包,适合第三方服务器与单人游玩。" }, + "app.onboarding.instance-mode.description": { + "message": "StarLight 实例自动安装管理员指定的整合包和版本,每次启动先检查并完成更新,需要登录 StarLight 账号。本地实例可自选版本和整合包,适合第三方服务器与单人游玩。" + }, "app.instance-mode.title": { "message": "实例类型" }, "app.instance-mode.starlight": { "message": "StarLight 实例" }, "app.instance-mode.local": { "message": "本地实例" }, - "app.instance-mode.starlight-description": { "message": "游玩 StarLight 服务器必选。自动安装服务器指定的整合包、游戏版本和加载器。每次启动需要登录 StarLight,检查并完成更新后再进入游戏。" }, - "app.instance-mode.local-description": { "message": "不会同步 StarLight 服务器的变更。跳过同步检查,启动更快,可自选整合包安装,更适合第三方服务器与本地个人游玩。" }, - "app.instance-mode.saving": { "message": "正在应用实例类型,并按需安装服务器整合包…" }, + "app.instance-mode.starlight-description": { + "message": "游玩 StarLight 服务器必选。自动安装服务器指定的整合包、游戏版本和加载器。每次启动需要登录 StarLight,检查并完成更新后再进入游戏。" + }, + "app.instance-mode.local-description": { + "message": "不会同步 StarLight 服务器的变更。跳过同步检查,启动更快,可自选整合包安装,更适合第三方服务器与本地个人游玩。" + }, + "app.instance-mode.saving": { + "message": "正在应用实例类型,并按需安装服务器整合包…" + }, "app.instance-mode.loading": { "message": "正在读取实例类型…" }, "app.instance-mode.retry": { "message": "重试" }, "app.instance-mode.local-packs-title": { "message": "本地整合包" }, - "app.instance-mode.local-packs": { "message": "可以自选整合包并安装为本地实例。切换为本地后保留现有游戏文件和存档,之后不再同步 StarLight 的变更。" }, + "app.instance-mode.local-packs": { + "message": "可以自选整合包并安装为本地实例。切换为本地后保留现有游戏文件和存档,之后不再同步 StarLight 的变更。" + }, "app.instance-mode.browse": { "message": "浏览整合包" }, "app.instance-mode.import": { "message": "导入为本地实例" }, - "app.hosted-packs.management": {"message": "内容管理"}, - "app.hosted-packs.mods": {"message": "管理 Mod"}, - "app.hosted-packs.packs": {"message": "管理整合包"}, + "app.hosted-packs.management": { "message": "内容管理" }, + "app.hosted-packs.mods": { "message": "管理 Mod" }, + "app.hosted-packs.packs": { "message": "管理整合包" }, "app.hosted-packs.title": { "message": "服务器管理的整合包" }, - "app.hosted-packs.description": { "message": "整合包和版本由管理员指定。每次启动都会检查服务器更新,有变更时先下载并安装,再启动游戏。需要有效的 StarLight 登录状态和网络连接。" }, - "app.hosted-packs.refresh": {"message": "刷新"}, - "app.hosted-packs.loading": {"message": "正在获取已发布整合包…"}, - "app.hosted-packs.bound": {"message": "已安装:{name} · {version}"}, - "app.hosted-packs.syncing": {"message": "正在比对文件并同步变动…"}, - "app.hosted-packs.complete": {"message": "已同步至 {version},变更 {count} 个文件,下载 {size} MiB。"}, - "app.hosted-packs.preserved": {"message": "保留了 {count} 个本地修改或个人文件"}, - "app.hosted-packs.empty": {"message": "暂无审核通过的整合包。"}, - "app.hosted-packs.update": {"message": "立即同步"}, + "app.hosted-packs.description": { + "message": "整合包和版本由管理员指定。每次启动都会检查服务器更新,有变更时先下载并安装,再启动游戏。需要有效的 StarLight 登录状态和网络连接。" + }, + "app.hosted-packs.refresh": { "message": "刷新" }, + "app.hosted-packs.loading": { "message": "正在获取已发布整合包…" }, + "app.hosted-packs.bound": { "message": "已安装:{name} · {version}" }, + "app.hosted-packs.syncing": { "message": "正在比对文件并同步变动…" }, + "app.hosted-packs.complete": { + "message": "已同步至 {version},变更 {count} 个文件,下载 {size} MiB。" + }, + "app.hosted-packs.preserved": { + "message": "保留了 {count} 个本地修改或个人文件" + }, + "app.hosted-packs.empty": { "message": "暂无审核通过的整合包。" }, + "app.hosted-packs.update": { "message": "立即同步" }, "app.hosted-packs.install": { "message": "重试自动安装" }, "app.home.luck.title": { "message": "每日幸运指数" }, "app.home.luck.get": { "message": "一键获取" }, @@ -71,14 +280,20 @@ "app.home.luck.loading": { "message": "正在询问小精灵..." }, "app.home.luck.error": { "message": "小精灵暂时没有回应,请稍后重试。" }, "app.home.luck.out-of": { "message": "满分 100" }, - "minecraft-account.skin-site.signed-in": { "message": "已登录 StarLight皮肤站" }, - "minecraft-account.skin-site.checking": { "message": "正在验证皮肤站登录状态…" }, + "minecraft-account.skin-site.signed-in": { + "message": "已登录 StarLight皮肤站" + }, + "minecraft-account.skin-site.checking": { + "message": "正在验证皮肤站登录状态…" + }, "minecraft-account.skin-site.sync-error": { "message": "暂时无法验证皮肤站登录状态,将自动重试。" }, "minecraft-account.skin-site.players": { "message": "皮肤站玩家({count})" }, "minecraft-account.skin-site.players.retry": { "message": "重试" }, - "minecraft-account.skin-site.players.empty": { "message": "这个皮肤站账号还没有玩家。" }, + "minecraft-account.skin-site.players.empty": { + "message": "这个皮肤站账号还没有玩家。" + }, "minecraft-account.skin-site.players.error": { "message": "暂时无法刷新玩家列表,已保留上一次结果。" }, @@ -10930,1340 +11145,2184 @@ "settings.backups.repository.usage-description": { "message": "实际存储 {stored},对应 {logical} 的备份数据,共 {count, plural, other {# 个快照}}。" }, - "app.settings.synced-options.choose-sync-source.command-history-description": { "message": "选择要从哪个实例复制命令历史记录。这些记录仅用于初始同步,之后可从任意实例编辑。" }, - "app.settings.synced-options.choose-sync-source.command-history-title": { "message": "选择命令历史记录来源" }, - "app.settings.synced-options.choose-sync-source.creative-hotbars-description": { "message": "选择要从哪个实例复制保存的创造模式快捷栏。这些快捷栏仅用于初始同步,之后可从任意实例编辑。" }, - "app.settings.synced-options.choose-sync-source.creative-hotbars-title": { "message": "选择创造模式快捷栏来源" }, - "app.settings.synced-options.choose-sync-source.data-packs-title": { "message": "选择数据包来源" }, - "app.settings.synced-options.choose-sync-source.game-settings-description": { "message": "选择要从哪个实例复制游戏设置。这些设置仅用于初始同步,之后可从任意实例编辑。" }, - "app.settings.synced-options.choose-sync-source.game-settings-title": { "message": "选择游戏设置来源" }, - "app.settings.synced-options.choose-sync-source.ineligible": { "message": "无法选择此实例作为同步来源" }, - "app.settings.synced-options.choose-sync-source.load-error": { "message": "无法加载同步来源,请重试。" }, - "app.settings.synced-options.choose-sync-source.multiplayer-servers-description": { "message": "选择要从哪个实例复制多人服务器列表。这些服务器仅用于初始同步,之后可从任意实例编辑。" }, - "app.settings.synced-options.choose-sync-source.multiplayer-servers-title": { "message": "选择多人服务器来源" }, - "app.settings.synced-options.choose-sync-source.no-instances-found": { "message": "没有找到实例" }, - "app.settings.synced-options.choose-sync-source.resource-packs-title": { "message": "选择资源包来源" }, - "app.settings.synced-options.choose-sync-source.retry": { "message": "重试" }, - "app.settings.synced-options.choose-sync-source.search-placeholder": { "message": "搜索实例" }, - "app.settings.synced-options.choose-sync-source.sync": { "message": "同步" }, - "app.settings.synced-options.command-history": { "message": "同步命令记录" }, - "app.settings.synced-options.command-history.description": { "message": "在不同的实例之间同步命令记录。" }, - "app.settings.synced-options.command-history.editor-title": { "message": "编辑指令历史记录" }, - "app.settings.synced-options.creative-hotbars": { "message": "同步保存的创造模式物品栏" }, - "app.settings.synced-options.creative-hotbars.description": { "message": "在不同的实例之间同步创造模式物品栏。" }, - "app.settings.synced-options.data-packs": { "message": "同步数据包" }, - "app.settings.synced-options.data-packs.description": { "message": "在所有实例间使用相同的数据包" }, - "app.settings.synced-options.data-packs.none-synced-yet": { "message": "尚未同步任何数据包" }, - "app.settings.synced-options.edit.no-instances": { "message": "请先添加实例,再编辑同步数据。" }, - "app.settings.synced-options.edit.no-synced-data": { "message": "请先选择同步来源,再编辑此设置。" }, - "app.settings.synced-options.game-settings": { "message": "同步游戏设置" }, - "app.settings.synced-options.game-settings.button": { "message": "编辑游戏设置" }, - "app.settings.synced-options.game-settings.description": { "message": "在所有实例间使用相同的游戏设置。" }, - "app.settings.synced-options.game-settings.disabled-tooltip": { "message": "请先启用游戏设置同步,再选择单个设置。" }, - "app.settings.synced-options.game-settings.no-options-to-edit": { "message": "目前没有可编辑的设置" }, - "app.settings.synced-options.multiplayer-servers": { "message": "同步多人模式服务器列表" }, - "app.settings.synced-options.multiplayer-servers.address": { "message": "服务器地址" }, - "app.settings.synced-options.multiplayer-servers.copy-address": { "message": "复制地址" }, - "app.settings.synced-options.multiplayer-servers.count": { "message": "{count, number} 个服务器" }, - "app.settings.synced-options.multiplayer-servers.description": { "message": "在不同的实例直接同步多人模式服务器列表。" }, - "app.settings.synced-options.multiplayer-servers.editor-title": { "message": "编辑同步的服务器" }, - "app.settings.synced-options.multiplayer-servers.empty": { "message": "当前未同步任何用户添加的服务器。" }, - "app.settings.synced-options.multiplayer-servers.filter-offline": { "message": "离线" }, - "app.settings.synced-options.multiplayer-servers.filter-online": { "message": "在线" }, - "app.settings.synced-options.multiplayer-servers.more-options": { "message": "更多服务器选项" }, - "app.settings.synced-options.multiplayer-servers.name": { "message": "服务器名称" }, - "app.settings.synced-options.multiplayer-servers.no-results": { "message": "没有匹配搜索或筛选条件的已同步服务器。" }, - "app.settings.synced-options.multiplayer-servers.none-synced-yet": { "message": "暂无已同步的服务器" }, - "app.settings.synced-options.multiplayer-servers.players-online": { "message": "{count, number} 人在线" }, - "app.settings.synced-options.multiplayer-servers.search": { "message": "搜索 {count, number} 个已同步服务器……" }, - "app.settings.synced-options.multiplayer-servers.status": { "message": "状态" }, - "app.settings.synced-options.packs.disabled": { "message": "请先启用同步,再编辑共享包。" }, - "app.settings.synced-options.resource-packs": { "message": "同步资源包" }, - "app.settings.synced-options.resource-packs.description": { "message": "在所有实例间使用相同的资源包" }, - "app.settings.synced-options.resource-packs.none-synced-yet": { "message": "尚未同步任何资源包" }, - "app.settings.synced-options.title": { "message": "跨所有实例同步的选项" }, - "app.settings.synced-packs.data-packs": { "message": "数据包" }, - "app.settings.synced-packs.disable": { "message": "禁用" }, - "app.settings.synced-packs.empty": { "message": "没有已同步的包" }, - "app.settings.synced-packs.enable": { "message": "启用" }, - "app.settings.synced-packs.remove": { "message": "移除" }, - "app.settings.synced-packs.resource-packs": { "message": "资源包" }, - "app.sync-instances-update.choose-source.data-packs": { "message": "选择要从哪个实例复制数据包。这些包仅用于初始化同步,之后可从任意实例进行编辑。" }, - "app.sync-instances-update.choose-source.resource-packs": { "message": "选择要从哪个实例复制资源包。这些包仅用于初始化同步,之后可从任意实例进行编辑。" }, - "instance.settings.tabs.backups": { "message": "备份" }, - "instance.settings.tabs.sync": { "message": "同步" }, - "app.settings.game-options.category.accessibility.description": { "message": "无障碍设置" }, - "app.settings.game-options.category.accessibility.label": { "message": "无障碍" }, - "app.settings.game-options.category.chat.description": { "message": "聊天显示与外观" }, - "app.settings.game-options.category.chat.label": { "message": "聊天" }, - "app.settings.game-options.category.controls.description": { "message": "鼠标、移动与按键绑定" }, - "app.settings.game-options.category.controls.label": { "message": "控制" }, - "app.settings.game-options.category.custom.description": { "message": "由模组添加的设置" }, - "app.settings.game-options.category.custom.label": { "message": "自定义设置" }, - "app.settings.game-options.category.language.description": { "message": "游戏语言" }, - "app.settings.game-options.category.language.label": { "message": "语言" }, - "app.settings.game-options.category.music-and-sound.description": { "message": "音量与音频偏好" }, - "app.settings.game-options.category.music-and-sound.label": { "message": "音乐和声音" }, - "app.settings.game-options.category.online.description": { "message": "在线与 Realms 偏好" }, - "app.settings.game-options.category.online.label": { "message": "在线" }, - "app.settings.game-options.category.skin-customization.description": { "message": "皮肤层与主手" }, - "app.settings.game-options.category.skin-customization.label": { "message": "皮肤自定义" }, - "app.settings.game-options.category.video.description": { "message": "镜头与显示设置" }, - "app.settings.game-options.category.video.label": { "message": "视频" }, - "app.settings.game-options.choice.all": { "message": "全部" }, - "app.settings.game-options.choice.balanced": { "message": "平衡" }, - "app.settings.game-options.choice.by-player": { "message": "按玩家" }, - "app.settings.game-options.choice.chat": { "message": "聊天" }, - "app.settings.game-options.choice.commands-only": { "message": "仅命令" }, - "app.settings.game-options.choice.constant": { "message": "持续" }, - "app.settings.game-options.choice.crosshair": { "message": "准星" }, - "app.settings.game-options.choice.custom": { "message": "自定义" }, - "app.settings.game-options.choice.decreased": { "message": "降低" }, - "app.settings.game-options.choice.default": { "message": "默认" }, - "app.settings.game-options.choice.fabulous": { "message": "极佳" }, - "app.settings.game-options.choice.fancy": { "message": "精美" }, - "app.settings.game-options.choice.far": { "message": "远" }, - "app.settings.game-options.choice.fast": { "message": "快速" }, - "app.settings.game-options.choice.frequent": { "message": "频繁" }, - "app.settings.game-options.choice.hidden": { "message": "隐藏" }, - "app.settings.game-options.choice.hotbar": { "message": "快捷栏" }, - "app.settings.game-options.choice.left": { "message": "左" }, - "app.settings.game-options.choice.limited": { "message": "有限" }, - "app.settings.game-options.choice.max-fps": { "message": "最高 FPS" }, - "app.settings.game-options.choice.maximum": { "message": "最大" }, - "app.settings.game-options.choice.minimal": { "message": "最少" }, - "app.settings.game-options.choice.minimum": { "message": "最小" }, - "app.settings.game-options.choice.nearby": { "message": "附近" }, - "app.settings.game-options.choice.never": { "message": "从不" }, - "app.settings.game-options.choice.none": { "message": "无" }, - "app.settings.game-options.choice.normal": { "message": "正常" }, - "app.settings.game-options.choice.off": { "message": "关闭" }, - "app.settings.game-options.choice.on": { "message": "开启" }, - "app.settings.game-options.choice.opengl": { "message": "OpenGL 渲染器" }, - "app.settings.game-options.choice.pause": { "message": "暂停菜单" }, - "app.settings.game-options.choice.pause-and-toast": { "message": "暂停菜单和提示" }, - "app.settings.game-options.choice.power-saver": { "message": "省电" }, - "app.settings.game-options.choice.right": { "message": "右" }, - "app.settings.game-options.choice.short": { "message": "短" }, - "app.settings.game-options.choice.shown": { "message": "显示" }, - "app.settings.game-options.choice.system": { "message": "系统" }, - "app.settings.game-options.choice.tiny": { "message": "极小" }, - "app.settings.game-options.choice.vulkan": { "message": "Vulkan 渲染器" }, - "app.settings.game-options.choice.when-minimized": { "message": "最小化时" }, - "app.settings.game-options.choice.while-afk": { "message": "挂机时" }, - "app.settings.game-options.compatibility.none": { "message": "部分实例无法使用此设置" }, - "app.settings.game-options.compatibility.reason.launcher-controlled": { "message": "此设置由启动器设置管理" }, - "app.settings.game-options.custom-value.placeholder": { "message": "输入值" }, - "app.settings.game-options.keybind.assigned-status": { "message": "已将 {setting} 绑定为 {binding}。" }, - "app.settings.game-options.keybind.cancelled-status": { "message": "已取消按键绑定更改。" }, - "app.settings.game-options.keybind.change": { "message": "{setting}:{binding}。激活以更改绑定。" }, - "app.settings.game-options.keybind.choose": { "message": "选择按键" }, - "app.settings.game-options.keybind.conflict-description": { "message": "{settings} 已使用此按键。" }, - "app.settings.game-options.keybind.key.arrow-down": { "message": "向下箭头" }, - "app.settings.game-options.keybind.key.arrow-left": { "message": "向左箭头" }, - "app.settings.game-options.keybind.key.arrow-right": { "message": "向右箭头" }, - "app.settings.game-options.keybind.key.arrow-up": { "message": "向上箭头" }, - "app.settings.game-options.keybind.key.backspace": { "message": "退格" }, - "app.settings.game-options.keybind.key.caps-lock": { "message": "大写锁定" }, - "app.settings.game-options.keybind.key.delete": { "message": "删除" }, - "app.settings.game-options.keybind.key.end": { "message": "End 键" }, - "app.settings.game-options.keybind.key.enter": { "message": "回车" }, - "app.settings.game-options.keybind.key.escape": { "message": "Esc 键" }, - "app.settings.game-options.keybind.key.home": { "message": "Home 键" }, - "app.settings.game-options.keybind.key.insert": { "message": "Insert 键" }, - "app.settings.game-options.keybind.key.keypad-key": { "message": "数字键盘 {key}" }, - "app.settings.game-options.keybind.key.left-alt": { "message": "左 Alt" }, - "app.settings.game-options.keybind.key.left-command": { "message": "左 Command" }, - "app.settings.game-options.keybind.key.left-control": { "message": "左 Ctrl" }, - "app.settings.game-options.keybind.key.left-shift": { "message": "左 Shift" }, - "app.settings.game-options.keybind.key.left-super": { "message": "左 Super" }, - "app.settings.game-options.keybind.key.menu": { "message": "菜单" }, - "app.settings.game-options.keybind.key.num-lock": { "message": "数字锁定" }, - "app.settings.game-options.keybind.key.page-down": { "message": "Page Down 键" }, - "app.settings.game-options.keybind.key.page-up": { "message": "Page Up 键" }, - "app.settings.game-options.keybind.key.pause": { "message": "暂停" }, - "app.settings.game-options.keybind.key.print-screen": { "message": "截图" }, - "app.settings.game-options.keybind.key.right-alt": { "message": "右 Alt" }, - "app.settings.game-options.keybind.key.right-command": { "message": "右 Command" }, - "app.settings.game-options.keybind.key.right-control": { "message": "右 Ctrl" }, - "app.settings.game-options.keybind.key.right-shift": { "message": "右 Shift" }, - "app.settings.game-options.keybind.key.right-super": { "message": "右 Super" }, - "app.settings.game-options.keybind.key.scroll-lock": { "message": "滚动锁定" }, - "app.settings.game-options.keybind.key.space": { "message": "空格" }, - "app.settings.game-options.keybind.key.tab": { "message": "Tab 键" }, - "app.settings.game-options.keybind.key.unbound": { "message": "未绑定" }, - "app.settings.game-options.keybind.key.unknown": { "message": "未知按键" }, - "app.settings.game-options.keybind.listen": { "message": "请按下一个按键……" }, - "app.settings.game-options.keybind.listening-status": { "message": "正在监听按键或鼠标按钮。按 Esc 清除绑定。" }, - "app.settings.game-options.keybind.mixed": { "message": "各实例不同" }, - "app.settings.game-options.keybind.modifier.alt": { "message": "Alt 键" }, - "app.settings.game-options.keybind.modifier.command": { "message": "Command 键" }, - "app.settings.game-options.keybind.modifier.control": { "message": "Ctrl 键" }, - "app.settings.game-options.keybind.modifier.shift": { "message": "Shift 键" }, - "app.settings.game-options.keybind.modifier.super": { "message": "Super 键" }, - "app.settings.game-options.keybind.mouse.button": { "message": "鼠标 {button}" }, - "app.settings.game-options.keybind.mouse.left": { "message": "鼠标左键" }, - "app.settings.game-options.keybind.mouse.middle": { "message": "鼠标中键" }, - "app.settings.game-options.keybind.mouse.right": { "message": "鼠标右键" }, - "app.settings.game-options.keybind.recording-label": { "message": "{setting}:正在监听按键或鼠标按钮。按 Esc 清除绑定。" }, - "app.settings.game-options.keybind.scancode": { "message": "扫描码 {code}" }, - "app.settings.game-options.keybind.unsupported-status": { "message": "Minecraft 无法使用此输入,请尝试其他按键。" }, - "app.settings.game-options.setting.advanced-opengl.label": { "message": "高级 OpenGL" }, - "app.settings.game-options.setting.allow-cursor-changes.label": { "message": "允许光标变化" }, - "app.settings.game-options.setting.allow-server-listing.description": { "message": "允许玩家名称出现在服务器列表中。" }, - "app.settings.game-options.setting.allow-server-listing.label": { "message": "服务器列表" }, - "app.settings.game-options.setting.alternate-blocks.label": { "message": "备用方块" }, - "app.settings.game-options.setting.ambient-occlusion.label": { "message": "平滑光照" }, - "app.settings.game-options.setting.ambient-volume.label": { "message": "环境" }, - "app.settings.game-options.setting.anaglyph-3d.label": { "message": "3D 红蓝立体" }, - "app.settings.game-options.setting.anisotropic-filtering.label": { "message": "各向异性过滤" }, - "app.settings.game-options.setting.anisotropy.label": { "message": "各向异性" }, - "app.settings.game-options.setting.attack-indicator.label": { "message": "攻击指示器" }, - "app.settings.game-options.setting.auto-jump.description": { "message": "自动跳过一格高的障碍物。" }, - "app.settings.game-options.setting.auto-jump.label": { "message": "自动跳跃" }, - "app.settings.game-options.setting.auto-suggestions.label": { "message": "命令建议" }, - "app.settings.game-options.setting.autosave-indicator.label": { "message": "自动保存指示器" }, - "app.settings.game-options.setting.biome-blend-radius.description": { "message": "生物群系颜色过渡的距离。" }, - "app.settings.game-options.setting.biome-blend-radius.label": { "message": "生物群系混合" }, - "app.settings.game-options.setting.blocks-volume.label": { "message": "方块" }, - "app.settings.game-options.setting.brightness.label": { "message": "亮度" }, - "app.settings.game-options.setting.cape.description": { "message": "显示玩家的披风,包括鞘翅纹理。" }, - "app.settings.game-options.setting.cape.label": { "message": "披风" }, - "app.settings.game-options.setting.chat-background-only.label": { "message": "仅聊天背景" }, - "app.settings.game-options.setting.chat-colors.label": { "message": "聊天颜色" }, - "app.settings.game-options.setting.chat-delay.label": { "message": "聊天延迟" }, - "app.settings.game-options.setting.chat-line-spacing.label": { "message": "聊天行间距" }, - "app.settings.game-options.setting.chat-links-prompt.description": { "message": "打开聊天链接前询问" }, - "app.settings.game-options.setting.chat-links-prompt.label": { "message": "打开链接前询问" }, - "app.settings.game-options.setting.chat-links.description": { "message": "允许打开聊天中的网页链接。" }, - "app.settings.game-options.setting.chat-links.label": { "message": "网页链接" }, - "app.settings.game-options.setting.chat-opacity.description": { "message": "聊天文字的不透明度。" }, - "app.settings.game-options.setting.chat-opacity.label": { "message": "聊天不透明度" }, - "app.settings.game-options.setting.chat-preview.label": { "message": "聊天预览" }, - "app.settings.game-options.setting.chat-scale.label": { "message": "聊天缩放" }, - "app.settings.game-options.setting.chat-visibility.label": { "message": "聊天可见性" }, - "app.settings.game-options.setting.chat-width.label": { "message": "聊天宽度" }, - "app.settings.game-options.setting.chunk-fade-time.label": { "message": "区块淡出时间" }, - "app.settings.game-options.setting.cloud-range.label": { "message": "云层距离" }, - "app.settings.game-options.setting.clouds.label": { "message": "云" }, - "app.settings.game-options.setting.ctrl-click-right-click.label": { "message": "按住 Control 点击代替右键点击" }, - "app.settings.game-options.setting.cutout-leaves.label": { "message": "半透明树叶" }, - "app.settings.game-options.setting.damage-tilt.label": { "message": "受伤倾斜" }, - "app.settings.game-options.setting.dark-splash.description": { "message": "将 Mojang Studios 加载界面从红色改为黑色。" }, - "app.settings.game-options.setting.dark-splash.label": { "message": "单色徽标" }, - "app.settings.game-options.setting.darkness-pulsing.label": { "message": "黑暗脉动" }, - "app.settings.game-options.setting.debug-gui-scale.label": { "message": "调试界面缩放" }, - "app.settings.game-options.setting.directional-audio.label": { "message": "方向性音频" }, - "app.settings.game-options.setting.discrete-mouse-scroll.description": { "message": "将每次鼠标滚轮输入视为一个滚动步进。" }, - "app.settings.game-options.setting.discrete-mouse-scroll.label": { "message": "离散滚动" }, - "app.settings.game-options.setting.entity-distance.label": { "message": "实体距离" }, - "app.settings.game-options.setting.entity-shadows.label": { "message": "实体阴影" }, - "app.settings.game-options.setting.exclusive-fullscreen.label": { "message": "独占全屏" }, - "app.settings.game-options.setting.extra-telemetry.label": { "message": "可选遥测" }, - "app.settings.game-options.setting.focused-chat-height.label": { "message": "聚焦时聊天高度" }, - "app.settings.game-options.setting.force-unicode-font.label": { "message": "强制 Unicode 字体" }, - "app.settings.game-options.setting.fov-effects.label": { "message": "视野效果" }, - "app.settings.game-options.setting.fov.label": { "message": "视野" }, - "app.settings.game-options.setting.fullscreen.label": { "message": "全屏" }, - "app.settings.game-options.setting.glint-speed.label": { "message": "附魔光效速度" }, - "app.settings.game-options.setting.glint-strength.label": { "message": "附魔光效强度" }, - "app.settings.game-options.setting.graphics-backend.label": { "message": "图形后端" }, - "app.settings.game-options.setting.graphics.description": { "message": "控制画面质量和性能。" }, - "app.settings.game-options.setting.graphics.label": { "message": "画面" }, - "app.settings.game-options.setting.gui-scale.description": { "message": "游戏界面和 HUD 的大小。" }, - "app.settings.game-options.setting.gui-scale.label": { "message": "界面缩放" }, - "app.settings.game-options.setting.hat.description": { "message": "显示帽子皮肤层。" }, - "app.settings.game-options.setting.hat.label": { "message": "帽子" }, - "app.settings.game-options.setting.held-item-tooltips.label": { "message": "手持物品提示" }, - "app.settings.game-options.setting.hide-lightning-flashes.label": { "message": "隐藏闪电闪光" }, - "app.settings.game-options.setting.hide-matched-names.label": { "message": "隐藏匹配的名称" }, - "app.settings.game-options.setting.hide-server-address.label": { "message": "隐藏服务器地址" }, - "app.settings.game-options.setting.hide-splash-texts.label": { "message": "隐藏启动标语" }, - "app.settings.game-options.setting.high-contrast-outline.label": { "message": "高对比度方块轮廓" }, - "app.settings.game-options.setting.high-contrast.description": { "message": "增强界面元素的对比度。" }, - "app.settings.game-options.setting.high-contrast.label": { "message": "高对比度" }, - "app.settings.game-options.setting.hostile-volume.label": { "message": "敌对生物" }, - "app.settings.game-options.setting.improved-transparency.label": { "message": "改进透明度" }, - "app.settings.game-options.setting.in-game-notifications.label": { "message": "游戏内通知" }, - "app.settings.game-options.setting.inactivity-framerate-limit.label": { "message": "降低帧率" }, - "app.settings.game-options.setting.invert-horizontal-mouse.label": { "message": "反转水平鼠标" }, - "app.settings.game-options.setting.invert-mouse.description": { "message": "反转垂直鼠标移动。" }, - "app.settings.game-options.setting.invert-mouse.label": { "message": "反转鼠标" }, - "app.settings.game-options.setting.jacket.description": { "message": "显示夹克皮肤层。" }, - "app.settings.game-options.setting.jacket.label": { "message": "夹克" }, - "app.settings.game-options.setting.japanese-glyph-variants.label": { "message": "日文字符变体" }, - "app.settings.game-options.setting.key-advancements.label": { "message": "进度" }, - "app.settings.game-options.setting.key-attack.label": { "message": "攻击" }, - "app.settings.game-options.setting.key-back.label": { "message": "后退" }, - "app.settings.game-options.setting.key-chat.label": { "message": "打开聊天" }, - "app.settings.game-options.setting.key-command.label": { "message": "命令" }, - "app.settings.game-options.setting.key-debug-advanced-tooltips.label": { "message": "显示高级提示" }, - "app.settings.game-options.setting.key-debug-chunk-borders.label": { "message": "显示区块边界" }, - "app.settings.game-options.setting.key-debug-clear-chat.label": { "message": "清空聊天" }, - "app.settings.game-options.setting.key-debug-copy-location.label": { "message": "复制位置" }, - "app.settings.game-options.setting.key-debug-copy-recreate-command.label": { "message": "复制重建命令" }, - "app.settings.game-options.setting.key-debug-crash.label": { "message": "触发调试崩溃" }, - "app.settings.game-options.setting.key-debug-dump-dynamic-textures.label": { "message": "转储动态纹理" }, - "app.settings.game-options.setting.key-debug-dump-version.label": { "message": "转储版本" }, - "app.settings.game-options.setting.key-debug-focus-pause.label": { "message": "失去焦点时暂停" }, - "app.settings.game-options.setting.key-debug-fps-charts.label": { "message": "FPS 图表" }, - "app.settings.game-options.setting.key-debug-hitboxes.label": { "message": "显示碰撞箱" }, - "app.settings.game-options.setting.key-debug-improved-transparency.label": { "message": "改进透明度调试视图" }, - "app.settings.game-options.setting.key-debug-lightmap-texture.label": { "message": "光照贴图纹理" }, - "app.settings.game-options.setting.key-debug-modifier.label": { "message": "调试修饰键" }, - "app.settings.game-options.setting.key-debug-network-charts.label": { "message": "网络图表" }, - "app.settings.game-options.setting.key-debug-options.label": { "message": "调试选项" }, - "app.settings.game-options.setting.key-debug-overlay.label": { "message": "调试覆盖层" }, - "app.settings.game-options.setting.key-debug-profiling-chart.label": { "message": "性能分析图表" }, - "app.settings.game-options.setting.key-debug-profiling.label": { "message": "开始性能分析" }, - "app.settings.game-options.setting.key-debug-reload-chunks.label": { "message": "重新加载区块" }, - "app.settings.game-options.setting.key-debug-reload-resource-packs.label": { "message": "重新加载资源包" }, - "app.settings.game-options.setting.key-debug-spectate.label": { "message": "旁观实体" }, - "app.settings.game-options.setting.key-debug-switch-game-mode.label": { "message": "切换游戏模式" }, - "app.settings.game-options.setting.key-drop.label": { "message": "丢弃物品" }, - "app.settings.game-options.setting.key-forward.label": { "message": "前进" }, - "app.settings.game-options.setting.key-friends.label": { "message": "好友" }, - "app.settings.game-options.setting.key-fullscreen.label": { "message": "切换全屏" }, - "app.settings.game-options.setting.key-hotbar-1.label": { "message": "快捷栏 1" }, - "app.settings.game-options.setting.key-hotbar-2.label": { "message": "快捷栏 2" }, - "app.settings.game-options.setting.key-hotbar-3.label": { "message": "快捷栏 3" }, - "app.settings.game-options.setting.key-hotbar-4.label": { "message": "快捷栏 4" }, - "app.settings.game-options.setting.key-hotbar-5.label": { "message": "快捷栏 5" }, - "app.settings.game-options.setting.key-hotbar-6.label": { "message": "快捷栏 6" }, - "app.settings.game-options.setting.key-hotbar-7.label": { "message": "快捷栏 7" }, - "app.settings.game-options.setting.key-hotbar-8.label": { "message": "快捷栏 8" }, - "app.settings.game-options.setting.key-hotbar-9.label": { "message": "快捷栏 9" }, - "app.settings.game-options.setting.key-inventory.label": { "message": "物品栏" }, - "app.settings.game-options.setting.key-jump.label": { "message": "跳跃" }, - "app.settings.game-options.setting.key-left.label": { "message": "向左平移" }, - "app.settings.game-options.setting.key-load-toolbar.label": { "message": "加载工具栏" }, - "app.settings.game-options.setting.key-perspective.label": { "message": "切换视角" }, - "app.settings.game-options.setting.key-pick-item.label": { "message": "选取方块" }, - "app.settings.game-options.setting.key-player-list.label": { "message": "玩家列表" }, - "app.settings.game-options.setting.key-quick-actions.label": { "message": "快捷操作" }, - "app.settings.game-options.setting.key-right.label": { "message": "向右平移" }, - "app.settings.game-options.setting.key-save-toolbar.label": { "message": "保存工具栏" }, - "app.settings.game-options.setting.key-screenshot.label": { "message": "截图" }, - "app.settings.game-options.setting.key-smooth-camera.label": { "message": "切换电影视角" }, - "app.settings.game-options.setting.key-sneak.label": { "message": "潜行" }, - "app.settings.game-options.setting.key-social-interactions.label": { "message": "社交互动" }, - "app.settings.game-options.setting.key-spectator-hotbar.label": { "message": "旁观者快捷栏" }, - "app.settings.game-options.setting.key-spectator-outlines.label": { "message": "高亮旁观者" }, - "app.settings.game-options.setting.key-sprint.label": { "message": "疾跑" }, - "app.settings.game-options.setting.key-swap-offhand.label": { "message": "交换副手" }, - "app.settings.game-options.setting.key-toggle-gui.label": { "message": "切换 HUD" }, - "app.settings.game-options.setting.key-toggle-spectator-shader.label": { "message": "切换旁观者着色器" }, - "app.settings.game-options.setting.key-use.label": { "message": "使用物品" }, - "app.settings.game-options.setting.language.label": { "message": "语言" }, - "app.settings.game-options.setting.left-pants-leg.label": { "message": "左裤腿" }, - "app.settings.game-options.setting.left-sleeve.label": { "message": "左袖" }, - "app.settings.game-options.setting.legacy-framerate-limit.label": { "message": "帧率上限" }, - "app.settings.game-options.setting.legacy-view-distance.label": { "message": "视距" }, - "app.settings.game-options.setting.mac-fullscreen-menu.label": { "message": "全屏时显示 macOS 菜单" }, - "app.settings.game-options.setting.main-hand.description": { "message": "选择主手为左手还是右手。" }, - "app.settings.game-options.setting.main-hand.label": { "message": "主手" }, - "app.settings.game-options.setting.master-volume.label": { "message": "主音量" }, - "app.settings.game-options.setting.max-framerate.label": { "message": "最大帧率" }, - "app.settings.game-options.setting.menu-background-blur.label": { "message": "菜单背景模糊" }, - "app.settings.game-options.setting.mipmap-levels.description": { "message": "平滑远处纹理。" }, - "app.settings.game-options.setting.mipmap-levels.label": { "message": "Mipmap 级别" }, - "app.settings.game-options.setting.mouse-wheel-sensitivity.label": { "message": "鼠标滚轮灵敏度" }, - "app.settings.game-options.setting.music-frequency.label": { "message": "音乐频率" }, - "app.settings.game-options.setting.music-toast.description": { "message": "选择是否在暂停菜单和提示中显示音乐标题。" }, - "app.settings.game-options.setting.music-toast.label": { "message": "音乐通知" }, - "app.settings.game-options.setting.music-volume.label": { "message": "音乐" }, - "app.settings.game-options.setting.narrator-hotkey.label": { "message": "旁白快捷键" }, - "app.settings.game-options.setting.narrator.description": { "message": "选择旁白朗读的内容。" }, - "app.settings.game-options.setting.narrator.label": { "message": "旁白" }, - "app.settings.game-options.setting.neutral-volume.label": { "message": "友好生物" }, - "app.settings.game-options.setting.notification-time.description": { "message": "提示通知保持显示的时长。" }, - "app.settings.game-options.setting.notification-time.label": { "message": "通知时长" }, - "app.settings.game-options.setting.operator-items-tab.label": { "message": "管理员物品栏标签" }, - "app.settings.game-options.setting.panorama-speed.label": { "message": "全景图速度" }, - "app.settings.game-options.setting.particles.label": { "message": "粒子" }, - "app.settings.game-options.setting.players-volume.label": { "message": "玩家" }, - "app.settings.game-options.setting.prioritize-chunk-updates.label": { "message": "区块更新优先级" }, - "app.settings.game-options.setting.quit-shortcuts.label": { "message": "退出快捷键" }, - "app.settings.game-options.setting.raw-mouse-input.label": { "message": "原始鼠标输入" }, - "app.settings.game-options.setting.realms-notifications.label": { "message": "Realms 通知" }, - "app.settings.game-options.setting.record-volume.label": { "message": "唱片机/音符盒" }, - "app.settings.game-options.setting.reduced-debug-info.label": { "message": "减少调试信息" }, - "app.settings.game-options.setting.render-distance.label": { "message": "渲染距离" }, - "app.settings.game-options.setting.right-pants-leg.label": { "message": "右裤腿" }, - "app.settings.game-options.setting.right-sleeve.label": { "message": "右袖" }, - "app.settings.game-options.setting.rotate-with-minecart.label": { "message": "随矿车旋转" }, - "app.settings.game-options.setting.save-chat-drafts.label": { "message": "保存聊天草稿" }, - "app.settings.game-options.setting.screen-effects.label": { "message": "屏幕效果" }, - "app.settings.game-options.setting.secure-chat-only.label": { "message": "仅显示安全聊天" }, - "app.settings.game-options.setting.sensitivity.label": { "message": "鼠标灵敏度" }, - "app.settings.game-options.setting.server-textures.label": { "message": "服务器纹理" }, - "app.settings.game-options.setting.share-presence.label": { "message": "共享在线状态" }, - "app.settings.game-options.setting.simulation-distance.description": { "message": "实体更新以及方块和流体更新的距离。" }, - "app.settings.game-options.setting.simulation-distance.label": { "message": "模拟距离" }, - "app.settings.game-options.setting.snooper.label": { "message": "数据收集" }, - "app.settings.game-options.setting.sprint-window.label": { "message": "疾跑窗口" }, - "app.settings.game-options.setting.subtitles.description": { "message": "显示游戏声音的字幕。" }, - "app.settings.game-options.setting.subtitles.label": { "message": "字幕" }, - "app.settings.game-options.setting.text-background-opacity.label": { "message": "文字背景不透明度" }, - "app.settings.game-options.setting.texture-filtering.label": { "message": "纹理过滤" }, - "app.settings.game-options.setting.toggle-attack.label": { "message": "切换攻击" }, - "app.settings.game-options.setting.toggle-crouch.description": { "message": "按一次即可保持潜行。" }, - "app.settings.game-options.setting.toggle-crouch.label": { "message": "切换潜行" }, - "app.settings.game-options.setting.toggle-sprint.description": { "message": "按一次即可保持疾跑。" }, - "app.settings.game-options.setting.toggle-sprint.label": { "message": "切换疾跑" }, - "app.settings.game-options.setting.toggle-use.label": { "message": "切换使用" }, - "app.settings.game-options.setting.touchscreen.label": { "message": "触屏模式" }, - "app.settings.game-options.setting.ui-volume.label": { "message": "界面" }, - "app.settings.game-options.setting.unfocused-chat-height.label": { "message": "未聚焦时聊天高度" }, - "app.settings.game-options.setting.use-vbo.label": { "message": "使用 VBO" }, - "app.settings.game-options.setting.view-bobbing.description": { "message": "行走时为镜头添加摇摆效果。" }, - "app.settings.game-options.setting.view-bobbing.label": { "message": "视角摇摆" }, - "app.settings.game-options.setting.vignette.label": { "message": "晕影" }, - "app.settings.game-options.setting.voice-volume.label": { "message": "语音和语音朗读" }, - "app.settings.game-options.setting.vsync.description": { "message": "将帧率限制为显示器刷新率,以防止画面撕裂。" }, - "app.settings.game-options.setting.vsync.label": { "message": "垂直同步" }, - "app.settings.game-options.setting.weather-radius.label": { "message": "天气范围" }, - "app.settings.game-options.setting.weather-volume.label": { "message": "天气" }, - "app.settings.game-options.validation.changed-since-opened": { "message": "此设置已在其他位置更改,请检查后重试。" }, - "app.settings.game-options.validation.invalid-value": { "message": "请选择有效值。" }, - "app.settings.game-options.validation.missing-value": { "message": "请先选择一个值。" }, - "app.settings.game-options.validation.no-compatible-instances": { "message": "部分实例无法使用此设置" }, - "app.settings.synced-options.game-settings.conflict-text": { "message": "我们已加载最新设置。请检查更改后再次保存。" }, - "app.settings.synced-options.game-settings.conflict-title": { "message": "这些设置已在其他地方更改" }, - "app.settings.synced-options.game-settings.custom-empty": { "message": "Modrinth 在某个实例中找到模组添加的设置后,它们会显示在这里。" }, - "app.settings.synced-options.game-settings.disable-all": { "message": "取消同步全部" }, - "app.settings.synced-options.game-settings.empty": { "message": "没有匹配当前搜索或筛选条件的设置。" }, - "app.settings.synced-options.game-settings.enable-all": { "message": "同步全部" }, - "app.settings.synced-options.game-settings.load-failed": { "message": "无法加载游戏设置。" }, - "app.settings.synced-options.game-settings.loading": { "message": "正在加载设置……" }, - "app.settings.synced-options.game-settings.not-being-synced": { "message": "未同步" }, - "app.settings.synced-options.game-settings.retry": { "message": "重试" }, - "app.settings.synced-options.game-settings.save": { "message": "保存设置" }, - "app.settings.synced-options.game-settings.search": { "message": "搜索设置……" }, - "app.settings.synced-options.game-settings.sync-description": { "message": "同步的选项会应用到每个实例,并覆盖整合包中包含的值。" }, - "app.settings.synced-options.game-settings.sync-setting": { "message": "在所有实例间同步{setting}" }, - "app.settings.synced-options.game-settings.sync-title": { "message": "同步游戏设置" }, - "app.settings.synced-options.game-settings.synced-across-instances": { "message": "已在所有实例间同步" }, - "app.settings.synced-options.game-settings.title": { "message": "游戏设置" }, - "app.settings.synced-options.game-settings.unsaved-changes": { "message": "你有未保存的更改。" }, - "app.settings.synced-options.game-settings.unsync-setting": { "message": "停止在所有实例间同步{setting}" }, - "app.settings.synced-options.game-settings.value-mixed": { "message": "各实例不同" }, - "app.settings.synced-options.game-settings.value-off": { "message": "关闭" }, - "app.settings.synced-options.game-settings.value-on": { "message": "开启" }, - "app.settings.synced-options.game-settings.value-unset": { "message": "选择一个值" }, - "app.anniversary.banner": { "message": "Starlight {ordinal} 周年快乐!" }, - "instance.settings.tabs.window.window-title": { "message": "窗口标题" }, - "instance.settings.tabs.window.window-title.description": { "message": "自定义 Minecraft 窗口标题。留空则使用默认标题。" }, - "instance.settings.tabs.window.window-title.enter": { "message": "输入窗口标题..." }, - "app.settings.defaults.window-title.enabled": { "message": "自定义 Minecraft 窗口标题" }, - "app.settings.defaults.window-title.enabled-description": { "message": "允许实例自定义 Minecraft 窗口标题。" }, - "app.settings.defaults.window-title.default": { "message": "默认窗口标题" }, - "app.settings.defaults.window-title.default-description": { "message": "当实例未提供自己的标题时使用。" }, - "app.settings.defaults.window-title.default-placeholder": { "message": "输入默认窗口标题..." }, - "app.action-bar.announcements": { "message": "公告" }, - "app.remote-announcements.center-title": { "message": "公告" }, - "app.remote-announcements.empty": { "message": "暂无公告" }, - "app.settings.tabs.logs": { "message": "日志与诊断" }, - "app.settings.logs.export.title": { "message": "导出日志" }, - "app.settings.logs.export.description": { "message": "将启动器日志打包,可附带环境摘要、最近的实例日志与崩溃分析。" }, - "app.settings.logs.export.button": { "message": "导出日志…" }, - "app.settings.logs.open-folder": { "message": "打开日志文件夹" }, - "app.settings.logs.export.modal.title": { "message": "导出日志" }, - "app.settings.logs.export.modal.description": { "message": "选择要包含的详细程度。导出的日志会把访问令牌、Minecraft 令牌与 IP 地址替换为占位符。" }, - "app.settings.logs.export.modal.range": { "message": "时间范围" }, - "app.settings.logs.export.modal.range.30-minutes": { "message": "最近 30 分钟" }, - "app.settings.logs.export.modal.range.2-hours": { "message": "最近 2 小时" }, - "app.settings.logs.export.modal.range.all": { "message": "全部会话" }, - "app.settings.logs.export.modal.level": { "message": "日志等级" }, - "app.settings.logs.export.modal.level.all": { "message": "全部等级" }, - "app.settings.logs.export.modal.level.debug": { "message": "DEBUG 及以上" }, - "app.settings.logs.export.modal.level.info": { "message": "INFO 及以上" }, - "app.settings.logs.export.modal.extras": { "message": "附加信息" }, - "app.settings.logs.export.modal.system-info": { "message": "系统与环境摘要" }, - "app.settings.logs.export.modal.instance-logs": { "message": "最近的实例日志" }, - "app.settings.logs.export.modal.crash-analysis": { "message": "崩溃分析(检测到崩溃时)" }, - "app.settings.logs.export.modal.cancel": { "message": "取消" }, - "app.settings.logs.export.modal.confirm": { "message": "导出" }, - "app.settings.logs.export.notification-title": { "message": "日志已导出" }, - "app.settings.logs.export.notification-text": { "message": "日志压缩包已保存到 {path}" }, - "app.skins.armor.title": {"message": "盔甲与纹饰"}, - "app.skins.armor.save": {"message": "保存"}, - "app.skins.armor.close": {"message": "关闭"}, - "app.skins.armor.description": {"message": "在皮肤上试穿盔甲与纹饰。未保存前不会写入任何内容。"}, - "app.skins.armor.unsaved": {"message": "有未保存的更改"}, - "app.skins.armor.reset-to-saved": {"message": "重置为已保存"}, - "app.skins.armor.restore-defaults": {"message": "恢复默认"}, - "app.instances.batch-edit-groups.add": {"message": "添加"}, - "app.instances.batch-edit-groups.add-to-group-title": {"message": "添加实例到「{groupName}」"}, - "app.instances.batch-edit-groups.added": {"message": "已添加"}, - "app.instances.batch-edit-groups.cancel": {"message": "取消"}, - "app.instances.batch-edit-groups.create": {"message": "创建分组"}, - "app.instances.batch-edit-groups.group-name": {"message": "分组名称"}, - "app.instances.batch-edit-groups.group-name-placeholder": {"message": "输入分组名称"}, - "app.instances.batch-edit-groups.no-instances-found": {"message": "未找到实例"}, - "app.instances.batch-edit-groups.save-changes": {"message": "保存"}, - "app.instances.batch-edit-groups.search-instance": {"message": "搜索实例"}, - "app.instances.group.add-to-group": {"message": "添加到分组"}, - "app.instances.group.collapse": {"message": "折叠分组"}, - "app.instances.group.delete-description": {"message": "此分组中的实例将被取消分组。"}, - "app.instances.group.delete-group": {"message": "删除分组"}, - "app.instances.group.edit-name": {"message": "编辑分组名称"}, - "app.instances.group.empty": {"message": "拖放实例到此处添加。"}, - "app.instances.group.expand": {"message": "展开分组"}, - "app.instances.group.move-down": {"message": "下移分组"}, - "app.instances.group.move-up": {"message": "上移分组"}, - "app.instances.group.name-empty": {"message": "分组名称不能为空。"}, - "app.instances.group.name-too-long": {"message": "分组名称不能超过 {maxLength} 个字符。"}, - "app.instances.group.new-instance": {"message": "新建实例"}, - "app.instances.group.pinned": {"message": "已置顶"}, - "app.instances.group.rename-group": {"message": "重命名分组"}, - "app.instances.sort-by": {"message": "排序方式"}, - "app.instances.sort.direction.asc-alphabetical": {"message": "A–Z 正序"}, - "app.instances.sort.direction.asc-date": {"message": "最早的在前"}, - "app.instances.sort.direction.asc-recency": {"message": "最早游玩在前"}, - "app.instances.sort.direction.asc-version": {"message": "旧版本在前"}, - "app.instances.sort.direction.desc-alphabetical": {"message": "Z–A 倒序"}, - "app.instances.sort.direction.desc-date": {"message": "最新的在前"}, - "app.instances.sort.direction.desc-recency": {"message": "最近游玩在前"}, - "app.instances.sort.direction.desc-version": {"message": "新版本在前"}, - "app.library.context-menu.pin-instance": {"message": "置顶实例"}, - "app.library.context-menu.remove-from-group": {"message": "移出分组"}, - "app.library.context-menu.unpin-instance": {"message": "取消置顶"}, - "app.library.group.new": {"message": "新建分组"}, - "app.library.selection.new-group": {"message": "新建分组"}, - "app.library.selection.remove-from-group": {"message": "移出分组"}, - "app.log-share.settings.title": {"message": "日志分享与 AI 分析"}, - "app.log-share.settings.description": {"message": "将崩溃诊断分享到 LogShare,获取结构化摘要与可选的 AI 分析。优先使用 LogShare,不可用时回退到 mclo.gs。"}, - "app.log-share.settings.share-provider": {"message": "日志分享服务"}, - "app.log-share.settings.share-provider-description": {"message": "分享诊断链接时上传到的目标服务。"}, - "app.log-share.settings.ai-source": {"message": "AI 分析来源"}, - "app.log-share.settings.ai-source-logshare": {"message": "LogShare AI"}, - "app.log-share.settings.ai-source-custom": {"message": "自定义 AI 服务商"}, - "app.log-share.settings.ai-source-description": {"message": "LogShare AI 在云端分析;自定义则使用下方配置的服务商。"}, - "app.log-share.settings.auto-upload": {"message": "自动上传以获取结构化摘要"}, - "app.log-share.settings.auto-upload-description": {"message": "仅在 LogShare 模式下生效。"}, - "app.log-share.settings.multi-file": {"message": "多文件上传"}, - "app.log-share.settings.multi-file-description": {"message": "同时上传 latest.log、崩溃报告、hs_err 以及启动器日志。"}, - "app.log-share.settings.no-storage": {"message": "不落盘直接分析"}, - "app.log-share.settings.no-storage-description": {"message": "直接提交内容进行分析并跳过存储上传。会关闭自动摘要。"}, - "app.log-share.settings.show-progress": {"message": "显示 AI 分析进度"}, - "app.log-share.settings.show-progress-description": {"message": "展示 LogAgent 运行期间的思考、工具及上限状态。"}, - "app.log-share.shared-logs.title": {"message": "已分享日志"}, - "app.log-share.shared-logs.description": {"message": "你分享过的所有诊断都记录在这里。删除条目会将其从记录中移除,并向分享服务请求删除。"}, - "app.log-share.shared-logs.empty": {"message": "暂无已分享的日志。"}, - "app.log-share.shared-logs.delete": {"message": "删除"}, - "app.log-share.shared-logs.deleting": {"message": "删除中…"}, - "app.log-share.shared-logs.deleted": {"message": "已删除共享日志。"}, - "app.log-share.shared-logs.delete-failed": {"message": "删除已分享日志失败。"}, - "app.log-share.shared-logs.truncated": {"message": "已截断"}, - "app.minecraft-crash.share-fallback": {"message": "LogShare 不可用,已改用 mclo.gs 分享诊断信息。"}, - "app.log-share.ai.action": {"message": "AI 深度分析"}, - "app.log-share.summary.title": {"message": "LogShare 分析"}, - "app.log-share.summary.loading": {"message": "正在加载结构化摘要…"}, - "app.log-share.summary.empty": {"message": "LogShare 未返回这些日志的结构化摘要。"}, - "app.log-share.summary.no-storage": {"message": "启用不落盘模式时无法获取结构化摘要。"}, - "app.log-share.summary.failed": {"message": "无法加载 LogShare 摘要:{message}"}, - "app.log-share.summary.problems": {"message": "问题"}, - "app.log-share.summary.information": {"message": "信息"}, - "app.minecraft-crash.logs.loading": {"message": "正在加载崩溃日志…"}, - "app.minecraft-crash.logs.empty": {"message": "未找到本次启动产生的日志。"}, - "app.minecraft-crash.logs.failed": {"message": "无法加载崩溃日志:{message}"}, - "app.minecraft-crash.log-file.loading": {"message": "正在加载日志文件…"}, - "app.minecraft-crash.log-file.failed": {"message": "无法加载此日志文件:{message}"}, - "app.log-share.ai.tab": {"message": "LogAgent 深度分析"}, - "app.log-share.ai.root-cause": {"message": "核心根因"}, - "app.log-share.ai.confidence": {"message": "诊断置信度"}, - "app.log-share.ai.steps": {"message": "推荐排障步骤"}, - "app.log-share.ai.evidence": {"message": "支撑证据链"}, - "app.log-share.ai.thinking": {"message": "思考中…"}, - "app.log-share.ai.queued": {"message": "排队中,前方约 {position} 个任务"}, - "app.log-share.ai.queued-front": {"message": "排队中,正在等待空闲槽位"}, - "app.log-share.ai.tool": {"message": "正在使用工具:{name}"}, - "app.log-share.ai.unknown-tool": {"message": "工具"}, - "app.log-share.ai.tool-result": {"message": "已收到工具结果"}, - "app.log-share.ai.limit": {"message": "已达到工具调用上限"}, - "app.log-share.ai.working": {"message": "正在使用 LogAgent 分析…"}, - "app.log-share.ai.failed": {"message": "LogAgent 分析失败:{message}"}, - "app.shortcut-settings.change-shortcut": {"message": "更改「{action}」的快捷键"}, - "app.shortcut-settings.enable": {"message": "启用快捷滚动"}, - "app.shortcut-settings.end-action": {"message": "滚动到页面底部"}, - "app.shortcut-settings.end-action-description": {"message": "将当前页面移动到底部。"}, - "app.shortcut-settings.home-action": {"message": "滚动到页面顶部"}, - "app.shortcut-settings.home-action-description": {"message": "将当前页面移动到顶部。"}, - "app.shortcut-settings.nav-create": {"message": "新建实例"}, - "app.shortcut-settings.nav-create-description": {"message": "前往新建实例。"}, - "app.shortcut-settings.nav-description": {"message": "使用各自的快捷键跳转到对应菜单项。每个快捷键默认关闭,需逐个启用。"}, - "app.shortcut-settings.nav-discover": {"message": "发现内容"}, - "app.shortcut-settings.nav-discover-description": {"message": "前往浏览内容。"}, - "app.shortcut-settings.nav-downloads": {"message": "下载"}, - "app.shortcut-settings.nav-downloads-description": {"message": "前往下载。"}, - "app.shortcut-settings.nav-home": {"message": "首页"}, - "app.shortcut-settings.nav-home-description": {"message": "前往首页。"}, - "app.shortcut-settings.nav-lab": {"message": "实验室"}, - "app.shortcut-settings.nav-lab-description": {"message": "前往实验室。"}, - "app.shortcut-settings.nav-library": {"message": "内容库"}, - "app.shortcut-settings.nav-library-description": {"message": "前往内容库。"}, - "app.shortcut-settings.nav-multiplayer": {"message": "多人游戏"}, - "app.shortcut-settings.nav-multiplayer-description": {"message": "前往多人游戏。"}, - "app.shortcut-settings.nav-settings": {"message": "设置"}, - "app.shortcut-settings.nav-settings-description": {"message": "前往设置。"}, - "app.shortcut-settings.nav-skins": {"message": "皮肤选择器"}, - "app.shortcut-settings.nav-skins-description": {"message": "前往皮肤选择器。"}, - "app.shortcut-settings.nav-title": {"message": "导航快捷键"}, - "app.shortcut-settings.nav-worlds": {"message": "世界"}, - "app.shortcut-settings.nav-worlds-description": {"message": "前往你的世界。"}, - "app.shortcut-settings.page-down-action": {"message": "向下滚动一屏"}, - "app.shortcut-settings.page-down-action-description": {"message": "将当前页面向下移动一屏。"}, - "app.shortcut-settings.page-up-action": {"message": "向上滚动一屏"}, - "app.shortcut-settings.page-up-action-description": {"message": "将当前页面向上移动一屏。"}, - "app.shortcut-settings.scroll-description": {"message": "使用键盘在长页面中快速滚动。"}, - "app.shortcut-settings.scroll-title": {"message": "快速滚动"}, - "app.settings.tabs.shortcut-settings": {"message": "键盘快捷键"}, - "app.skin-site.bugs.title": {"message": "问题与建议"}, - "app.skin-site.bugs.back": {"message": "返回主页"}, - "app.skin-site.bugs.subtitle": {"message": "反馈问题并跟踪处理进度。"}, - "app.skin-site.bugs.loading": {"message": "加载中…"}, - "app.skin-site.bugs.failed": {"message": "无法加载问题列表。"}, - "app.skin-site.bugs.empty": {"message": "没有符合筛选条件的问题。"}, - "app.skin-site.bugs.all": {"message": "全部"}, - "app.skin-site.bugs.more": {"message": "加载更多"}, - "app.skin-site.bugs.open-only": {"message": "仅显示未关闭"}, - "app.skin-site.bugs.comments": {"message": "{count} 条评论"}, - "app.skin-site.bugs.type.bug": {"message": "缺陷"}, - "app.skin-site.bugs.type.feature": {"message": "功能建议"}, - "app.skin-site.bugs.type.improvement": {"message": "改进"}, - "app.skin-site.bugs.status.pending": {"message": "待处理"}, - "app.skin-site.bugs.status.confirmed": {"message": "已确认"}, - "app.skin-site.bugs.status.needs-info": {"message": "待补充信息"}, - "app.skin-site.bugs.status.in-progress": {"message": "处理中"}, - "app.skin-site.bugs.status.verifying": {"message": "验证中"}, - "app.skin-site.bugs.status.closed": {"message": "已关闭"}, - "app.skin-site.bugs.priority.critical": {"message": "紧急"}, - "app.skin-site.bugs.priority.high": {"message": "高"}, - "app.skin-site.bugs.priority.normal": {"message": "普通"}, - "app.skin-site.bugs.priority.low": {"message": "低"}, - "app.skin-site.bugs.resolution.fixed": {"message": "已修复"}, - "app.skin-site.bugs.resolution.wont-fix": {"message": "不予修复"}, - "app.skin-site.bugs.resolution.by-design": {"message": "设计如此"}, - "app.skin-site.bugs.resolution.duplicate": {"message": "重复问题"}, - "app.skin-site.bugs.resolution.cannot-reproduce": {"message": "无法复现"}, - "app.skin-site.bugs.resolution.invalid": {"message": "无效"}, - "app.skin-site.bug-detail.back": {"message": "返回列表"}, - "app.skin-site.bug-detail.loading": {"message": "加载中…"}, - "app.skin-site.bug-detail.failed": {"message": "无法加载该问题。"}, - "app.skin-site.bug-detail.issue-no": {"message": "问题 #{id}"}, - "app.skin-site.bug-detail.by": {"message": "由 {name} 提交"}, - "app.skin-site.bug-detail.assignee": {"message": "负责人:{name}"}, - "app.skin-site.bug-detail.unassigned": {"message": "未分配"}, - "app.skin-site.bug-detail.description-empty": {"message": "未提供描述。"}, - "app.skin-site.bug-detail.resolution": {"message": "处理结果:{name}"}, - "app.skin-site.bug-detail.comments-title": {"message": "评论"}, - "app.skin-site.bug-detail.comments-empty": {"message": "还没有评论,来抢沙发吧。"}, - "app.skin-site.bug-detail.comments-loading": {"message": "加载评论中…"}, - "app.skin-site.bug-detail.comment-placeholder": {"message": "写下你的评论…"}, - "app.skin-site.bug-detail.comment-submit": {"message": "发表评论"}, - "app.skin-site.bug-detail.comment-sending": {"message": "发送中…"}, - "app.skin-site.bug-detail.comment-empty-error": {"message": "评论内容不能为空。"}, - "app.skin-site.bug-detail.comment-posted": {"message": "评论已发表。"}, - "app.skin-site.bug-detail.comment-deleted": {"message": "评论已删除。"}, - "app.skin-site.bug-detail.delete": {"message": "删除"}, - "app.skin-site.bug-detail.deleting": {"message": "删除中…"}, - "app.skin-site.bug-detail.system": {"message": "系统"}, - "app.skin-site.bug-create.title": {"message": "新建问题"}, - "app.skin-site.bug-create.type": {"message": "类型"}, - "app.skin-site.bug-create.title-field": {"message": "标题"}, - "app.skin-site.bug-create.title-placeholder": {"message": "简要描述问题"}, - "app.skin-site.bug-create.description": {"message": "描述"}, - "app.skin-site.bug-create.description-placeholder": {"message": "发生了什么?如何复现?"}, - "app.skin-site.bug-create.private": {"message": "私密问题"}, - "app.skin-site.bug-create.private-hint": {"message": "仅你和维护者可见。"}, - "app.skin-site.bug-create.images": {"message": "图片"}, - "app.skin-site.bug-create.images-hint": {"message": "可选,最多 5 张图片。"}, - "app.skin-site.bug-create.submit": {"message": "创建问题"}, - "app.skin-site.bug-create.submitting": {"message": "创建中…"}, - "app.skin-site.bug-create.created": {"message": "问题已创建。"}, - "app.skin-site.bug-create.cancel": {"message": "取消"}, - "app.skin-site.bug-notifications.title": {"message": "通知"}, - "app.skin-site.bug-notifications.back": {"message": "返回主页"}, - "app.skin-site.bug-notifications.subtitle": {"message": "你提交或关注的问题动态。"}, - "app.skin-site.bug-notifications.loading": {"message": "加载中…"}, - "app.skin-site.bug-notifications.failed": {"message": "无法加载通知。"}, - "app.skin-site.bug-notifications.empty": {"message": "暂无新通知。"}, - "app.skin-site.bug-notifications.item": {"message": "{name} 更新了问题 #{id}"}, - "app.skin-site.bug-notifications.mark-all": {"message": "全部标为已读"}, - "app.skin-site.bug-notifications.marked-all": {"message": "已将全部通知标为已读。"}, - "app.skin-site.bugs.create": {"message": "新建问题"}, - "app.skin-site.bugs.notifications": {"message": "通知"}, - "app.skin-site.home.title": {"message": "星光皮肤站"}, - "app.skin-site.home.subtitle": {"message": "星光服务器玩家的衣柜。"}, - "app.skin-site.home.address": {"message": "皮肤站地址"}, - "app.skin-site.home.copied": {"message": "地址已复制。"}, - "app.skin-site.home.copy": {"message": "复制"}, - "app.skin-site.admin.title": {"message": "管理控制台"}, - "app.skin-site.admin.back": {"message": "返回主页"}, - "app.skin-site.admin.no-access": {"message": "你没有访问权限。"}, - "app.skin-site.admin.no-access-hint": {"message": "请使用管理员账号登录后使用本控制台。"}, - "app.skin-site.admin.group.security": {"message": "系统安全"}, - "app.skin-site.admin.group.users": {"message": "用户管理"}, - "app.skin-site.admin.group.community": {"message": "社区管理"}, - "app.skin-site.admin.group.resources": {"message": "资源分发"}, - "app.skin-site.admin.item.servers": {"message": "服务器管理"}, - "app.skin-site.admin.item.summary": {"message": "状态概览"}, - "app.skin-site.admin.item.access-audit": {"message": "登录与认证审计"}, - "app.skin-site.admin.item.users": {"message": "用户信息"}, - "app.skin-site.admin.item.players": {"message": "玩家信息"}, - "app.skin-site.admin.item.review": {"message": "玩家审核"}, - "app.skin-site.admin.item.broadcast": {"message": "公告操作"}, - "app.skin-site.admin.item.chat": {"message": "聊天室管理"}, - "app.skin-site.admin.item.wiki": {"message": "维基管理"}, - "app.skin-site.admin.item.donations": {"message": "捐赠管理"}, - "app.skin-site.admin.item.mods": {"message": "Mod 管理"}, - "app.skin-site.admin.item.launcher": {"message": "启动器管理"}, - "app.skin-site.admin.item.downloads": {"message": "下载管理"}, - "app.skin-site.home.admin": {"message": "管理"}, - "app.skin-site.home.settings": {"message": "账号"}, - "app.skin-site.home.sign-in": {"message": "登录"}, - "app.skin-site.home.players": {"message": "角色"}, - "app.skin-site.home.my-players": {"message": "我的角色"}, - "app.skin-site.home.no-players": {"message": "尚未绑定角色。"}, - "app.skin-site.home.loading": {"message": "加载中…"}, - "app.skin-site.home.error": {"message": "加载失败。"}, - "app.skin-site.home.luck": {"message": "幸运值"}, - "app.skin-site.home.luck-unavailable": {"message": "不可用"}, - "app.skin-site.home.signed-out-title": {"message": "未登录"}, - "app.skin-site.home.signed-out-body": {"message": "登录后可管理你的账号与皮肤。"}, - "app.skin-site.admin.summary.title": {"message": "系统概况"}, - "app.skin-site.admin.summary.loading": {"message": "加载中…"}, - "app.skin-site.admin.summary.failed": {"message": "无法加载状态。"}, - "app.skin-site.admin.summary.uptime": {"message": "运行时长"}, - "app.skin-site.admin.summary.memory": {"message": "内存占用"}, - "app.skin-site.admin.summary.users": {"message": "用户数量"}, - "app.skin-site.admin.summary.players": {"message": "玩家数量"}, - "app.skin-site.admin.summary.realtime-hint": {"message": "实时 CPU 与 GC 图表依赖管理端 WebSocket,目前尚未接入。"}, - "app.skin-site.admin.broadcast.title": {"message": "公告"}, - "app.skin-site.admin.broadcast.loading": {"message": "加载中…"}, - "app.skin-site.admin.broadcast.failed": {"message": "加载失败。"}, - "app.skin-site.admin.broadcast.empty": {"message": "尚未发布过任何公告。"}, - "app.skin-site.admin.broadcast.create": {"message": "发布新公告"}, - "app.skin-site.admin.broadcast.save": {"message": "保存"}, - "app.skin-site.admin.broadcast.cancel": {"message": "取消"}, - "app.skin-site.admin.broadcast.edit": {"message": "编辑"}, - "app.skin-site.admin.broadcast.remove": {"message": "删除"}, - "app.skin-site.admin.broadcast.title-field": {"message": "标题"}, - "app.skin-site.admin.broadcast.subtitle-field": {"message": "副标题"}, - "app.skin-site.admin.broadcast.content-field": {"message": "内容"}, - "app.skin-site.admin.broadcast.expire-at": {"message": "过期时间"}, - "app.skin-site.admin.broadcast.star": {"message": "标星"}, - "app.skin-site.admin.broadcast.level": {"message": "重要程度"}, - "app.skin-site.admin.broadcast.image": {"message": "图片"}, - "app.skin-site.admin.broadcast.remove-image": {"message": "移除图片"}, - "app.skin-site.admin.broadcast.created": {"message": "公告已发布。"}, - "app.skin-site.admin.broadcast.updated": {"message": "公告已更新。"}, - "app.skin-site.admin.broadcast.deleted": {"message": "公告已删除。"}, - "app.skin-site.admin.broadcast.confirm-delete": {"message": "确定删除「{title}」吗?此操作不可撤销。"}, - "app.skin-site.admin.broadcast.uploader": {"message": "由 {name} 发布"}, - "app.skin-site.admin.players.title": {"message": "玩家"}, - "app.skin-site.admin.players.loading": {"message": "加载中…"}, - "app.skin-site.admin.players.failed": {"message": "无法加载玩家列表。"}, - "app.skin-site.admin.players.empty": {"message": "没有找到玩家。"}, - "app.skin-site.admin.players.search": {"message": "搜索"}, - "app.skin-site.admin.players.search-name": {"message": "名称"}, - "app.skin-site.admin.players.search-owner": {"message": "所有者"}, - "app.skin-site.admin.players.search-uuid": {"message": "UUID"}, - "app.skin-site.admin.players.total": {"message": "共 {count} 名玩家"}, - "app.skin-site.admin.players.prev": {"message": "上一页"}, - "app.skin-site.admin.players.next": {"message": "下一页"}, - "app.skin-site.admin.players.page": {"message": "第 {page} 页"}, - "app.skin-site.admin.players.name": {"message": "名称"}, - "app.skin-site.admin.players.owner": {"message": "所有者"}, - "app.skin-site.admin.players.source": {"message": "来源"}, - "app.skin-site.admin.players.legacy": {"message": "旧版适配"}, - "app.skin-site.admin.players.actions": {"message": "操作"}, - "app.skin-site.admin.players.edit": {"message": "编辑"}, - "app.skin-site.admin.players.save": {"message": "保存"}, - "app.skin-site.admin.players.cancel": {"message": "取消"}, - "app.skin-site.admin.players.remove": {"message": "删除"}, - "app.skin-site.admin.players.updated": {"message": "玩家已更新。"}, - "app.skin-site.admin.players.deleted": {"message": "玩家已删除。"}, - "app.skin-site.admin.players.confirm-delete": {"message": "确定删除玩家「{name}」吗?此操作不可撤销。"}, - "app.skin-site.admin.players.mojang": {"message": "正版"}, - "app.skin-site.admin.players.custom": {"message": "皮肤站"}, - "app.skin-site.admin.users.title": {"message": "用户"}, - "app.skin-site.admin.users.loading": {"message": "加载中…"}, - "app.skin-site.admin.users.failed": {"message": "无法加载用户列表。"}, - "app.skin-site.admin.users.empty": {"message": "没有找到用户。"}, - "app.skin-site.admin.users.search": {"message": "搜索"}, - "app.skin-site.admin.users.search-name": {"message": "用户名"}, - "app.skin-site.admin.users.search-email": {"message": "邮箱"}, - "app.skin-site.admin.users.search-uuid": {"message": "UUID"}, - "app.skin-site.admin.users.total": {"message": "共 {count} 名用户"}, - "app.skin-site.admin.users.prev": {"message": "上一页"}, - "app.skin-site.admin.users.next": {"message": "下一页"}, - "app.skin-site.admin.users.page": {"message": "第 {page} 页"}, - "app.skin-site.admin.users.edit": {"message": "编辑"}, - "app.skin-site.admin.users.save": {"message": "保存"}, - "app.skin-site.admin.users.cancel": {"message": "取消"}, - "app.skin-site.admin.users.remove": {"message": "删除"}, - "app.skin-site.admin.users.players": {"message": "角色"}, - "app.skin-site.admin.users.updated": {"message": "用户已更新。"}, - "app.skin-site.admin.users.deleted": {"message": "用户已删除。"}, - "app.skin-site.admin.users.confirm-delete": {"message": "确定删除用户「{name}」吗?此操作不可撤销。"}, - "app.skin-site.admin.users.username": {"message": "用户名"}, - "app.skin-site.admin.users.email": {"message": "邮箱"}, - "app.skin-site.admin.users.qq": {"message": "QQ"}, - "app.skin-site.admin.users.password": {"message": "新密码"}, - "app.skin-site.admin.users.password-hint": {"message": "留空则保持原密码不变。"}, - "app.skin-site.admin.users.banned": {"message": "封禁"}, - "app.skin-site.admin.users.permissions": {"message": "权限"}, - "app.skin-site.admin.users.permissions-hint": {"message": "以逗号分隔,例如 WEB_ADMIN, SERVER_ADMIN。"}, - "app.skin-site.admin.users.no-players": {"message": "没有绑定的角色。"}, - "app.skin-site.admin.users.registered": {"message": "注册时间"}, - "app.skin-site.admin.review.title": {"message": "玩家审核"}, - "app.skin-site.admin.review.loading": {"message": "加载中…"}, - "app.skin-site.admin.review.failed": {"message": "无法加载申请列表。"}, - "app.skin-site.admin.review.empty": {"message": "没有找到申请。"}, - "app.skin-site.admin.review.total": {"message": "共 {count} 份申请"}, - "app.skin-site.admin.review.filter-all": {"message": "全部"}, - "app.skin-site.admin.review.filter-pending": {"message": "待处理"}, - "app.skin-site.admin.review.filter-passed": {"message": "已通过"}, - "app.skin-site.admin.review.filter-rejected": {"message": "已拒绝"}, - "app.skin-site.admin.review.result-pending": {"message": "待处理"}, - "app.skin-site.admin.review.result-passed": {"message": "已通过"}, - "app.skin-site.admin.review.result-rejected": {"message": "已拒绝"}, - "app.skin-site.admin.review.mode-auto": {"message": "自动"}, - "app.skin-site.admin.review.mode-manual": {"message": "人工"}, - "app.skin-site.admin.review.applicant": {"message": "申请人"}, - "app.skin-site.admin.review.submitted-at": {"message": "提交时间"}, - "app.skin-site.admin.review.score": {"message": "得分"}, - "app.skin-site.admin.review.reviewer": {"message": "审核人"}, - "app.skin-site.admin.review.details": {"message": "详情"}, - "app.skin-site.admin.review.hide": {"message": "收起"}, - "app.skin-site.admin.review.answers": {"message": "作答内容"}, - "app.skin-site.admin.review.no-answers": {"message": "没有作答记录。"}, - "app.skin-site.admin.review.auto-checks": {"message": "自动检查"}, - "app.skin-site.admin.review.context": {"message": "提交环境"}, - "app.skin-site.admin.review.ip": {"message": "IP"}, - "app.skin-site.admin.review.user-agent": {"message": "浏览器标识"}, - "app.skin-site.admin.review.activity": {"message": "行为记录"}, - "app.skin-site.admin.review.duration": {"message": "时长"}, - "app.skin-site.admin.review.hidden-time": {"message": "切出时长"}, - "app.skin-site.admin.review.screen-switches": {"message": "切屏次数"}, - "app.skin-site.admin.review.history": {"message": "审核记录"}, - "app.skin-site.admin.review.accept": {"message": "通过"}, - "app.skin-site.admin.review.reject": {"message": "拒绝"}, - "app.skin-site.admin.review.reason": {"message": "理由"}, - "app.skin-site.admin.review.accept-done": {"message": "申请已通过。"}, - "app.skin-site.admin.review.reject-done": {"message": "申请已拒绝。"}, - "app.skin-site.admin.review.regen-invite": {"message": "重发邀请码"}, - "app.skin-site.admin.review.invite-regenerated": {"message": "邀请码已重新生成。"}, - "app.skin-site.admin.review.invite-sent": {"message": "邮件已发送。"}, - "app.skin-site.admin.review.invite-not-sent": {"message": "邮件未发送。"}, - "app.skin-site.admin.review.prev": {"message": "上一页"}, - "app.skin-site.admin.review.next": {"message": "下一页"}, - "app.skin-site.admin.review.page": {"message": "第 {page} 页"}, - "app.skin-site.admin.launcher.title": {"message": "启动器版本"}, - "app.skin-site.admin.launcher.loading": {"message": "加载中…"}, - "app.skin-site.admin.launcher.failed": {"message": "无法加载版本列表。"}, - "app.skin-site.admin.launcher.empty": {"message": "尚未上传任何版本。"}, - "app.skin-site.admin.launcher.latest-url": {"message": "最新版本地址"}, - "app.skin-site.admin.launcher.version": {"message": "版本"}, - "app.skin-site.admin.launcher.channel": {"message": "渠道"}, - "app.skin-site.admin.launcher.uploader": {"message": "上传者"}, - "app.skin-site.admin.launcher.uploaded-at": {"message": "上传时间"}, - "app.skin-site.admin.launcher.published-at": {"message": "发布时间"}, - "app.skin-site.admin.launcher.force-update": {"message": "强制更新"}, - "app.skin-site.admin.launcher.active": {"message": "当前版本"}, - "app.skin-site.admin.launcher.activate": {"message": "设为当前"}, - "app.skin-site.admin.launcher.activated": {"message": "已设为当前版本。"}, - "app.skin-site.admin.launcher.upload": {"message": "上传版本"}, - "app.skin-site.admin.launcher.upload-hint": {"message": "选择发布包 ZIP(最大 4 GiB)。"}, - "app.skin-site.admin.launcher.uploading": {"message": "上传中…"}, - "app.skin-site.admin.launcher.upload-done": {"message": "版本已上传。"}, - "app.skin-site.admin.launcher.force-flag": {"message": "强制更新"}, - "app.skin-site.admin.launcher.prev": {"message": "上一页"}, - "app.skin-site.admin.launcher.next": {"message": "下一页"}, - "app.skin-site.admin.launcher.page": {"message": "第 {page} 页"}, - "app.skin-site.admin.launcher.total": {"message": "共 {count} 个版本"}, - "app.skin-site.admin.downloads.title": {"message": "资源下载"}, - "app.skin-site.admin.downloads.loading": {"message": "加载中…"}, - "app.skin-site.admin.downloads.failed": {"message": "无法加载资源列表。"}, - "app.skin-site.admin.downloads.empty": {"message": "没有下载条目。"}, - "app.skin-site.admin.downloads.categories": {"message": "分类"}, - "app.skin-site.admin.downloads.all": {"message": "全部条目"}, - "app.skin-site.admin.downloads.add-category": {"message": "添加"}, - "app.skin-site.admin.downloads.category-name": {"message": "分类名称"}, - "app.skin-site.admin.downloads.remove-category": {"message": "移除"}, - "app.skin-site.admin.downloads.total": {"message": "共 {count} 个条目"}, - "app.skin-site.admin.downloads.add-item": {"message": "新建条目"}, - "app.skin-site.admin.downloads.save": {"message": "保存"}, - "app.skin-site.admin.downloads.cancel": {"message": "取消"}, - "app.skin-site.admin.downloads.edit": {"message": "编辑"}, - "app.skin-site.admin.downloads.remove": {"message": "删除"}, - "app.skin-site.admin.downloads.filename": {"message": "文件名"}, - "app.skin-site.admin.downloads.description": {"message": "描述"}, - "app.skin-site.admin.downloads.routes": {"message": "下载线路"}, - "app.skin-site.admin.downloads.route-name": {"message": "名称"}, - "app.skin-site.admin.downloads.route-url": {"message": "URL"}, - "app.skin-site.admin.downloads.add-route": {"message": "添加线路"}, - "app.skin-site.admin.downloads.remove-route": {"message": "移除"}, - "app.skin-site.admin.downloads.statuses": {"message": "状态"}, - "app.skin-site.admin.downloads.bind-category": {"message": "所属分类"}, - "app.skin-site.admin.downloads.uploader": {"message": "由 {name} 上传"}, - "app.skin-site.admin.downloads.created": {"message": "条目已创建。"}, - "app.skin-site.admin.downloads.updated": {"message": "条目已更新。"}, - "app.skin-site.admin.downloads.deleted": {"message": "条目已删除。"}, - "app.skin-site.admin.downloads.category-added": {"message": "分类已添加。"}, - "app.skin-site.admin.downloads.category-deleted": {"message": "分类已删除。"}, - "app.skin-site.admin.downloads.confirm-delete-item": {"message": "确定删除「{name}」吗?此操作不可撤销。"}, - "app.skin-site.admin.downloads.confirm-delete-category": {"message": "确定删除分类「{name}」吗?"}, - "app.skin-site.admin.servers.title": {"message": "地图服务器"}, - "app.skin-site.admin.servers.loading": {"message": "加载中…"}, - "app.skin-site.admin.servers.failed": {"message": "无法加载服务器列表。"}, - "app.skin-site.admin.servers.empty": {"message": "尚未配置地图服务器。"}, - "app.skin-site.admin.servers.server-name": {"message": "服务器名称"}, - "app.skin-site.admin.servers.internal": {"message": "内部"}, - "app.skin-site.admin.servers.maps": {"message": "地图"}, - "app.skin-site.admin.servers.map-name": {"message": "地图名称"}, - "app.skin-site.admin.servers.map-url": {"message": "地图地址"}, - "app.skin-site.admin.servers.add-map": {"message": "添加地图"}, - "app.skin-site.admin.servers.remove-map": {"message": "移除"}, - "app.skin-site.admin.servers.add-server": {"message": "添加服务器"}, - "app.skin-site.admin.servers.remove-server": {"message": "删除服务器"}, - "app.skin-site.admin.servers.save": {"message": "保存更改"}, - "app.skin-site.admin.servers.saving": {"message": "保存中…"}, - "app.skin-site.admin.servers.saved": {"message": "地图服务器已保存。"}, - "app.skin-site.admin.audit.title": {"message": "登录与认证审计"}, - "app.skin-site.admin.audit.loading": {"message": "加载中…"}, - "app.skin-site.admin.audit.failed": {"message": "无法加载审计日志。"}, - "app.skin-site.admin.audit.empty": {"message": "没有审计记录。"}, - "app.skin-site.admin.audit.subject": {"message": "主体"}, - "app.skin-site.admin.audit.action": {"message": "操作"}, - "app.skin-site.admin.audit.source-ip": {"message": "来源 IP"}, - "app.skin-site.admin.audit.reported-ip": {"message": "上报玩家 IP"}, - "app.skin-site.admin.audit.count": {"message": "次数"}, - "app.skin-site.admin.audit.first-at": {"message": "首次出现"}, - "app.skin-site.admin.audit.last-at": {"message": "最近出现"}, - "app.skin-site.admin.audit.proxy": {"message": "代理"}, - "app.skin-site.admin.audit.internal": {"message": "内部"}, - "app.skin-site.admin.audit.search-subject": {"message": "主体"}, - "app.skin-site.admin.audit.search-ip": {"message": "IP"}, - "app.skin-site.admin.audit.search-action": {"message": "操作"}, - "app.skin-site.admin.audit.search": {"message": "搜索"}, - "app.skin-site.admin.audit.clear": {"message": "清空"}, - "app.skin-site.admin.audit.load-more": {"message": "加载更多"}, - "app.skin-site.admin.audit.retention": {"message": "保留天数"}, - "app.skin-site.admin.audit.record-limit": {"message": "记录上限"}, - "app.skin-site.admin.audit.save-retention": {"message": "保存"}, - "app.skin-site.admin.audit.retention-saved": {"message": "保留天数已更新。"}, - "app.skin-site.admin.donations.title": {"message": "捐赠管理"}, - "app.skin-site.admin.donations.loading": {"message": "加载中…"}, - "app.skin-site.admin.donations.failed": {"message": "无法加载捐赠列表。"}, - "app.skin-site.admin.donations.empty": {"message": "暂无捐赠记录。"}, - "app.skin-site.admin.donations.total-amount": {"message": "总额"}, - "app.skin-site.admin.donations.donor-count": {"message": "捐赠人数"}, - "app.skin-site.admin.donations.donor": {"message": "捐赠者"}, - "app.skin-site.admin.donations.amount": {"message": "金额"}, - "app.skin-site.admin.donations.updated-at": {"message": "更新时间"}, - "app.skin-site.admin.donations.external": {"message": "外部"}, - "app.skin-site.admin.donations.linked": {"message": "已绑定"}, - "app.skin-site.admin.donations.create": {"message": "新增捐赠"}, - "app.skin-site.admin.donations.save": {"message": "保存"}, - "app.skin-site.admin.donations.cancel": {"message": "取消"}, - "app.skin-site.admin.donations.edit": {"message": "编辑"}, - "app.skin-site.admin.donations.remove": {"message": "删除"}, - "app.skin-site.admin.donations.amount-field": {"message": "金额"}, - "app.skin-site.admin.donations.display-name": {"message": "显示名称"}, - "app.skin-site.admin.donations.user-uuid": {"message": "用户 UUID"}, - "app.skin-site.admin.donations.qq": {"message": "QQ"}, - "app.skin-site.admin.donations.append-mode": {"message": "在原有金额上累加"}, - "app.skin-site.admin.donations.created": {"message": "捐赠记录已创建。"}, - "app.skin-site.admin.donations.updated": {"message": "捐赠记录已更新。"}, - "app.skin-site.admin.donations.deleted": {"message": "捐赠记录已删除。"}, - "app.skin-site.admin.donations.confirm-delete": {"message": "确定删除「{name}」的捐赠记录吗?此操作不可撤销。"}, - "app.skin-site.admin.donations.load-more": {"message": "加载更多"}, - "app.skin-site.admin.mods.title": {"message": "Mod 管理"}, - "app.skin-site.admin.mods.tab-mods": {"message": "Mod 文件"}, - "app.skin-site.admin.mods.tab-tags": {"message": "规则标签"}, - "app.skin-site.admin.mods.tab-runtime": {"message": "运行时"}, - "app.skin-site.admin.mods.loading": {"message": "加载中…"}, - "app.skin-site.admin.mods.failed": {"message": "无法加载 Mod 列表。"}, - "app.skin-site.admin.mods.empty": {"message": "没有找到 Mod。"}, - "app.skin-site.admin.mods.total": {"message": "共 {count} 个 Mod"}, - "app.skin-site.admin.mods.search": {"message": "按名称搜索"}, - "app.skin-site.admin.mods.filter-all": {"message": "全部标签"}, - "app.skin-site.admin.mods.prev": {"message": "上一页"}, - "app.skin-site.admin.mods.next": {"message": "下一页"}, - "app.skin-site.admin.mods.page": {"message": "第 {page} 页"}, - "app.skin-site.admin.mods.name": {"message": "名称"}, - "app.skin-site.admin.mods.version": {"message": "版本"}, - "app.skin-site.admin.mods.mod-id": {"message": "Mod ID"}, - "app.skin-site.admin.mods.size": {"message": "大小"}, - "app.skin-site.admin.mods.loaders": {"message": "加载器"}, - "app.skin-site.admin.mods.domain-rules": {"message": "域名规则"}, - "app.skin-site.admin.mods.edit": {"message": "编辑"}, - "app.skin-site.admin.mods.save": {"message": "保存"}, - "app.skin-site.admin.mods.cancel": {"message": "取消"}, - "app.skin-site.admin.mods.remove": {"message": "删除"}, - "app.skin-site.admin.mods.updated": {"message": "Mod 已更新。"}, - "app.skin-site.admin.mods.deleted": {"message": "Mod 已删除。"}, - "app.skin-site.admin.mods.confirm-delete": {"message": "确定删除 Mod「{name}」吗?此操作不可撤销。"}, - "app.skin-site.admin.mods.rule-tags": {"message": "规则标签"}, - "app.skin-site.admin.mods.tag-name": {"message": "标签名称"}, - "app.skin-site.admin.mods.tag-rules": {"message": "规则"}, - "app.skin-site.admin.mods.tag-rules-hint": {"message": "每行一条规则。"}, - "app.skin-site.admin.mods.new-tag": {"message": "新建标签"}, - "app.skin-site.admin.mods.tag-created": {"message": "标签已创建。"}, - "app.skin-site.admin.mods.tag-updated": {"message": "标签已更新。"}, - "app.skin-site.admin.mods.tag-deleted": {"message": "标签已删除。"}, - "app.skin-site.admin.mods.confirm-delete-tag": {"message": "确定删除标签「{name}」吗?"}, - "app.skin-site.admin.mods.runtime": {"message": "运行时版本"}, - "app.skin-site.admin.mods.manifest-url": {"message": "清单地址"}, - "app.skin-site.admin.mods.upload-runtime": {"message": "上传运行时"}, - "app.skin-site.admin.mods.runtime-version": {"message": "版本"}, - "app.skin-site.admin.mods.uploading": {"message": "上传中…"}, - "app.skin-site.admin.mods.runtime-uploaded": {"message": "运行时已上传。"}, - "app.skin-site.admin.mods.activate": {"message": "设为当前"}, - "app.skin-site.admin.mods.activated": {"message": "运行时已启用。"}, - "app.skin-site.admin.mods.available": {"message": "可用"}, - "app.skin-site.admin.mods.unavailable": {"message": "不可用"}, - "app.skin-site.admin.mods.active": {"message": "当前版本"}, - "app.skin-site.admin.mods.uploader": {"message": "由 {name} 上传"}, - "app.skin-site.home.nav.title": {"message": "皮肤站"}, - "app.skin-site.home.nav.download": {"message": "资源下载"}, - "app.skin-site.home.nav.download-desc": {"message": "浏览服务器分享的文件。"}, - "app.skin-site.home.nav.review": {"message": "玩家审核"}, - "app.skin-site.home.nav.review-desc": {"message": "提交并审核玩家申请。"}, - "app.skin-site.home.nav.wiki": {"message": "服务器维基"}, - "app.skin-site.home.nav.wiki-desc": {"message": "服务器的指南与文档。"}, - "app.skin-site.home.nav.bugs": {"message": "问题追踪"}, - "app.skin-site.home.nav.bugs-desc": {"message": "反馈问题并跟踪处理进度。"}, - "app.skin-site.home.nav.map": {"message": "服务器地图"}, - "app.skin-site.home.nav.map-desc": {"message": "探索实时世界地图。"}, - "app.skin-site.home.nav.profile": {"message": "个人资料"}, - "app.skin-site.home.nav.profile-desc": {"message": "管理你的账号与皮肤。"}, - "app.skin-site.admin.wiki.title": {"message": "维基管理"}, - "app.skin-site.admin.wiki.tab-page": {"message": "页面"}, - "app.skin-site.admin.wiki.tab-audit": {"message": "审计日志"}, - "app.skin-site.admin.wiki.tab-settings": {"message": "设置"}, - "app.skin-site.admin.wiki.loading": {"message": "加载中…"}, - "app.skin-site.admin.wiki.page-path": {"message": "页面路径"}, - "app.skin-site.admin.wiki.load-page": {"message": "加载页面"}, - "app.skin-site.admin.wiki.page-not-found": {"message": "未找到该页面。"}, - "app.skin-site.admin.wiki.revision": {"message": "版本"}, - "app.skin-site.admin.wiki.author": {"message": "作者"}, - "app.skin-site.admin.wiki.updated-at": {"message": "更新时间"}, - "app.skin-site.admin.wiki.categories": {"message": "分类"}, - "app.skin-site.admin.wiki.read-policy": {"message": "阅读权限"}, - "app.skin-site.admin.wiki.edit-policy": {"message": "编辑权限"}, - "app.skin-site.admin.wiki.save-protect": {"message": "保存权限"}, - "app.skin-site.admin.wiki.protect-saved": {"message": "页面权限已更新。"}, - "app.skin-site.admin.wiki.set-home": {"message": "设为首页"}, - "app.skin-site.admin.wiki.home-set": {"message": "首页已更新。"}, - "app.skin-site.admin.wiki.set-deleted": {"message": "标记为已删除"}, - "app.skin-site.admin.wiki.set-restored": {"message": "恢复页面"}, - "app.skin-site.admin.wiki.deleted-updated": {"message": "页面删除状态已更新。"}, - "app.skin-site.admin.wiki.audit-empty": {"message": "没有审计记录。"}, - "app.skin-site.admin.wiki.audit-target": {"message": "目标"}, - "app.skin-site.admin.wiki.load-more": {"message": "加载更多"}, - "app.skin-site.admin.wiki.settings-hint": {"message": "以逗号分隔域名。"}, - "app.skin-site.admin.wiki.domains": {"message": "域名"}, - "app.skin-site.admin.wiki.home-page": {"message": "首页"}, - "app.skin-site.admin.wiki.default-skin": {"message": "默认皮肤"}, - "app.skin-site.admin.wiki.max-page-bytes": {"message": "页面最大字节数"}, - "app.skin-site.admin.wiki.max-attachment-bytes": {"message": "附件最大字节数"}, - "app.skin-site.admin.wiki.save-settings": {"message": "保存设置"}, - "app.skin-site.admin.wiki.settings-saved": {"message": "设置已保存。"}, - "app.skin-site.admin.review.tab-list": {"message": "申请列表"}, - "app.skin-site.admin.review.tab-papers": {"message": "试卷管理"}, - "app.skin-site.admin.review.papers-empty": {"message": "还没有审核试卷。"}, - "app.skin-site.admin.review.new-paper": {"message": "新建试卷"}, - "app.skin-site.admin.review.edit-paper": {"message": "编辑"}, - "app.skin-site.admin.review.paper-title": {"message": "试卷标题"}, - "app.skin-site.admin.review.paper-annotation": {"message": "描述"}, - "app.skin-site.admin.review.paper-enabled": {"message": "启用"}, - "app.skin-site.admin.review.paper-mode": {"message": "阅卷模式"}, - "app.skin-site.admin.review.paper-pass-score": {"message": "通过分数"}, - "app.skin-site.admin.review.paper-target-permissions": {"message": "通过后获得的身份"}, - "app.skin-site.admin.review.paper-questions": {"message": "题目"}, - "app.skin-site.admin.review.add-question": {"message": "添加题目"}, - "app.skin-site.admin.review.question-title": {"message": "题目"}, - "app.skin-site.admin.review.question-type": {"message": "题型"}, - "app.skin-site.admin.review.question-score": {"message": "分数"}, - "app.skin-site.admin.review.question-annotation": {"message": "题目注解"}, - "app.skin-site.admin.review.add-option": {"message": "添加选项"}, - "app.skin-site.admin.review.option-description": {"message": "选项文本"}, - "app.skin-site.admin.review.option-correct": {"message": "正确"}, - "app.skin-site.admin.review.remove-question": {"message": "删除"}, - "app.skin-site.admin.review.question-no": {"message": "第 {n} 题"}, - "app.skin-site.admin.review.paper-created": {"message": "试卷已创建。"}, - "app.skin-site.admin.review.paper-saved": {"message": "试卷已保存。"}, - "app.skin-site.admin.review.mail-title": {"message": "通过邮件"}, - "app.skin-site.admin.review.mail-enabled": {"message": "审核通过后发送邮件"}, - "app.skin-site.admin.review.mail-subject": {"message": "邮件标题"}, - "app.skin-site.admin.review.mail-message": {"message": "邮件正文"}, - "app.skin-site.admin.review.mail-extra": {"message": "邮件补充说明"}, - "app.skin-site.admin.review.mail-include-invite": {"message": "附带邀请码"}, - "app.skin-site.admin.review.prevent-duplicate": {"message": "禁止重复作答"}, - "app.skin-site.admin.review.copy-link": {"message": "复制直链"}, - "app.skin-site.admin.review.link-copied": {"message": "直链已复制。"}, - "app.skin-site.admin.review.move-up": {"message": "上移"}, - "app.skin-site.admin.review.move-down": {"message": "下移"}, - "app.skin-site.admin.review.mail-subject-required": {"message": "开启邮件后必须填写邮件标题。"}, - "app.skin-site.admin.review.mail-message-required": {"message": "开启邮件后必须填写邮件正文。"}, - "app.skin-site.admin.users.actions": {"message": "操作"}, - "app.skin-site.admin.users.audit": {"message": "审计"}, - "app.skin-site.admin.users.audit-empty": {"message": "暂无管理审计记录。"}, - "app.skin-site.admin.users.audit-more": {"message": "加载更早记录"}, - "app.skin-site.admin.users.audit-title": {"message": "用户管理审计"}, - "app.skin-site.admin.users.bot": {"message": "机器人"}, - "app.skin-site.admin.users.bot-active": {"message": "Token 有效"}, - "app.skin-site.admin.users.bot-ban": {"message": "封禁 Bot 创建"}, - "app.skin-site.admin.users.bot-creation-banned": {"message": "禁止创建 Token"}, - "app.skin-site.admin.users.bot-exists": {"message": "已创建 Bot"}, - "app.skin-site.admin.users.bot-inactive": {"message": "Token 已失效"}, - "app.skin-site.admin.users.bot-invalidate": {"message": "使 Token 失效"}, - "app.skin-site.admin.users.bot-not-exists": {"message": "尚未创建 Bot"}, - "app.skin-site.admin.users.bot-save-username": {"message": "保存机器人用户名"}, - "app.skin-site.admin.users.bot-title": {"message": "Bot 管理"}, - "app.skin-site.admin.users.bot-token-hint": {"message": "Token 标识"}, - "app.skin-site.admin.users.bot-unban": {"message": "解除 Bot 封禁"}, - "app.skin-site.admin.users.bot-username": {"message": "机器人用户名"}, - "app.skin-site.admin.users.bound-players-title": {"message": "绑定玩家"}, - "app.skin-site.admin.users.frequency": {"message": "频率限制"}, - "app.skin-site.admin.users.frequency-clear": {"message": "取消限制"}, - "app.skin-site.admin.users.frequency-current": {"message": "当前 {percent}%"}, - "app.skin-site.admin.users.frequency-hint": {"message": "以下列频率对指定用户进行丢包处理,限制其进入服务器的频率。"}, - "app.skin-site.admin.users.frequency-none": {"message": "当前未启用"}, - "app.skin-site.admin.users.frequency-range": {"message": "仅允许设置 10% 至 100% 的整数。"}, - "app.skin-site.admin.users.frequency-save": {"message": "保存限制"}, - "app.skin-site.admin.users.frequency-title": {"message": "频率限制"}, - "app.skin-site.admin.users.invite": {"message": "邀请用户"}, - "app.skin-site.admin.users.invite-copied": {"message": "邀请码已复制。"}, - "app.skin-site.admin.users.invite-copy": {"message": "复制"}, - "app.skin-site.admin.users.invite-email": {"message": "邮箱"}, - "app.skin-site.admin.users.invite-email-invalid": {"message": "请输入正确格式的邮箱。"}, - "app.skin-site.admin.users.invite-generate": {"message": "生成邀请码"}, - "app.skin-site.admin.users.invite-title": {"message": "邀请用户"}, - "app.skin-site.admin.users.legacy-adapted": {"message": "已适配旧内服"}, - "app.skin-site.admin.users.registered-at": {"message": "注册时间"}, - "app.skin-site.admin.users.status": {"message": "状态"}, - "app.skin-site.admin.users.status-banned": {"message": "已封禁"}, - "app.skin-site.admin.users.status-normal": {"message": "正常"}, - "app.skin-site.admin.mods.upload": {"message": "上传"}, - "app.skin-site.admin.mods.select-mode": {"message": "选择多个"}, - "app.skin-site.admin.mods.exit-select": {"message": "完成选择"}, - "app.skin-site.admin.mods.select-all": {"message": "全选本页"}, - "app.skin-site.admin.mods.clear-selection": {"message": "取消全选"}, - "app.skin-site.admin.mods.selected-count": {"message": "已选择 {count} 项"}, - "app.skin-site.admin.mods.delete-selected": {"message": "删除所选"}, - "app.skin-site.admin.mods.confirm-delete-multi": {"message": "确定删除选中的 {count} 个 Mod?此操作不可撤销。"}, - "app.skin-site.admin.mods.deleted-multi": {"message": "已删除所选 Mod。"}, - "app.skin-site.admin.mods.server-address": {"message": "服务器地址"}, - "app.skin-site.admin.mods.server-address-hint": {"message": "按服务器地址过滤"}, - "app.skin-site.admin.mods.apply": {"message": "应用"}, - "app.skin-site.admin.mods.clear": {"message": "清除"}, - "app.skin-site.admin.mods.upload-title": {"message": "上传 Mod"}, - "app.skin-site.admin.mods.upload-drop": {"message": "拖拽 .jar 文件到此处,或点击选择"}, - "app.skin-site.admin.mods.upload-sub": {"message": "支持多文件,单个不超过 {size} MB"}, - "app.skin-site.admin.mods.upload-tags": {"message": "附加规则标签"}, - "app.skin-site.admin.mods.upload-queue": {"message": "文件列表"}, - "app.skin-site.admin.mods.upload-pending": {"message": "等待中"}, - "app.skin-site.admin.mods.upload-checking": {"message": "检查中…"}, - "app.skin-site.admin.mods.upload-uploading": {"message": "上传中…"}, - "app.skin-site.admin.mods.upload-conflict": {"message": "名称已存在"}, - "app.skin-site.admin.mods.upload-success": {"message": "完成"}, - "app.skin-site.admin.mods.upload-error": {"message": "失败"}, - "app.skin-site.admin.mods.upload-exists": {"message": "该文件已存在于服务器。"}, - "app.skin-site.admin.mods.upload-name-conflict": {"message": "已存在同名 Mod"}, - "app.skin-site.admin.mods.upload-name-conflict-hint": {"message": "覆盖已有 Mod,或跳过此文件。"}, - "app.skin-site.admin.mods.upload-overwrite": {"message": "覆盖"}, - "app.skin-site.admin.mods.upload-skip": {"message": "跳过"}, - "app.skin-site.admin.mods.upload-close": {"message": "关闭"}, - "app.skin-site.admin.mods.upload-all-done": {"message": "全部文件处理完成。"}, - "app.skin-site.admin.mods.upload-too-large": {"message": "文件过大:{name}"}, - "app.skin-site.chat.avatar.label": {"message": "房间头像"}, - "app.skin-site.chat.avatar.upload": {"message": "上传头像"}, - "app.skin-site.chat.avatar.replace": {"message": "更换头像"}, - "app.skin-site.chat.avatar.remove": {"message": "移除头像"}, - "app.skin-site.chat.avatar.hint": {"message": "支持 PNG、JPEG、WebP,单边不超过 8192 像素,文件不超过 5 MB。"}, - "app.skin-site.chat.avatar.bad-format": {"message": "房间头像仅支持 PNG、JPEG 和 WebP 格式。"}, - "app.skin-site.chat.avatar.too-large": {"message": "房间头像大小不能超过 5 MB。"}, - "app.skin-site.chat.avatar.not-image": {"message": "房间头像必须是图片。"}, - "app.skin-site.chat.admin.title": {"message": "聊天室管理"}, - "app.skin-site.chat.admin.subtitle": {"message": "创建公开或私有房间,并将日常管理权限交给合适的成员。"}, - "app.skin-site.chat.admin.create-room": {"message": "创建房间"}, - "app.skin-site.chat.admin.loading": {"message": "加载中…"}, - "app.skin-site.chat.admin.failed": {"message": "无法加载房间列表。"}, - "app.skin-site.chat.admin.empty": {"message": "还没有聊天室。"}, - "app.skin-site.chat.admin.edit": {"message": "编辑"}, - "app.skin-site.chat.admin.remove": {"message": "删除"}, - "app.skin-site.chat.admin.save": {"message": "保存"}, - "app.skin-site.chat.admin.cancel": {"message": "取消"}, - "app.skin-site.chat.admin.name": {"message": "房间名称"}, - "app.skin-site.chat.admin.description": {"message": "房间简介"}, - "app.skin-site.chat.admin.visibility": {"message": "可见性"}, - "app.skin-site.chat.admin.visibility-public": {"message": "公开"}, - "app.skin-site.chat.admin.visibility-private": {"message": "私有"}, - "app.skin-site.chat.admin.kind": {"message": "房间类型"}, - "app.skin-site.chat.admin.kind-standard": {"message": "普通房间"}, - "app.skin-site.chat.admin.kind-settlement": {"message": "聚落房间"}, - "app.skin-site.chat.admin.prerequisite-mode": {"message": "前置条件判定"}, - "app.skin-site.chat.admin.prerequisite-any": {"message": "满足任一"}, - "app.skin-site.chat.admin.prerequisite-all": {"message": "满足全部"}, - "app.skin-site.chat.admin.prerequisite-permissions": {"message": "所需权限"}, - "app.skin-site.chat.admin.prerequisite-hint": {"message": "以逗号分隔;留空表示无限制。"}, - "app.skin-site.chat.admin.created": {"message": "房间已创建。"}, - "app.skin-site.chat.admin.updated": {"message": "房间资料已更新。"}, - "app.skin-site.chat.admin.deleted": {"message": "房间已删除。"}, - "app.skin-site.chat.admin.confirm-delete": {"message": "确认永久删除房间“{name}”及其中全部消息吗?"}, - "app.skin-site.chat.admin.manager-badge": {"message": "管理"}, - "app.skin-site.chat.admin.voice-badge": {"message": "语音"}, - "app.skin-site.chat.user-picker.label": {"message": "选择用户"}, - "app.skin-site.chat.user-picker.placeholder": {"message": "搜索用户名、邮箱或账号 ID"}, - "app.skin-site.chat.user-picker.by-user": {"message": "按用户信息"}, - "app.skin-site.chat.user-picker.by-player": {"message": "按玩家 ID"}, - "app.skin-site.chat.user-picker.hint": {"message": "支持按用户名、邮箱模糊搜索,也可输入账号 ID 搜索。"}, - "app.skin-site.chat.user-picker.player-hint": {"message": "输入玩家名称,查找绑定该玩家 ID 的用户。"}, - "app.skin-site.chat.user-picker.empty": {"message": "没有找到匹配的用户"}, - "app.skin-site.chat.user-picker.banned": {"message": "该账号已被封禁,不能执行此操作。"}, - "app.skin-site.chat.user-picker.guest": {"message": "游客不能成为房管、聚落主或聚落成员。"}, - "app.skin-site.chat.user-picker.already-in": {"message": "该用户已经在当前房间中。"}, - "app.skin-site.chat.user-picker.not-allowed": {"message": "该用户不在当前允许选择的范围内"}, - "app.skin-site.chat.user-picker.select": {"message": "选择此用户"}, - "app.skin-site.chat.user-picker.selected": {"message": "已选择此用户"}, - "app.skin-site.chat.user-picker.id": {"message": "ID"}, - "app.skin-site.chat.admin.managers": {"message": "房管"}, - "app.skin-site.chat.admin.manager-title": {"message": "任命房管"}, - "app.skin-site.chat.admin.manager-hint": {"message": "房管可以管理该房间的成员、消息与文件。"}, - "app.skin-site.chat.admin.grant": {"message": "任命"}, - "app.skin-site.chat.admin.revoke": {"message": "撤销"}, - "app.skin-site.chat.admin.manager-granted": {"message": "已任命房管。"}, - "app.skin-site.chat.admin.manager-revoked": {"message": "已撤销房管。"}, - "app.skin-site.chat.admin.owner-title": {"message": "聚落主"}, - "app.skin-site.chat.admin.owner-hint": {"message": "为该房间任命首任聚落主。"}, - "app.skin-site.chat.admin.owner-assigned": {"message": "已任命聚落主。"}, - "app.skin-site.chat.admin.owner-current": {"message": "当前聚落主:{name}"}, - "app.skin-site.chat.admin.owner-none": {"message": "尚未设置聚落主。"}, - "app.skin-site.chat.admin.audit": {"message": "审计"}, - "app.skin-site.chat.admin.audit-title": {"message": "房间审计"}, - "app.skin-site.chat.admin.audit-empty": {"message": "暂无审计记录。"}, - "app.skin-site.chat.admin.audit-target": {"message": "对象:{name}"}, - "app.skin-site.chat.admin.close": {"message": "关闭"}, - "app.skin-site.chat.voice-runtime-title": {"message": "实时语音库"}, - "app.skin-site.chat.voice-logs": {"message": "查看日志"}, - "app.skin-site.chat.voice-config": {"message": "配置语音库"}, - "app.skin-site.chat.voice-retry": {"message": "重新部署"}, - "app.skin-site.chat.voice-config-title": {"message": "语音库配置"}, - "app.skin-site.chat.voice-logs-title": {"message": "LiveKit 日志"}, - "app.skin-site.chat.voice-capacity-title": {"message": "语音容量"}, - "app.skin-site.chat.voice-capacity": {"message": "语音容量"}, - "app.skin-site.chat.participant-limit": {"message": "参与人数上限"}, - "app.skin-site.chat.listener-limit": {"message": "旁听人数上限"}, - "app.skin-site.chat.voice-capacity-updated": {"message": "语音容量已更新。"}, - "app.skin-site.chat.voice-enabled": {"message": "启用实时语音"}, - "app.skin-site.chat.voice-runtime-path": {"message": "语音库目录"}, - "app.skin-site.chat.voice-mirror": {"message": "镜像基础地址"}, - "app.skin-site.chat.voice-public-url": {"message": "公网访问地址"}, - "app.skin-site.chat.voice-bind": {"message": "监听地址"}, - "app.skin-site.chat.voice-signal-port": {"message": "信令端口"}, - "app.skin-site.chat.voice-rtc-tcp-port": {"message": "媒体 TCP 端口"}, - "app.skin-site.chat.voice-rtc-udp-port": {"message": "媒体 UDP 端口"}, - "app.skin-site.chat.voice-startup-timeout": {"message": "启动等待时间(秒)"}, - "app.skin-site.chat.voice-use-external-ip": {"message": "使用外部 IP"}, - "app.skin-site.chat.voice-node-ip": {"message": "节点 IP"}, - "app.skin-site.chat.voice-saved": {"message": "语音库配置已保存。"}, - "app.skin-site.chat.voice-logs-auto-refresh": {"message": "自动刷新"}, - "app.skin-site.chat.voice-logs-refresh": {"message": "刷新"}, - "app.skin-site.chat.voice-logs-empty": {"message": "暂无日志。"}, - "app.skin-site.chat.admin.voice-runtime-title": {"message": "实时语音库"}, - "app.skin-site.chat.admin.voice-logs": {"message": "查看日志"}, - "app.skin-site.chat.admin.voice-config": {"message": "配置语音库"}, - "app.skin-site.chat.admin.voice-retry": {"message": "重新部署"}, - "app.skin-site.chat.admin.voice-config-title": {"message": "语音库配置"}, - "app.skin-site.chat.admin.voice-logs-title": {"message": "LiveKit 日志"}, - "app.skin-site.chat.admin.voice-capacity-title": {"message": "语音容量"}, - "app.skin-site.chat.admin.voice-capacity": {"message": "语音容量"}, - "app.skin-site.chat.admin.participant-limit": {"message": "参与人数上限"}, - "app.skin-site.chat.admin.listener-limit": {"message": "旁听人数上限"}, - "app.skin-site.chat.admin.voice-capacity-updated": {"message": "语音容量已更新。"}, - "app.skin-site.chat.admin.voice-enabled": {"message": "启用实时语音"}, - "app.skin-site.chat.admin.voice-runtime-path": {"message": "语音库目录"}, - "app.skin-site.chat.admin.voice-mirror": {"message": "镜像基础地址"}, - "app.skin-site.chat.admin.voice-public-url": {"message": "公网访问地址"}, - "app.skin-site.chat.admin.voice-bind": {"message": "监听地址"}, - "app.skin-site.chat.admin.voice-signal-port": {"message": "信令端口"}, - "app.skin-site.chat.admin.voice-rtc-tcp-port": {"message": "媒体 TCP 端口"}, - "app.skin-site.chat.admin.voice-rtc-udp-port": {"message": "媒体 UDP 端口"}, - "app.skin-site.chat.admin.voice-startup-timeout": {"message": "启动等待时间(秒)"}, - "app.skin-site.chat.admin.voice-use-external-ip": {"message": "使用外部 IP"}, - "app.skin-site.chat.admin.voice-node-ip": {"message": "节点 IP"}, - "app.skin-site.chat.admin.voice-saved": {"message": "语音库配置已保存。"}, - "app.skin-site.chat.admin.voice-logs-auto-refresh": {"message": "自动刷新"}, - "app.skin-site.chat.admin.voice-logs-refresh": {"message": "刷新"}, - "app.skin-site.chat.admin.voice-logs-empty": {"message": "暂无日志。"}, - "app.skin-site.admin.wiki.tab-migration": {"message": "迁移中心"}, - "app.skin-site.admin.wiki.migration-title": {"message": "MediaWiki 迁移"}, - "app.skin-site.admin.wiki.migration-hint": {"message": "迁移内容会先进入不可见暂存区,所有页面完成导入或排除审核后才能整批发布。"}, - "app.skin-site.admin.wiki.auto-title": {"message": "从 MediaWiki API 自动读取"}, - "app.skin-site.admin.wiki.api-url": {"message": "api.php 地址"}, - "app.skin-site.admin.wiki.bot-username": {"message": "Bot 用户名(选填)"}, - "app.skin-site.admin.wiki.bot-password": {"message": "Bot 密码(选填)"}, - "app.skin-site.admin.wiki.credentials-hint": {"message": "凭据只存在于本次后台任务的内存中,携带凭据时必须使用 HTTPS。"}, - "app.skin-site.admin.wiki.allow-private": {"message": "明确允许访问内网来源"}, - "app.skin-site.admin.wiki.allow-private-hint": {"message": "内网来源可能访问皮肤站所在网络中的服务,请只对可信地址启用。"}, - "app.skin-site.admin.wiki.start-auto": {"message": "创建自动迁移任务"}, - "app.skin-site.admin.wiki.xml-title": {"message": "导入 MediaWiki XML"}, - "app.skin-site.admin.wiki.xml-file": {"message": "导出文件"}, - "app.skin-site.admin.wiki.start-xml": {"message": "上传并建立暂存任务"}, - "app.skin-site.admin.wiki.stale-rules": {"message": "疑似过时页面筛选规则"}, - "app.skin-site.admin.wiki.stale-days": {"message": "未更新天数"}, - "app.skin-site.admin.wiki.maintainer-window": {"message": "维护者统计天数"}, - "app.skin-site.admin.wiki.max-maintainers": {"message": "维护者人数不超过"}, - "app.skin-site.admin.wiki.stale-markers": {"message": "过时分类、模板或文字标记"}, - "app.skin-site.admin.wiki.stale-markers-hint": {"message": "使用逗号分隔"}, - "app.skin-site.admin.wiki.jobs": {"message": "迁移任务"}, - "app.skin-site.admin.wiki.refresh-jobs": {"message": "刷新"}, - "app.skin-site.admin.wiki.jobs-empty": {"message": "还没有迁移任务。"}, - "app.skin-site.admin.wiki.reviewed-count": {"message": "已审核 {reviewed} / {total}"}, - "app.skin-site.admin.wiki.imported-excluded": {"message": "导入 {imported} · 排除 {excluded}"}, - "app.skin-site.admin.wiki.view-job": {"message": "查看任务"}, - "app.skin-site.admin.wiki.delete-job": {"message": "删除迁移记录"}, - "app.skin-site.admin.wiki.confirm-delete-job": {"message": "确认删除迁移任务 #{id}?此操作不可恢复。"}, - "app.skin-site.admin.wiki.candidates": {"message": "候选页面"}, - "app.skin-site.admin.wiki.candidates-empty": {"message": "选择任务后查看其页面。"}, - "app.skin-site.admin.wiki.last-updated": {"message": "更新于 {date}"}, - "app.skin-site.admin.wiki.maintainer-count": {"message": "{count} 位维护者"}, - "app.skin-site.admin.wiki.conflict": {"message": "标题冲突"}, - "app.skin-site.admin.wiki.target-title": {"message": "目标标题"}, - "app.skin-site.admin.wiki.overwrite": {"message": "覆盖"}, - "app.skin-site.admin.wiki.import": {"message": "导入"}, - "app.skin-site.admin.wiki.exclude": {"message": "排除"}, - "app.skin-site.admin.wiki.publish": {"message": "整批发布"}, - "app.skin-site.admin.wiki.rollback": {"message": "回滚"}, - "app.skin-site.admin.wiki.confirm-rollback": {"message": "确认回滚本批迁移?已发布的页面将被还原。"}, - "app.skin-site.admin.wiki.state-importing": {"message": "正在读取"}, - "app.skin-site.admin.wiki.state-review": {"message": "等待审核"}, - "app.skin-site.admin.wiki.state-publishing": {"message": "正在发布"}, - "app.skin-site.admin.wiki.state-published": {"message": "已发布"}, - "app.skin-site.admin.wiki.state-failed": {"message": "失败"}, - "app.skin-site.admin.wiki.state-rolled-back": {"message": "已回滚"}, - "app.skin-site.admin.wiki.decision-pending": {"message": "待审核"}, - "app.skin-site.admin.wiki.decision-import": {"message": "导入"}, - "app.skin-site.admin.wiki.decision-exclude": {"message": "排除"}, - "app.skin-site.admin.wiki.migration-started": {"message": "迁移任务已创建。"}, - "app.skin-site.admin.wiki.xml-staged": {"message": "XML 已解析并进入逐页审核阶段。"}, - "app.skin-site.admin.wiki.published": {"message": "已发布 {imported} 个页面,覆盖 {overwritten} 个。"}, - "app.skin-site.admin.wiki.rolled-back": {"message": "迁移批次已安全回滚。"}, - "app.skin-site.admin.wiki.job-deleted": {"message": "迁移记录已删除。"}, - "app.skin-site.admin.wiki.select-xml": {"message": "请先选择 MediaWiki XML 文件。"}, - "app.skin-site.wiki.title": {"message": "StarLight 维基"}, - "app.skin-site.wiki.search-placeholder": {"message": "输入页面标题"}, - "app.skin-site.wiki.open": {"message": "打开"}, - "app.skin-site.wiki.loading": {"message": "加载中…"}, - "app.skin-site.wiki.not-found": {"message": "页面不存在。"}, - "app.skin-site.wiki.empty": {"message": "在上方输入页面标题开始阅读。"}, - "app.skin-site.wiki.updated-at": {"message": "最后编辑于 {date},编辑者 {author}"}, - "app.skin-site.wiki.revision": {"message": "修订 #{id}"}, - "app.skin-site.wiki.categories": {"message": "分类"}, - "app.skin-site.wiki.back": {"message": "返回"} + "app.settings.synced-options.choose-sync-source.command-history-description": { + "message": "选择要从哪个实例复制命令历史记录。这些记录仅用于初始同步,之后可从任意实例编辑。" + }, + "app.settings.synced-options.choose-sync-source.command-history-title": { + "message": "选择命令历史记录来源" + }, + "app.settings.synced-options.choose-sync-source.creative-hotbars-description": { + "message": "选择要从哪个实例复制保存的创造模式快捷栏。这些快捷栏仅用于初始同步,之后可从任意实例编辑。" + }, + "app.settings.synced-options.choose-sync-source.creative-hotbars-title": { + "message": "选择创造模式快捷栏来源" + }, + "app.settings.synced-options.choose-sync-source.data-packs-title": { + "message": "选择数据包来源" + }, + "app.settings.synced-options.choose-sync-source.game-settings-description": { + "message": "选择要从哪个实例复制游戏设置。这些设置仅用于初始同步,之后可从任意实例编辑。" + }, + "app.settings.synced-options.choose-sync-source.game-settings-title": { + "message": "选择游戏设置来源" + }, + "app.settings.synced-options.choose-sync-source.ineligible": { + "message": "无法选择此实例作为同步来源" + }, + "app.settings.synced-options.choose-sync-source.load-error": { + "message": "无法加载同步来源,请重试。" + }, + "app.settings.synced-options.choose-sync-source.multiplayer-servers-description": { + "message": "选择要从哪个实例复制多人服务器列表。这些服务器仅用于初始同步,之后可从任意实例编辑。" + }, + "app.settings.synced-options.choose-sync-source.multiplayer-servers-title": { + "message": "选择多人服务器来源" + }, + "app.settings.synced-options.choose-sync-source.no-instances-found": { + "message": "没有找到实例" + }, + "app.settings.synced-options.choose-sync-source.resource-packs-title": { + "message": "选择资源包来源" + }, + "app.settings.synced-options.choose-sync-source.retry": { "message": "重试" }, + "app.settings.synced-options.choose-sync-source.search-placeholder": { + "message": "搜索实例" + }, + "app.settings.synced-options.choose-sync-source.sync": { "message": "同步" }, + "app.settings.synced-options.command-history": { "message": "同步命令记录" }, + "app.settings.synced-options.command-history.description": { + "message": "在不同的实例之间同步命令记录。" + }, + "app.settings.synced-options.command-history.editor-title": { + "message": "编辑指令历史记录" + }, + "app.settings.synced-options.creative-hotbars": { + "message": "同步保存的创造模式物品栏" + }, + "app.settings.synced-options.creative-hotbars.description": { + "message": "在不同的实例之间同步创造模式物品栏。" + }, + "app.settings.synced-options.data-packs": { "message": "同步数据包" }, + "app.settings.synced-options.data-packs.description": { + "message": "在所有实例间使用相同的数据包" + }, + "app.settings.synced-options.data-packs.none-synced-yet": { + "message": "尚未同步任何数据包" + }, + "app.settings.synced-options.edit.no-instances": { + "message": "请先添加实例,再编辑同步数据。" + }, + "app.settings.synced-options.edit.no-synced-data": { + "message": "请先选择同步来源,再编辑此设置。" + }, + "app.settings.synced-options.game-settings": { "message": "同步游戏设置" }, + "app.settings.synced-options.game-settings.button": { + "message": "编辑游戏设置" + }, + "app.settings.synced-options.game-settings.description": { + "message": "在所有实例间使用相同的游戏设置。" + }, + "app.settings.synced-options.game-settings.disabled-tooltip": { + "message": "请先启用游戏设置同步,再选择单个设置。" + }, + "app.settings.synced-options.game-settings.no-options-to-edit": { + "message": "目前没有可编辑的设置" + }, + "app.settings.synced-options.multiplayer-servers": { + "message": "同步多人模式服务器列表" + }, + "app.settings.synced-options.multiplayer-servers.address": { + "message": "服务器地址" + }, + "app.settings.synced-options.multiplayer-servers.copy-address": { + "message": "复制地址" + }, + "app.settings.synced-options.multiplayer-servers.count": { + "message": "{count, number} 个服务器" + }, + "app.settings.synced-options.multiplayer-servers.description": { + "message": "在不同的实例直接同步多人模式服务器列表。" + }, + "app.settings.synced-options.multiplayer-servers.editor-title": { + "message": "编辑同步的服务器" + }, + "app.settings.synced-options.multiplayer-servers.empty": { + "message": "当前未同步任何用户添加的服务器。" + }, + "app.settings.synced-options.multiplayer-servers.filter-offline": { + "message": "离线" + }, + "app.settings.synced-options.multiplayer-servers.filter-online": { + "message": "在线" + }, + "app.settings.synced-options.multiplayer-servers.more-options": { + "message": "更多服务器选项" + }, + "app.settings.synced-options.multiplayer-servers.name": { + "message": "服务器名称" + }, + "app.settings.synced-options.multiplayer-servers.no-results": { + "message": "没有匹配搜索或筛选条件的已同步服务器。" + }, + "app.settings.synced-options.multiplayer-servers.none-synced-yet": { + "message": "暂无已同步的服务器" + }, + "app.settings.synced-options.multiplayer-servers.players-online": { + "message": "{count, number} 人在线" + }, + "app.settings.synced-options.multiplayer-servers.search": { + "message": "搜索 {count, number} 个已同步服务器……" + }, + "app.settings.synced-options.multiplayer-servers.status": { + "message": "状态" + }, + "app.settings.synced-options.packs.disabled": { + "message": "请先启用同步,再编辑共享包。" + }, + "app.settings.synced-options.resource-packs": { "message": "同步资源包" }, + "app.settings.synced-options.resource-packs.description": { + "message": "在所有实例间使用相同的资源包" + }, + "app.settings.synced-options.resource-packs.none-synced-yet": { + "message": "尚未同步任何资源包" + }, + "app.settings.synced-options.title": { "message": "跨所有实例同步的选项" }, + "app.settings.synced-packs.data-packs": { "message": "数据包" }, + "app.settings.synced-packs.disable": { "message": "禁用" }, + "app.settings.synced-packs.empty": { "message": "没有已同步的包" }, + "app.settings.synced-packs.enable": { "message": "启用" }, + "app.settings.synced-packs.remove": { "message": "移除" }, + "app.settings.synced-packs.resource-packs": { "message": "资源包" }, + "app.sync-instances-update.choose-source.data-packs": { + "message": "选择要从哪个实例复制数据包。这些包仅用于初始化同步,之后可从任意实例进行编辑。" + }, + "app.sync-instances-update.choose-source.resource-packs": { + "message": "选择要从哪个实例复制资源包。这些包仅用于初始化同步,之后可从任意实例进行编辑。" + }, + "instance.settings.tabs.backups": { "message": "备份" }, + "instance.settings.tabs.sync": { "message": "同步" }, + "app.settings.game-options.category.accessibility.description": { + "message": "无障碍设置" + }, + "app.settings.game-options.category.accessibility.label": { + "message": "无障碍" + }, + "app.settings.game-options.category.chat.description": { + "message": "聊天显示与外观" + }, + "app.settings.game-options.category.chat.label": { "message": "聊天" }, + "app.settings.game-options.category.controls.description": { + "message": "鼠标、移动与按键绑定" + }, + "app.settings.game-options.category.controls.label": { "message": "控制" }, + "app.settings.game-options.category.custom.description": { + "message": "由模组添加的设置" + }, + "app.settings.game-options.category.custom.label": { + "message": "自定义设置" + }, + "app.settings.game-options.category.language.description": { + "message": "游戏语言" + }, + "app.settings.game-options.category.language.label": { "message": "语言" }, + "app.settings.game-options.category.music-and-sound.description": { + "message": "音量与音频偏好" + }, + "app.settings.game-options.category.music-and-sound.label": { + "message": "音乐和声音" + }, + "app.settings.game-options.category.online.description": { + "message": "在线与 Realms 偏好" + }, + "app.settings.game-options.category.online.label": { "message": "在线" }, + "app.settings.game-options.category.skin-customization.description": { + "message": "皮肤层与主手" + }, + "app.settings.game-options.category.skin-customization.label": { + "message": "皮肤自定义" + }, + "app.settings.game-options.category.video.description": { + "message": "镜头与显示设置" + }, + "app.settings.game-options.category.video.label": { "message": "视频" }, + "app.settings.game-options.choice.all": { "message": "全部" }, + "app.settings.game-options.choice.balanced": { "message": "平衡" }, + "app.settings.game-options.choice.by-player": { "message": "按玩家" }, + "app.settings.game-options.choice.chat": { "message": "聊天" }, + "app.settings.game-options.choice.commands-only": { "message": "仅命令" }, + "app.settings.game-options.choice.constant": { "message": "持续" }, + "app.settings.game-options.choice.crosshair": { "message": "准星" }, + "app.settings.game-options.choice.custom": { "message": "自定义" }, + "app.settings.game-options.choice.decreased": { "message": "降低" }, + "app.settings.game-options.choice.default": { "message": "默认" }, + "app.settings.game-options.choice.fabulous": { "message": "极佳" }, + "app.settings.game-options.choice.fancy": { "message": "精美" }, + "app.settings.game-options.choice.far": { "message": "远" }, + "app.settings.game-options.choice.fast": { "message": "快速" }, + "app.settings.game-options.choice.frequent": { "message": "频繁" }, + "app.settings.game-options.choice.hidden": { "message": "隐藏" }, + "app.settings.game-options.choice.hotbar": { "message": "快捷栏" }, + "app.settings.game-options.choice.left": { "message": "左" }, + "app.settings.game-options.choice.limited": { "message": "有限" }, + "app.settings.game-options.choice.max-fps": { "message": "最高 FPS" }, + "app.settings.game-options.choice.maximum": { "message": "最大" }, + "app.settings.game-options.choice.minimal": { "message": "最少" }, + "app.settings.game-options.choice.minimum": { "message": "最小" }, + "app.settings.game-options.choice.nearby": { "message": "附近" }, + "app.settings.game-options.choice.never": { "message": "从不" }, + "app.settings.game-options.choice.none": { "message": "无" }, + "app.settings.game-options.choice.normal": { "message": "正常" }, + "app.settings.game-options.choice.off": { "message": "关闭" }, + "app.settings.game-options.choice.on": { "message": "开启" }, + "app.settings.game-options.choice.opengl": { "message": "OpenGL 渲染器" }, + "app.settings.game-options.choice.pause": { "message": "暂停菜单" }, + "app.settings.game-options.choice.pause-and-toast": { + "message": "暂停菜单和提示" + }, + "app.settings.game-options.choice.power-saver": { "message": "省电" }, + "app.settings.game-options.choice.right": { "message": "右" }, + "app.settings.game-options.choice.short": { "message": "短" }, + "app.settings.game-options.choice.shown": { "message": "显示" }, + "app.settings.game-options.choice.system": { "message": "系统" }, + "app.settings.game-options.choice.tiny": { "message": "极小" }, + "app.settings.game-options.choice.vulkan": { "message": "Vulkan 渲染器" }, + "app.settings.game-options.choice.when-minimized": { "message": "最小化时" }, + "app.settings.game-options.choice.while-afk": { "message": "挂机时" }, + "app.settings.game-options.compatibility.none": { + "message": "部分实例无法使用此设置" + }, + "app.settings.game-options.compatibility.reason.launcher-controlled": { + "message": "此设置由启动器设置管理" + }, + "app.settings.game-options.custom-value.placeholder": { "message": "输入值" }, + "app.settings.game-options.keybind.assigned-status": { + "message": "已将 {setting} 绑定为 {binding}。" + }, + "app.settings.game-options.keybind.cancelled-status": { + "message": "已取消按键绑定更改。" + }, + "app.settings.game-options.keybind.change": { + "message": "{setting}:{binding}。激活以更改绑定。" + }, + "app.settings.game-options.keybind.choose": { "message": "选择按键" }, + "app.settings.game-options.keybind.conflict-description": { + "message": "{settings} 已使用此按键。" + }, + "app.settings.game-options.keybind.key.arrow-down": { "message": "向下箭头" }, + "app.settings.game-options.keybind.key.arrow-left": { "message": "向左箭头" }, + "app.settings.game-options.keybind.key.arrow-right": { + "message": "向右箭头" + }, + "app.settings.game-options.keybind.key.arrow-up": { "message": "向上箭头" }, + "app.settings.game-options.keybind.key.backspace": { "message": "退格" }, + "app.settings.game-options.keybind.key.caps-lock": { "message": "大写锁定" }, + "app.settings.game-options.keybind.key.delete": { "message": "删除" }, + "app.settings.game-options.keybind.key.end": { "message": "End 键" }, + "app.settings.game-options.keybind.key.enter": { "message": "回车" }, + "app.settings.game-options.keybind.key.escape": { "message": "Esc 键" }, + "app.settings.game-options.keybind.key.home": { "message": "Home 键" }, + "app.settings.game-options.keybind.key.insert": { "message": "Insert 键" }, + "app.settings.game-options.keybind.key.keypad-key": { + "message": "数字键盘 {key}" + }, + "app.settings.game-options.keybind.key.left-alt": { "message": "左 Alt" }, + "app.settings.game-options.keybind.key.left-command": { + "message": "左 Command" + }, + "app.settings.game-options.keybind.key.left-control": { + "message": "左 Ctrl" + }, + "app.settings.game-options.keybind.key.left-shift": { "message": "左 Shift" }, + "app.settings.game-options.keybind.key.left-super": { "message": "左 Super" }, + "app.settings.game-options.keybind.key.menu": { "message": "菜单" }, + "app.settings.game-options.keybind.key.num-lock": { "message": "数字锁定" }, + "app.settings.game-options.keybind.key.page-down": { + "message": "Page Down 键" + }, + "app.settings.game-options.keybind.key.page-up": { "message": "Page Up 键" }, + "app.settings.game-options.keybind.key.pause": { "message": "暂停" }, + "app.settings.game-options.keybind.key.print-screen": { "message": "截图" }, + "app.settings.game-options.keybind.key.right-alt": { "message": "右 Alt" }, + "app.settings.game-options.keybind.key.right-command": { + "message": "右 Command" + }, + "app.settings.game-options.keybind.key.right-control": { + "message": "右 Ctrl" + }, + "app.settings.game-options.keybind.key.right-shift": { + "message": "右 Shift" + }, + "app.settings.game-options.keybind.key.right-super": { + "message": "右 Super" + }, + "app.settings.game-options.keybind.key.scroll-lock": { + "message": "滚动锁定" + }, + "app.settings.game-options.keybind.key.space": { "message": "空格" }, + "app.settings.game-options.keybind.key.tab": { "message": "Tab 键" }, + "app.settings.game-options.keybind.key.unbound": { "message": "未绑定" }, + "app.settings.game-options.keybind.key.unknown": { "message": "未知按键" }, + "app.settings.game-options.keybind.listen": { "message": "请按下一个按键……" }, + "app.settings.game-options.keybind.listening-status": { + "message": "正在监听按键或鼠标按钮。按 Esc 清除绑定。" + }, + "app.settings.game-options.keybind.mixed": { "message": "各实例不同" }, + "app.settings.game-options.keybind.modifier.alt": { "message": "Alt 键" }, + "app.settings.game-options.keybind.modifier.command": { + "message": "Command 键" + }, + "app.settings.game-options.keybind.modifier.control": { + "message": "Ctrl 键" + }, + "app.settings.game-options.keybind.modifier.shift": { "message": "Shift 键" }, + "app.settings.game-options.keybind.modifier.super": { "message": "Super 键" }, + "app.settings.game-options.keybind.mouse.button": { + "message": "鼠标 {button}" + }, + "app.settings.game-options.keybind.mouse.left": { "message": "鼠标左键" }, + "app.settings.game-options.keybind.mouse.middle": { "message": "鼠标中键" }, + "app.settings.game-options.keybind.mouse.right": { "message": "鼠标右键" }, + "app.settings.game-options.keybind.recording-label": { + "message": "{setting}:正在监听按键或鼠标按钮。按 Esc 清除绑定。" + }, + "app.settings.game-options.keybind.scancode": { "message": "扫描码 {code}" }, + "app.settings.game-options.keybind.unsupported-status": { + "message": "Minecraft 无法使用此输入,请尝试其他按键。" + }, + "app.settings.game-options.setting.advanced-opengl.label": { + "message": "高级 OpenGL" + }, + "app.settings.game-options.setting.allow-cursor-changes.label": { + "message": "允许光标变化" + }, + "app.settings.game-options.setting.allow-server-listing.description": { + "message": "允许玩家名称出现在服务器列表中。" + }, + "app.settings.game-options.setting.allow-server-listing.label": { + "message": "服务器列表" + }, + "app.settings.game-options.setting.alternate-blocks.label": { + "message": "备用方块" + }, + "app.settings.game-options.setting.ambient-occlusion.label": { + "message": "平滑光照" + }, + "app.settings.game-options.setting.ambient-volume.label": { + "message": "环境" + }, + "app.settings.game-options.setting.anaglyph-3d.label": { + "message": "3D 红蓝立体" + }, + "app.settings.game-options.setting.anisotropic-filtering.label": { + "message": "各向异性过滤" + }, + "app.settings.game-options.setting.anisotropy.label": { + "message": "各向异性" + }, + "app.settings.game-options.setting.attack-indicator.label": { + "message": "攻击指示器" + }, + "app.settings.game-options.setting.auto-jump.description": { + "message": "自动跳过一格高的障碍物。" + }, + "app.settings.game-options.setting.auto-jump.label": { + "message": "自动跳跃" + }, + "app.settings.game-options.setting.auto-suggestions.label": { + "message": "命令建议" + }, + "app.settings.game-options.setting.autosave-indicator.label": { + "message": "自动保存指示器" + }, + "app.settings.game-options.setting.biome-blend-radius.description": { + "message": "生物群系颜色过渡的距离。" + }, + "app.settings.game-options.setting.biome-blend-radius.label": { + "message": "生物群系混合" + }, + "app.settings.game-options.setting.blocks-volume.label": { + "message": "方块" + }, + "app.settings.game-options.setting.brightness.label": { "message": "亮度" }, + "app.settings.game-options.setting.cape.description": { + "message": "显示玩家的披风,包括鞘翅纹理。" + }, + "app.settings.game-options.setting.cape.label": { "message": "披风" }, + "app.settings.game-options.setting.chat-background-only.label": { + "message": "仅聊天背景" + }, + "app.settings.game-options.setting.chat-colors.label": { + "message": "聊天颜色" + }, + "app.settings.game-options.setting.chat-delay.label": { + "message": "聊天延迟" + }, + "app.settings.game-options.setting.chat-line-spacing.label": { + "message": "聊天行间距" + }, + "app.settings.game-options.setting.chat-links-prompt.description": { + "message": "打开聊天链接前询问" + }, + "app.settings.game-options.setting.chat-links-prompt.label": { + "message": "打开链接前询问" + }, + "app.settings.game-options.setting.chat-links.description": { + "message": "允许打开聊天中的网页链接。" + }, + "app.settings.game-options.setting.chat-links.label": { + "message": "网页链接" + }, + "app.settings.game-options.setting.chat-opacity.description": { + "message": "聊天文字的不透明度。" + }, + "app.settings.game-options.setting.chat-opacity.label": { + "message": "聊天不透明度" + }, + "app.settings.game-options.setting.chat-preview.label": { + "message": "聊天预览" + }, + "app.settings.game-options.setting.chat-scale.label": { + "message": "聊天缩放" + }, + "app.settings.game-options.setting.chat-visibility.label": { + "message": "聊天可见性" + }, + "app.settings.game-options.setting.chat-width.label": { + "message": "聊天宽度" + }, + "app.settings.game-options.setting.chunk-fade-time.label": { + "message": "区块淡出时间" + }, + "app.settings.game-options.setting.cloud-range.label": { + "message": "云层距离" + }, + "app.settings.game-options.setting.clouds.label": { "message": "云" }, + "app.settings.game-options.setting.ctrl-click-right-click.label": { + "message": "按住 Control 点击代替右键点击" + }, + "app.settings.game-options.setting.cutout-leaves.label": { + "message": "半透明树叶" + }, + "app.settings.game-options.setting.damage-tilt.label": { + "message": "受伤倾斜" + }, + "app.settings.game-options.setting.dark-splash.description": { + "message": "将 Mojang Studios 加载界面从红色改为黑色。" + }, + "app.settings.game-options.setting.dark-splash.label": { + "message": "单色徽标" + }, + "app.settings.game-options.setting.darkness-pulsing.label": { + "message": "黑暗脉动" + }, + "app.settings.game-options.setting.debug-gui-scale.label": { + "message": "调试界面缩放" + }, + "app.settings.game-options.setting.directional-audio.label": { + "message": "方向性音频" + }, + "app.settings.game-options.setting.discrete-mouse-scroll.description": { + "message": "将每次鼠标滚轮输入视为一个滚动步进。" + }, + "app.settings.game-options.setting.discrete-mouse-scroll.label": { + "message": "离散滚动" + }, + "app.settings.game-options.setting.entity-distance.label": { + "message": "实体距离" + }, + "app.settings.game-options.setting.entity-shadows.label": { + "message": "实体阴影" + }, + "app.settings.game-options.setting.exclusive-fullscreen.label": { + "message": "独占全屏" + }, + "app.settings.game-options.setting.extra-telemetry.label": { + "message": "可选遥测" + }, + "app.settings.game-options.setting.focused-chat-height.label": { + "message": "聚焦时聊天高度" + }, + "app.settings.game-options.setting.force-unicode-font.label": { + "message": "强制 Unicode 字体" + }, + "app.settings.game-options.setting.fov-effects.label": { + "message": "视野效果" + }, + "app.settings.game-options.setting.fov.label": { "message": "视野" }, + "app.settings.game-options.setting.fullscreen.label": { "message": "全屏" }, + "app.settings.game-options.setting.glint-speed.label": { + "message": "附魔光效速度" + }, + "app.settings.game-options.setting.glint-strength.label": { + "message": "附魔光效强度" + }, + "app.settings.game-options.setting.graphics-backend.label": { + "message": "图形后端" + }, + "app.settings.game-options.setting.graphics.description": { + "message": "控制画面质量和性能。" + }, + "app.settings.game-options.setting.graphics.label": { "message": "画面" }, + "app.settings.game-options.setting.gui-scale.description": { + "message": "游戏界面和 HUD 的大小。" + }, + "app.settings.game-options.setting.gui-scale.label": { + "message": "界面缩放" + }, + "app.settings.game-options.setting.hat.description": { + "message": "显示帽子皮肤层。" + }, + "app.settings.game-options.setting.hat.label": { "message": "帽子" }, + "app.settings.game-options.setting.held-item-tooltips.label": { + "message": "手持物品提示" + }, + "app.settings.game-options.setting.hide-lightning-flashes.label": { + "message": "隐藏闪电闪光" + }, + "app.settings.game-options.setting.hide-matched-names.label": { + "message": "隐藏匹配的名称" + }, + "app.settings.game-options.setting.hide-server-address.label": { + "message": "隐藏服务器地址" + }, + "app.settings.game-options.setting.hide-splash-texts.label": { + "message": "隐藏启动标语" + }, + "app.settings.game-options.setting.high-contrast-outline.label": { + "message": "高对比度方块轮廓" + }, + "app.settings.game-options.setting.high-contrast.description": { + "message": "增强界面元素的对比度。" + }, + "app.settings.game-options.setting.high-contrast.label": { + "message": "高对比度" + }, + "app.settings.game-options.setting.hostile-volume.label": { + "message": "敌对生物" + }, + "app.settings.game-options.setting.improved-transparency.label": { + "message": "改进透明度" + }, + "app.settings.game-options.setting.in-game-notifications.label": { + "message": "游戏内通知" + }, + "app.settings.game-options.setting.inactivity-framerate-limit.label": { + "message": "降低帧率" + }, + "app.settings.game-options.setting.invert-horizontal-mouse.label": { + "message": "反转水平鼠标" + }, + "app.settings.game-options.setting.invert-mouse.description": { + "message": "反转垂直鼠标移动。" + }, + "app.settings.game-options.setting.invert-mouse.label": { + "message": "反转鼠标" + }, + "app.settings.game-options.setting.jacket.description": { + "message": "显示夹克皮肤层。" + }, + "app.settings.game-options.setting.jacket.label": { "message": "夹克" }, + "app.settings.game-options.setting.japanese-glyph-variants.label": { + "message": "日文字符变体" + }, + "app.settings.game-options.setting.key-advancements.label": { + "message": "进度" + }, + "app.settings.game-options.setting.key-attack.label": { "message": "攻击" }, + "app.settings.game-options.setting.key-back.label": { "message": "后退" }, + "app.settings.game-options.setting.key-chat.label": { "message": "打开聊天" }, + "app.settings.game-options.setting.key-command.label": { "message": "命令" }, + "app.settings.game-options.setting.key-debug-advanced-tooltips.label": { + "message": "显示高级提示" + }, + "app.settings.game-options.setting.key-debug-chunk-borders.label": { + "message": "显示区块边界" + }, + "app.settings.game-options.setting.key-debug-clear-chat.label": { + "message": "清空聊天" + }, + "app.settings.game-options.setting.key-debug-copy-location.label": { + "message": "复制位置" + }, + "app.settings.game-options.setting.key-debug-copy-recreate-command.label": { + "message": "复制重建命令" + }, + "app.settings.game-options.setting.key-debug-crash.label": { + "message": "触发调试崩溃" + }, + "app.settings.game-options.setting.key-debug-dump-dynamic-textures.label": { + "message": "转储动态纹理" + }, + "app.settings.game-options.setting.key-debug-dump-version.label": { + "message": "转储版本" + }, + "app.settings.game-options.setting.key-debug-focus-pause.label": { + "message": "失去焦点时暂停" + }, + "app.settings.game-options.setting.key-debug-fps-charts.label": { + "message": "FPS 图表" + }, + "app.settings.game-options.setting.key-debug-hitboxes.label": { + "message": "显示碰撞箱" + }, + "app.settings.game-options.setting.key-debug-improved-transparency.label": { + "message": "改进透明度调试视图" + }, + "app.settings.game-options.setting.key-debug-lightmap-texture.label": { + "message": "光照贴图纹理" + }, + "app.settings.game-options.setting.key-debug-modifier.label": { + "message": "调试修饰键" + }, + "app.settings.game-options.setting.key-debug-network-charts.label": { + "message": "网络图表" + }, + "app.settings.game-options.setting.key-debug-options.label": { + "message": "调试选项" + }, + "app.settings.game-options.setting.key-debug-overlay.label": { + "message": "调试覆盖层" + }, + "app.settings.game-options.setting.key-debug-profiling-chart.label": { + "message": "性能分析图表" + }, + "app.settings.game-options.setting.key-debug-profiling.label": { + "message": "开始性能分析" + }, + "app.settings.game-options.setting.key-debug-reload-chunks.label": { + "message": "重新加载区块" + }, + "app.settings.game-options.setting.key-debug-reload-resource-packs.label": { + "message": "重新加载资源包" + }, + "app.settings.game-options.setting.key-debug-spectate.label": { + "message": "旁观实体" + }, + "app.settings.game-options.setting.key-debug-switch-game-mode.label": { + "message": "切换游戏模式" + }, + "app.settings.game-options.setting.key-drop.label": { "message": "丢弃物品" }, + "app.settings.game-options.setting.key-forward.label": { "message": "前进" }, + "app.settings.game-options.setting.key-friends.label": { "message": "好友" }, + "app.settings.game-options.setting.key-fullscreen.label": { + "message": "切换全屏" + }, + "app.settings.game-options.setting.key-hotbar-1.label": { + "message": "快捷栏 1" + }, + "app.settings.game-options.setting.key-hotbar-2.label": { + "message": "快捷栏 2" + }, + "app.settings.game-options.setting.key-hotbar-3.label": { + "message": "快捷栏 3" + }, + "app.settings.game-options.setting.key-hotbar-4.label": { + "message": "快捷栏 4" + }, + "app.settings.game-options.setting.key-hotbar-5.label": { + "message": "快捷栏 5" + }, + "app.settings.game-options.setting.key-hotbar-6.label": { + "message": "快捷栏 6" + }, + "app.settings.game-options.setting.key-hotbar-7.label": { + "message": "快捷栏 7" + }, + "app.settings.game-options.setting.key-hotbar-8.label": { + "message": "快捷栏 8" + }, + "app.settings.game-options.setting.key-hotbar-9.label": { + "message": "快捷栏 9" + }, + "app.settings.game-options.setting.key-inventory.label": { + "message": "物品栏" + }, + "app.settings.game-options.setting.key-jump.label": { "message": "跳跃" }, + "app.settings.game-options.setting.key-left.label": { "message": "向左平移" }, + "app.settings.game-options.setting.key-load-toolbar.label": { + "message": "加载工具栏" + }, + "app.settings.game-options.setting.key-perspective.label": { + "message": "切换视角" + }, + "app.settings.game-options.setting.key-pick-item.label": { + "message": "选取方块" + }, + "app.settings.game-options.setting.key-player-list.label": { + "message": "玩家列表" + }, + "app.settings.game-options.setting.key-quick-actions.label": { + "message": "快捷操作" + }, + "app.settings.game-options.setting.key-right.label": { + "message": "向右平移" + }, + "app.settings.game-options.setting.key-save-toolbar.label": { + "message": "保存工具栏" + }, + "app.settings.game-options.setting.key-screenshot.label": { + "message": "截图" + }, + "app.settings.game-options.setting.key-smooth-camera.label": { + "message": "切换电影视角" + }, + "app.settings.game-options.setting.key-sneak.label": { "message": "潜行" }, + "app.settings.game-options.setting.key-social-interactions.label": { + "message": "社交互动" + }, + "app.settings.game-options.setting.key-spectator-hotbar.label": { + "message": "旁观者快捷栏" + }, + "app.settings.game-options.setting.key-spectator-outlines.label": { + "message": "高亮旁观者" + }, + "app.settings.game-options.setting.key-sprint.label": { "message": "疾跑" }, + "app.settings.game-options.setting.key-swap-offhand.label": { + "message": "交换副手" + }, + "app.settings.game-options.setting.key-toggle-gui.label": { + "message": "切换 HUD" + }, + "app.settings.game-options.setting.key-toggle-spectator-shader.label": { + "message": "切换旁观者着色器" + }, + "app.settings.game-options.setting.key-use.label": { "message": "使用物品" }, + "app.settings.game-options.setting.language.label": { "message": "语言" }, + "app.settings.game-options.setting.left-pants-leg.label": { + "message": "左裤腿" + }, + "app.settings.game-options.setting.left-sleeve.label": { "message": "左袖" }, + "app.settings.game-options.setting.legacy-framerate-limit.label": { + "message": "帧率上限" + }, + "app.settings.game-options.setting.legacy-view-distance.label": { + "message": "视距" + }, + "app.settings.game-options.setting.mac-fullscreen-menu.label": { + "message": "全屏时显示 macOS 菜单" + }, + "app.settings.game-options.setting.main-hand.description": { + "message": "选择主手为左手还是右手。" + }, + "app.settings.game-options.setting.main-hand.label": { "message": "主手" }, + "app.settings.game-options.setting.master-volume.label": { + "message": "主音量" + }, + "app.settings.game-options.setting.max-framerate.label": { + "message": "最大帧率" + }, + "app.settings.game-options.setting.menu-background-blur.label": { + "message": "菜单背景模糊" + }, + "app.settings.game-options.setting.mipmap-levels.description": { + "message": "平滑远处纹理。" + }, + "app.settings.game-options.setting.mipmap-levels.label": { + "message": "Mipmap 级别" + }, + "app.settings.game-options.setting.mouse-wheel-sensitivity.label": { + "message": "鼠标滚轮灵敏度" + }, + "app.settings.game-options.setting.music-frequency.label": { + "message": "音乐频率" + }, + "app.settings.game-options.setting.music-toast.description": { + "message": "选择是否在暂停菜单和提示中显示音乐标题。" + }, + "app.settings.game-options.setting.music-toast.label": { + "message": "音乐通知" + }, + "app.settings.game-options.setting.music-volume.label": { "message": "音乐" }, + "app.settings.game-options.setting.narrator-hotkey.label": { + "message": "旁白快捷键" + }, + "app.settings.game-options.setting.narrator.description": { + "message": "选择旁白朗读的内容。" + }, + "app.settings.game-options.setting.narrator.label": { "message": "旁白" }, + "app.settings.game-options.setting.neutral-volume.label": { + "message": "友好生物" + }, + "app.settings.game-options.setting.notification-time.description": { + "message": "提示通知保持显示的时长。" + }, + "app.settings.game-options.setting.notification-time.label": { + "message": "通知时长" + }, + "app.settings.game-options.setting.operator-items-tab.label": { + "message": "管理员物品栏标签" + }, + "app.settings.game-options.setting.panorama-speed.label": { + "message": "全景图速度" + }, + "app.settings.game-options.setting.particles.label": { "message": "粒子" }, + "app.settings.game-options.setting.players-volume.label": { + "message": "玩家" + }, + "app.settings.game-options.setting.prioritize-chunk-updates.label": { + "message": "区块更新优先级" + }, + "app.settings.game-options.setting.quit-shortcuts.label": { + "message": "退出快捷键" + }, + "app.settings.game-options.setting.raw-mouse-input.label": { + "message": "原始鼠标输入" + }, + "app.settings.game-options.setting.realms-notifications.label": { + "message": "Realms 通知" + }, + "app.settings.game-options.setting.record-volume.label": { + "message": "唱片机/音符盒" + }, + "app.settings.game-options.setting.reduced-debug-info.label": { + "message": "减少调试信息" + }, + "app.settings.game-options.setting.render-distance.label": { + "message": "渲染距离" + }, + "app.settings.game-options.setting.right-pants-leg.label": { + "message": "右裤腿" + }, + "app.settings.game-options.setting.right-sleeve.label": { "message": "右袖" }, + "app.settings.game-options.setting.rotate-with-minecart.label": { + "message": "随矿车旋转" + }, + "app.settings.game-options.setting.save-chat-drafts.label": { + "message": "保存聊天草稿" + }, + "app.settings.game-options.setting.screen-effects.label": { + "message": "屏幕效果" + }, + "app.settings.game-options.setting.secure-chat-only.label": { + "message": "仅显示安全聊天" + }, + "app.settings.game-options.setting.sensitivity.label": { + "message": "鼠标灵敏度" + }, + "app.settings.game-options.setting.server-textures.label": { + "message": "服务器纹理" + }, + "app.settings.game-options.setting.share-presence.label": { + "message": "共享在线状态" + }, + "app.settings.game-options.setting.simulation-distance.description": { + "message": "实体更新以及方块和流体更新的距离。" + }, + "app.settings.game-options.setting.simulation-distance.label": { + "message": "模拟距离" + }, + "app.settings.game-options.setting.snooper.label": { "message": "数据收集" }, + "app.settings.game-options.setting.sprint-window.label": { + "message": "疾跑窗口" + }, + "app.settings.game-options.setting.subtitles.description": { + "message": "显示游戏声音的字幕。" + }, + "app.settings.game-options.setting.subtitles.label": { "message": "字幕" }, + "app.settings.game-options.setting.text-background-opacity.label": { + "message": "文字背景不透明度" + }, + "app.settings.game-options.setting.texture-filtering.label": { + "message": "纹理过滤" + }, + "app.settings.game-options.setting.toggle-attack.label": { + "message": "切换攻击" + }, + "app.settings.game-options.setting.toggle-crouch.description": { + "message": "按一次即可保持潜行。" + }, + "app.settings.game-options.setting.toggle-crouch.label": { + "message": "切换潜行" + }, + "app.settings.game-options.setting.toggle-sprint.description": { + "message": "按一次即可保持疾跑。" + }, + "app.settings.game-options.setting.toggle-sprint.label": { + "message": "切换疾跑" + }, + "app.settings.game-options.setting.toggle-use.label": { + "message": "切换使用" + }, + "app.settings.game-options.setting.touchscreen.label": { + "message": "触屏模式" + }, + "app.settings.game-options.setting.ui-volume.label": { "message": "界面" }, + "app.settings.game-options.setting.unfocused-chat-height.label": { + "message": "未聚焦时聊天高度" + }, + "app.settings.game-options.setting.use-vbo.label": { "message": "使用 VBO" }, + "app.settings.game-options.setting.view-bobbing.description": { + "message": "行走时为镜头添加摇摆效果。" + }, + "app.settings.game-options.setting.view-bobbing.label": { + "message": "视角摇摆" + }, + "app.settings.game-options.setting.vignette.label": { "message": "晕影" }, + "app.settings.game-options.setting.voice-volume.label": { + "message": "语音和语音朗读" + }, + "app.settings.game-options.setting.vsync.description": { + "message": "将帧率限制为显示器刷新率,以防止画面撕裂。" + }, + "app.settings.game-options.setting.vsync.label": { "message": "垂直同步" }, + "app.settings.game-options.setting.weather-radius.label": { + "message": "天气范围" + }, + "app.settings.game-options.setting.weather-volume.label": { + "message": "天气" + }, + "app.settings.game-options.validation.changed-since-opened": { + "message": "此设置已在其他位置更改,请检查后重试。" + }, + "app.settings.game-options.validation.invalid-value": { + "message": "请选择有效值。" + }, + "app.settings.game-options.validation.missing-value": { + "message": "请先选择一个值。" + }, + "app.settings.game-options.validation.no-compatible-instances": { + "message": "部分实例无法使用此设置" + }, + "app.settings.synced-options.game-settings.conflict-text": { + "message": "我们已加载最新设置。请检查更改后再次保存。" + }, + "app.settings.synced-options.game-settings.conflict-title": { + "message": "这些设置已在其他地方更改" + }, + "app.settings.synced-options.game-settings.custom-empty": { + "message": "Modrinth 在某个实例中找到模组添加的设置后,它们会显示在这里。" + }, + "app.settings.synced-options.game-settings.disable-all": { + "message": "取消同步全部" + }, + "app.settings.synced-options.game-settings.empty": { + "message": "没有匹配当前搜索或筛选条件的设置。" + }, + "app.settings.synced-options.game-settings.enable-all": { + "message": "同步全部" + }, + "app.settings.synced-options.game-settings.load-failed": { + "message": "无法加载游戏设置。" + }, + "app.settings.synced-options.game-settings.loading": { + "message": "正在加载设置……" + }, + "app.settings.synced-options.game-settings.not-being-synced": { + "message": "未同步" + }, + "app.settings.synced-options.game-settings.retry": { "message": "重试" }, + "app.settings.synced-options.game-settings.save": { "message": "保存设置" }, + "app.settings.synced-options.game-settings.search": { + "message": "搜索设置……" + }, + "app.settings.synced-options.game-settings.sync-description": { + "message": "同步的选项会应用到每个实例,并覆盖整合包中包含的值。" + }, + "app.settings.synced-options.game-settings.sync-setting": { + "message": "在所有实例间同步{setting}" + }, + "app.settings.synced-options.game-settings.sync-title": { + "message": "同步游戏设置" + }, + "app.settings.synced-options.game-settings.synced-across-instances": { + "message": "已在所有实例间同步" + }, + "app.settings.synced-options.game-settings.title": { "message": "游戏设置" }, + "app.settings.synced-options.game-settings.unsaved-changes": { + "message": "你有未保存的更改。" + }, + "app.settings.synced-options.game-settings.unsync-setting": { + "message": "停止在所有实例间同步{setting}" + }, + "app.settings.synced-options.game-settings.value-mixed": { + "message": "各实例不同" + }, + "app.settings.synced-options.game-settings.value-off": { "message": "关闭" }, + "app.settings.synced-options.game-settings.value-on": { "message": "开启" }, + "app.settings.synced-options.game-settings.value-unset": { + "message": "选择一个值" + }, + "app.anniversary.banner": { "message": "Starlight {ordinal} 周年快乐!" }, + "instance.settings.tabs.window.window-title": { "message": "窗口标题" }, + "instance.settings.tabs.window.window-title.description": { + "message": "自定义 Minecraft 窗口标题。留空则使用默认标题。" + }, + "instance.settings.tabs.window.window-title.enter": { + "message": "输入窗口标题..." + }, + "app.settings.defaults.window-title.enabled": { + "message": "自定义 Minecraft 窗口标题" + }, + "app.settings.defaults.window-title.enabled-description": { + "message": "允许实例自定义 Minecraft 窗口标题。" + }, + "app.settings.defaults.window-title.default": { "message": "默认窗口标题" }, + "app.settings.defaults.window-title.default-description": { + "message": "当实例未提供自己的标题时使用。" + }, + "app.settings.defaults.window-title.default-placeholder": { + "message": "输入默认窗口标题..." + }, + "app.action-bar.announcements": { "message": "公告" }, + "app.remote-announcements.center-title": { "message": "公告" }, + "app.remote-announcements.empty": { "message": "暂无公告" }, + "app.settings.tabs.logs": { "message": "日志与诊断" }, + "app.settings.logs.export.title": { "message": "导出日志" }, + "app.settings.logs.export.description": { + "message": "将启动器日志打包,可附带环境摘要、最近的实例日志与崩溃分析。" + }, + "app.settings.logs.export.button": { "message": "导出日志…" }, + "app.settings.logs.open-folder": { "message": "打开日志文件夹" }, + "app.settings.logs.export.modal.title": { "message": "导出日志" }, + "app.settings.logs.export.modal.description": { + "message": "选择要包含的详细程度。导出的日志会把访问令牌、Minecraft 令牌与 IP 地址替换为占位符。" + }, + "app.settings.logs.export.modal.range": { "message": "时间范围" }, + "app.settings.logs.export.modal.range.30-minutes": { + "message": "最近 30 分钟" + }, + "app.settings.logs.export.modal.range.2-hours": { "message": "最近 2 小时" }, + "app.settings.logs.export.modal.range.all": { "message": "全部会话" }, + "app.settings.logs.export.modal.level": { "message": "日志等级" }, + "app.settings.logs.export.modal.level.all": { "message": "全部等级" }, + "app.settings.logs.export.modal.level.debug": { "message": "DEBUG 及以上" }, + "app.settings.logs.export.modal.level.info": { "message": "INFO 及以上" }, + "app.settings.logs.export.modal.extras": { "message": "附加信息" }, + "app.settings.logs.export.modal.system-info": { "message": "系统与环境摘要" }, + "app.settings.logs.export.modal.instance-logs": { + "message": "最近的实例日志" + }, + "app.settings.logs.export.modal.crash-analysis": { + "message": "崩溃分析(检测到崩溃时)" + }, + "app.settings.logs.export.modal.cancel": { "message": "取消" }, + "app.settings.logs.export.modal.confirm": { "message": "导出" }, + "app.settings.logs.export.notification-title": { "message": "日志已导出" }, + "app.settings.logs.export.notification-text": { + "message": "日志压缩包已保存到 {path}" + }, + "app.skins.armor.title": { "message": "盔甲与纹饰" }, + "app.skins.armor.save": { "message": "保存" }, + "app.skins.armor.close": { "message": "关闭" }, + "app.skins.armor.description": { + "message": "在皮肤上试穿盔甲与纹饰。未保存前不会写入任何内容。" + }, + "app.skins.armor.unsaved": { "message": "有未保存的更改" }, + "app.skins.armor.reset-to-saved": { "message": "重置为已保存" }, + "app.skins.armor.restore-defaults": { "message": "恢复默认" }, + "app.instances.batch-edit-groups.add": { "message": "添加" }, + "app.instances.batch-edit-groups.add-to-group-title": { + "message": "添加实例到「{groupName}」" + }, + "app.instances.batch-edit-groups.added": { "message": "已添加" }, + "app.instances.batch-edit-groups.cancel": { "message": "取消" }, + "app.instances.batch-edit-groups.create": { "message": "创建分组" }, + "app.instances.batch-edit-groups.group-name": { "message": "分组名称" }, + "app.instances.batch-edit-groups.group-name-placeholder": { + "message": "输入分组名称" + }, + "app.instances.batch-edit-groups.no-instances-found": { + "message": "未找到实例" + }, + "app.instances.batch-edit-groups.save-changes": { "message": "保存" }, + "app.instances.batch-edit-groups.search-instance": { "message": "搜索实例" }, + "app.instances.group.add-to-group": { "message": "添加到分组" }, + "app.instances.group.collapse": { "message": "折叠分组" }, + "app.instances.group.delete-description": { + "message": "此分组中的实例将被取消分组。" + }, + "app.instances.group.delete-group": { "message": "删除分组" }, + "app.instances.group.edit-name": { "message": "编辑分组名称" }, + "app.instances.group.empty": { "message": "拖放实例到此处添加。" }, + "app.instances.group.expand": { "message": "展开分组" }, + "app.instances.group.move-down": { "message": "下移分组" }, + "app.instances.group.move-up": { "message": "上移分组" }, + "app.instances.group.name-empty": { "message": "分组名称不能为空。" }, + "app.instances.group.name-too-long": { + "message": "分组名称不能超过 {maxLength} 个字符。" + }, + "app.instances.group.new-instance": { "message": "新建实例" }, + "app.instances.group.pinned": { "message": "已置顶" }, + "app.instances.group.rename-group": { "message": "重命名分组" }, + "app.instances.sort-by": { "message": "排序方式" }, + "app.instances.sort.direction.asc-alphabetical": { "message": "A–Z 正序" }, + "app.instances.sort.direction.asc-date": { "message": "最早的在前" }, + "app.instances.sort.direction.asc-recency": { "message": "最早游玩在前" }, + "app.instances.sort.direction.asc-version": { "message": "旧版本在前" }, + "app.instances.sort.direction.desc-alphabetical": { "message": "Z–A 倒序" }, + "app.instances.sort.direction.desc-date": { "message": "最新的在前" }, + "app.instances.sort.direction.desc-recency": { "message": "最近游玩在前" }, + "app.instances.sort.direction.desc-version": { "message": "新版本在前" }, + "app.library.context-menu.pin-instance": { "message": "置顶实例" }, + "app.library.context-menu.remove-from-group": { "message": "移出分组" }, + "app.library.context-menu.unpin-instance": { "message": "取消置顶" }, + "app.library.group.new": { "message": "新建分组" }, + "app.library.selection.new-group": { "message": "新建分组" }, + "app.library.selection.remove-from-group": { "message": "移出分组" }, + "app.log-share.settings.title": { "message": "日志分享与 AI 分析" }, + "app.log-share.settings.description": { + "message": "将崩溃诊断分享到 LogShare,获取结构化摘要与可选的 AI 分析。优先使用 LogShare,不可用时回退到 mclo.gs。" + }, + "app.log-share.settings.share-provider": { "message": "日志分享服务" }, + "app.log-share.settings.share-provider-description": { + "message": "分享诊断链接时上传到的目标服务。" + }, + "app.log-share.settings.ai-source": { "message": "AI 分析来源" }, + "app.log-share.settings.ai-source-logshare": { "message": "LogShare AI" }, + "app.log-share.settings.ai-source-custom": { "message": "自定义 AI 服务商" }, + "app.log-share.settings.ai-source-description": { + "message": "LogShare AI 在云端分析;自定义则使用下方配置的服务商。" + }, + "app.log-share.settings.auto-upload": { + "message": "自动上传以获取结构化摘要" + }, + "app.log-share.settings.auto-upload-description": { + "message": "仅在 LogShare 模式下生效。" + }, + "app.log-share.settings.multi-file": { "message": "多文件上传" }, + "app.log-share.settings.multi-file-description": { + "message": "同时上传 latest.log、崩溃报告、hs_err 以及启动器日志。" + }, + "app.log-share.settings.no-storage": { "message": "不落盘直接分析" }, + "app.log-share.settings.no-storage-description": { + "message": "直接提交内容进行分析并跳过存储上传。会关闭自动摘要。" + }, + "app.log-share.settings.show-progress": { "message": "显示 AI 分析进度" }, + "app.log-share.settings.show-progress-description": { + "message": "展示 LogAgent 运行期间的思考、工具及上限状态。" + }, + "app.log-share.shared-logs.title": { "message": "已分享日志" }, + "app.log-share.shared-logs.description": { + "message": "你分享过的所有诊断都记录在这里。删除条目会将其从记录中移除,并向分享服务请求删除。" + }, + "app.log-share.shared-logs.empty": { "message": "暂无已分享的日志。" }, + "app.log-share.shared-logs.delete": { "message": "删除" }, + "app.log-share.shared-logs.deleting": { "message": "删除中…" }, + "app.log-share.shared-logs.deleted": { "message": "已删除共享日志。" }, + "app.log-share.shared-logs.delete-failed": { + "message": "删除已分享日志失败。" + }, + "app.log-share.shared-logs.truncated": { "message": "已截断" }, + "app.minecraft-crash.share-fallback": { + "message": "LogShare 不可用,已改用 mclo.gs 分享诊断信息。" + }, + "app.log-share.ai.action": { "message": "AI 深度分析" }, + "app.log-share.summary.title": { "message": "LogShare 分析" }, + "app.log-share.summary.loading": { "message": "正在加载结构化摘要…" }, + "app.log-share.summary.empty": { + "message": "LogShare 未返回这些日志的结构化摘要。" + }, + "app.log-share.summary.no-storage": { + "message": "启用不落盘模式时无法获取结构化摘要。" + }, + "app.log-share.summary.failed": { + "message": "无法加载 LogShare 摘要:{message}" + }, + "app.log-share.summary.problems": { "message": "问题" }, + "app.log-share.summary.information": { "message": "信息" }, + "app.minecraft-crash.logs.loading": { "message": "正在加载崩溃日志…" }, + "app.minecraft-crash.logs.empty": { "message": "未找到本次启动产生的日志。" }, + "app.minecraft-crash.logs.failed": { + "message": "无法加载崩溃日志:{message}" + }, + "app.minecraft-crash.log-file.loading": { "message": "正在加载日志文件…" }, + "app.minecraft-crash.log-file.failed": { + "message": "无法加载此日志文件:{message}" + }, + "app.log-share.ai.tab": { "message": "LogAgent 深度分析" }, + "app.log-share.ai.root-cause": { "message": "核心根因" }, + "app.log-share.ai.confidence": { "message": "诊断置信度" }, + "app.log-share.ai.steps": { "message": "推荐排障步骤" }, + "app.log-share.ai.evidence": { "message": "支撑证据链" }, + "app.log-share.ai.thinking": { "message": "思考中…" }, + "app.log-share.ai.queued": { "message": "排队中,前方约 {position} 个任务" }, + "app.log-share.ai.queued-front": { "message": "排队中,正在等待空闲槽位" }, + "app.log-share.ai.tool": { "message": "正在使用工具:{name}" }, + "app.log-share.ai.unknown-tool": { "message": "工具" }, + "app.log-share.ai.tool-result": { "message": "已收到工具结果" }, + "app.log-share.ai.limit": { "message": "已达到工具调用上限" }, + "app.log-share.ai.working": { "message": "正在使用 LogAgent 分析…" }, + "app.log-share.ai.failed": { "message": "LogAgent 分析失败:{message}" }, + "app.shortcut-settings.change-shortcut": { + "message": "更改「{action}」的快捷键" + }, + "app.shortcut-settings.enable": { "message": "启用快捷滚动" }, + "app.shortcut-settings.end-action": { "message": "滚动到页面底部" }, + "app.shortcut-settings.end-action-description": { + "message": "将当前页面移动到底部。" + }, + "app.shortcut-settings.home-action": { "message": "滚动到页面顶部" }, + "app.shortcut-settings.home-action-description": { + "message": "将当前页面移动到顶部。" + }, + "app.shortcut-settings.nav-create": { "message": "新建实例" }, + "app.shortcut-settings.nav-create-description": { + "message": "前往新建实例。" + }, + "app.shortcut-settings.nav-description": { + "message": "使用各自的快捷键跳转到对应菜单项。每个快捷键默认关闭,需逐个启用。" + }, + "app.shortcut-settings.nav-discover": { "message": "发现内容" }, + "app.shortcut-settings.nav-discover-description": { + "message": "前往浏览内容。" + }, + "app.shortcut-settings.nav-downloads": { "message": "下载" }, + "app.shortcut-settings.nav-downloads-description": { + "message": "前往下载。" + }, + "app.shortcut-settings.nav-home": { "message": "首页" }, + "app.shortcut-settings.nav-home-description": { "message": "前往首页。" }, + "app.shortcut-settings.nav-lab": { "message": "实验室" }, + "app.shortcut-settings.nav-lab-description": { "message": "前往实验室。" }, + "app.shortcut-settings.nav-library": { "message": "内容库" }, + "app.shortcut-settings.nav-library-description": { + "message": "前往内容库。" + }, + "app.shortcut-settings.nav-multiplayer": { "message": "多人游戏" }, + "app.shortcut-settings.nav-multiplayer-description": { + "message": "前往多人游戏。" + }, + "app.shortcut-settings.nav-settings": { "message": "设置" }, + "app.shortcut-settings.nav-settings-description": { "message": "前往设置。" }, + "app.shortcut-settings.nav-skins": { "message": "皮肤选择器" }, + "app.shortcut-settings.nav-skins-description": { + "message": "前往皮肤选择器。" + }, + "app.shortcut-settings.nav-title": { "message": "导航快捷键" }, + "app.shortcut-settings.nav-worlds": { "message": "世界" }, + "app.shortcut-settings.nav-worlds-description": { + "message": "前往你的世界。" + }, + "app.shortcut-settings.page-down-action": { "message": "向下滚动一屏" }, + "app.shortcut-settings.page-down-action-description": { + "message": "将当前页面向下移动一屏。" + }, + "app.shortcut-settings.page-up-action": { "message": "向上滚动一屏" }, + "app.shortcut-settings.page-up-action-description": { + "message": "将当前页面向上移动一屏。" + }, + "app.shortcut-settings.scroll-description": { + "message": "使用键盘在长页面中快速滚动。" + }, + "app.shortcut-settings.scroll-title": { "message": "快速滚动" }, + "app.settings.tabs.shortcut-settings": { "message": "键盘快捷键" }, + "app.skin-site.bugs.title": { "message": "问题与建议" }, + "app.skin-site.bugs.back": { "message": "返回主页" }, + "app.skin-site.bugs.subtitle": { "message": "反馈问题并跟踪处理进度。" }, + "app.skin-site.bugs.loading": { "message": "加载中…" }, + "app.skin-site.bugs.failed": { "message": "无法加载问题列表。" }, + "app.skin-site.bugs.empty": { "message": "没有符合筛选条件的问题。" }, + "app.skin-site.bugs.all": { "message": "全部" }, + "app.skin-site.bugs.more": { "message": "加载更多" }, + "app.skin-site.bugs.open-only": { "message": "仅显示未关闭" }, + "app.skin-site.bugs.comments": { "message": "{count} 条评论" }, + "app.skin-site.bugs.type.bug": { "message": "缺陷" }, + "app.skin-site.bugs.type.feature": { "message": "功能建议" }, + "app.skin-site.bugs.type.improvement": { "message": "改进" }, + "app.skin-site.bugs.status.pending": { "message": "待处理" }, + "app.skin-site.bugs.status.confirmed": { "message": "已确认" }, + "app.skin-site.bugs.status.needs-info": { "message": "待补充信息" }, + "app.skin-site.bugs.status.in-progress": { "message": "处理中" }, + "app.skin-site.bugs.status.verifying": { "message": "验证中" }, + "app.skin-site.bugs.status.closed": { "message": "已关闭" }, + "app.skin-site.bugs.priority.critical": { "message": "紧急" }, + "app.skin-site.bugs.priority.high": { "message": "高" }, + "app.skin-site.bugs.priority.normal": { "message": "普通" }, + "app.skin-site.bugs.priority.low": { "message": "低" }, + "app.skin-site.bugs.resolution.fixed": { "message": "已修复" }, + "app.skin-site.bugs.resolution.wont-fix": { "message": "不予修复" }, + "app.skin-site.bugs.resolution.by-design": { "message": "设计如此" }, + "app.skin-site.bugs.resolution.duplicate": { "message": "重复问题" }, + "app.skin-site.bugs.resolution.cannot-reproduce": { "message": "无法复现" }, + "app.skin-site.bugs.resolution.invalid": { "message": "无效" }, + "app.skin-site.bug-detail.back": { "message": "返回列表" }, + "app.skin-site.bug-detail.loading": { "message": "加载中…" }, + "app.skin-site.bug-detail.failed": { "message": "无法加载该问题。" }, + "app.skin-site.bug-detail.issue-no": { "message": "问题 #{id}" }, + "app.skin-site.bug-detail.by": { "message": "由 {name} 提交" }, + "app.skin-site.bug-detail.assignee": { "message": "负责人:{name}" }, + "app.skin-site.bug-detail.unassigned": { "message": "未分配" }, + "app.skin-site.bug-detail.description-empty": { "message": "未提供描述。" }, + "app.skin-site.bug-detail.resolution": { "message": "处理结果:{name}" }, + "app.skin-site.bug-detail.comments-title": { "message": "评论" }, + "app.skin-site.bug-detail.comments-empty": { + "message": "还没有评论,来抢沙发吧。" + }, + "app.skin-site.bug-detail.comments-loading": { "message": "加载评论中…" }, + "app.skin-site.bug-detail.comment-placeholder": { + "message": "写下你的评论…" + }, + "app.skin-site.bug-detail.comment-submit": { "message": "发表评论" }, + "app.skin-site.bug-detail.comment-sending": { "message": "发送中…" }, + "app.skin-site.bug-detail.comment-empty-error": { + "message": "评论内容不能为空。" + }, + "app.skin-site.bug-detail.comment-posted": { "message": "评论已发表。" }, + "app.skin-site.bug-detail.comment-deleted": { "message": "评论已删除。" }, + "app.skin-site.bug-detail.delete": { "message": "删除" }, + "app.skin-site.bug-detail.deleting": { "message": "删除中…" }, + "app.skin-site.bug-detail.system": { "message": "系统" }, + "app.skin-site.bug-create.title": { "message": "新建问题" }, + "app.skin-site.bug-create.type": { "message": "类型" }, + "app.skin-site.bug-create.title-field": { "message": "标题" }, + "app.skin-site.bug-create.title-placeholder": { "message": "简要描述问题" }, + "app.skin-site.bug-create.description": { "message": "描述" }, + "app.skin-site.bug-create.description-placeholder": { + "message": "发生了什么?如何复现?" + }, + "app.skin-site.bug-create.private": { "message": "私密问题" }, + "app.skin-site.bug-create.private-hint": { "message": "仅你和维护者可见。" }, + "app.skin-site.bug-create.images": { "message": "图片" }, + "app.skin-site.bug-create.images-hint": { + "message": "可选,最多 5 张图片。" + }, + "app.skin-site.bug-create.submit": { "message": "创建问题" }, + "app.skin-site.bug-create.submitting": { "message": "创建中…" }, + "app.skin-site.bug-create.created": { "message": "问题已创建。" }, + "app.skin-site.bug-create.cancel": { "message": "取消" }, + "app.skin-site.bug-notifications.title": { "message": "通知" }, + "app.skin-site.bug-notifications.back": { "message": "返回主页" }, + "app.skin-site.bug-notifications.subtitle": { + "message": "你提交或关注的问题动态。" + }, + "app.skin-site.bug-notifications.loading": { "message": "加载中…" }, + "app.skin-site.bug-notifications.failed": { "message": "无法加载通知。" }, + "app.skin-site.bug-notifications.empty": { "message": "暂无新通知。" }, + "app.skin-site.bug-notifications.item": { + "message": "{name} 更新了问题 #{id}" + }, + "app.skin-site.bug-notifications.mark-all": { "message": "全部标为已读" }, + "app.skin-site.bug-notifications.marked-all": { + "message": "已将全部通知标为已读。" + }, + "app.skin-site.bugs.create": { "message": "新建问题" }, + "app.skin-site.bugs.notifications": { "message": "通知" }, + "app.skin-site.home.title": { "message": "星光皮肤站" }, + "app.skin-site.home.subtitle": { "message": "星光服务器玩家的衣柜。" }, + "app.skin-site.home.address": { "message": "皮肤站地址" }, + "app.skin-site.home.copied": { "message": "地址已复制。" }, + "app.skin-site.home.copy": { "message": "复制" }, + "app.skin-site.admin.title": { "message": "管理控制台" }, + "app.skin-site.admin.back": { "message": "返回主页" }, + "app.skin-site.admin.no-access": { "message": "你没有访问权限。" }, + "app.skin-site.admin.no-access-hint": { + "message": "请使用管理员账号登录后使用本控制台。" + }, + "app.skin-site.admin.group.security": { "message": "系统安全" }, + "app.skin-site.admin.group.users": { "message": "用户管理" }, + "app.skin-site.admin.group.community": { "message": "社区管理" }, + "app.skin-site.admin.group.resources": { "message": "资源分发" }, + "app.skin-site.admin.item.servers": { "message": "服务器管理" }, + "app.skin-site.admin.item.summary": { "message": "状态概览" }, + "app.skin-site.admin.item.access-audit": { "message": "登录与认证审计" }, + "app.skin-site.admin.item.users": { "message": "用户信息" }, + "app.skin-site.admin.item.players": { "message": "玩家信息" }, + "app.skin-site.admin.item.review": { "message": "玩家审核" }, + "app.skin-site.admin.item.broadcast": { "message": "公告操作" }, + "app.skin-site.admin.item.chat": { "message": "聊天室管理" }, + "app.skin-site.admin.item.wiki": { "message": "维基管理" }, + "app.skin-site.admin.item.donations": { "message": "捐赠管理" }, + "app.skin-site.admin.item.mods": { "message": "Mod 管理" }, + "app.skin-site.admin.item.launcher": { "message": "启动器管理" }, + "app.skin-site.admin.item.downloads": { "message": "下载管理" }, + "app.skin-site.home.admin": { "message": "管理" }, + "app.skin-site.home.settings": { "message": "账号" }, + "app.skin-site.home.sign-in": { "message": "登录" }, + "app.skin-site.home.players": { "message": "角色" }, + "app.skin-site.home.my-players": { "message": "我的角色" }, + "app.skin-site.home.no-players": { "message": "尚未绑定角色。" }, + "app.skin-site.home.loading": { "message": "加载中…" }, + "app.skin-site.home.error": { "message": "加载失败。" }, + "app.skin-site.home.luck": { "message": "幸运值" }, + "app.skin-site.home.luck-unavailable": { "message": "不可用" }, + "app.skin-site.home.signed-out-title": { "message": "未登录" }, + "app.skin-site.home.signed-out-body": { + "message": "登录后可管理你的账号与皮肤。" + }, + "app.skin-site.admin.summary.title": { "message": "系统概况" }, + "app.skin-site.admin.summary.loading": { "message": "加载中…" }, + "app.skin-site.admin.summary.failed": { "message": "无法加载状态。" }, + "app.skin-site.admin.summary.uptime": { "message": "运行时长" }, + "app.skin-site.admin.summary.memory": { "message": "内存占用" }, + "app.skin-site.admin.summary.users": { "message": "用户数量" }, + "app.skin-site.admin.summary.players": { "message": "玩家数量" }, + "app.skin-site.admin.summary.realtime-hint": { + "message": "实时 CPU 与 GC 图表依赖管理端 WebSocket,目前尚未接入。" + }, + "app.skin-site.admin.broadcast.title": { "message": "公告" }, + "app.skin-site.admin.broadcast.loading": { "message": "加载中…" }, + "app.skin-site.admin.broadcast.failed": { "message": "加载失败。" }, + "app.skin-site.admin.broadcast.empty": { "message": "尚未发布过任何公告。" }, + "app.skin-site.admin.broadcast.create": { "message": "发布新公告" }, + "app.skin-site.admin.broadcast.save": { "message": "保存" }, + "app.skin-site.admin.broadcast.cancel": { "message": "取消" }, + "app.skin-site.admin.broadcast.edit": { "message": "编辑" }, + "app.skin-site.admin.broadcast.remove": { "message": "删除" }, + "app.skin-site.admin.broadcast.title-field": { "message": "标题" }, + "app.skin-site.admin.broadcast.subtitle-field": { "message": "副标题" }, + "app.skin-site.admin.broadcast.content-field": { "message": "内容" }, + "app.skin-site.admin.broadcast.expire-at": { "message": "过期时间" }, + "app.skin-site.admin.broadcast.star": { "message": "标星" }, + "app.skin-site.admin.broadcast.level": { "message": "重要程度" }, + "app.skin-site.admin.broadcast.image": { "message": "图片" }, + "app.skin-site.admin.broadcast.remove-image": { "message": "移除图片" }, + "app.skin-site.admin.broadcast.created": { "message": "公告已发布。" }, + "app.skin-site.admin.broadcast.updated": { "message": "公告已更新。" }, + "app.skin-site.admin.broadcast.deleted": { "message": "公告已删除。" }, + "app.skin-site.admin.broadcast.confirm-delete": { + "message": "确定删除「{title}」吗?此操作不可撤销。" + }, + "app.skin-site.admin.broadcast.uploader": { "message": "由 {name} 发布" }, + "app.skin-site.admin.players.title": { "message": "玩家" }, + "app.skin-site.admin.players.loading": { "message": "加载中…" }, + "app.skin-site.admin.players.failed": { "message": "无法加载玩家列表。" }, + "app.skin-site.admin.players.empty": { "message": "没有找到玩家。" }, + "app.skin-site.admin.players.search": { "message": "搜索" }, + "app.skin-site.admin.players.search-name": { "message": "名称" }, + "app.skin-site.admin.players.search-owner": { "message": "所有者" }, + "app.skin-site.admin.players.search-uuid": { "message": "UUID" }, + "app.skin-site.admin.players.total": { "message": "共 {count} 名玩家" }, + "app.skin-site.admin.players.prev": { "message": "上一页" }, + "app.skin-site.admin.players.next": { "message": "下一页" }, + "app.skin-site.admin.players.page": { "message": "第 {page} 页" }, + "app.skin-site.admin.players.name": { "message": "名称" }, + "app.skin-site.admin.players.owner": { "message": "所有者" }, + "app.skin-site.admin.players.source": { "message": "来源" }, + "app.skin-site.admin.players.legacy": { "message": "旧版适配" }, + "app.skin-site.admin.players.actions": { "message": "操作" }, + "app.skin-site.admin.players.edit": { "message": "编辑" }, + "app.skin-site.admin.players.save": { "message": "保存" }, + "app.skin-site.admin.players.cancel": { "message": "取消" }, + "app.skin-site.admin.players.remove": { "message": "删除" }, + "app.skin-site.admin.players.updated": { "message": "玩家已更新。" }, + "app.skin-site.admin.players.deleted": { "message": "玩家已删除。" }, + "app.skin-site.admin.players.confirm-delete": { + "message": "确定删除玩家「{name}」吗?此操作不可撤销。" + }, + "app.skin-site.admin.players.mojang": { "message": "正版" }, + "app.skin-site.admin.players.custom": { "message": "皮肤站" }, + "app.skin-site.admin.users.title": { "message": "用户" }, + "app.skin-site.admin.users.loading": { "message": "加载中…" }, + "app.skin-site.admin.users.failed": { "message": "无法加载用户列表。" }, + "app.skin-site.admin.users.empty": { "message": "没有找到用户。" }, + "app.skin-site.admin.users.search": { "message": "搜索" }, + "app.skin-site.admin.users.search-name": { "message": "用户名" }, + "app.skin-site.admin.users.search-email": { "message": "邮箱" }, + "app.skin-site.admin.users.search-uuid": { "message": "UUID" }, + "app.skin-site.admin.users.total": { "message": "共 {count} 名用户" }, + "app.skin-site.admin.users.prev": { "message": "上一页" }, + "app.skin-site.admin.users.next": { "message": "下一页" }, + "app.skin-site.admin.users.page": { "message": "第 {page} 页" }, + "app.skin-site.admin.users.edit": { "message": "编辑" }, + "app.skin-site.admin.users.save": { "message": "保存" }, + "app.skin-site.admin.users.cancel": { "message": "取消" }, + "app.skin-site.admin.users.remove": { "message": "删除" }, + "app.skin-site.admin.users.players": { "message": "角色" }, + "app.skin-site.admin.users.updated": { "message": "用户已更新。" }, + "app.skin-site.admin.users.deleted": { "message": "用户已删除。" }, + "app.skin-site.admin.users.confirm-delete": { + "message": "确定删除用户「{name}」吗?此操作不可撤销。" + }, + "app.skin-site.admin.users.username": { "message": "用户名" }, + "app.skin-site.admin.users.email": { "message": "邮箱" }, + "app.skin-site.admin.users.qq": { "message": "QQ" }, + "app.skin-site.admin.users.password": { "message": "新密码" }, + "app.skin-site.admin.users.password-hint": { + "message": "留空则保持原密码不变。" + }, + "app.skin-site.admin.users.banned": { "message": "封禁" }, + "app.skin-site.admin.users.permissions": { "message": "权限" }, + "app.skin-site.admin.users.permissions-hint": { + "message": "以逗号分隔,例如 WEB_ADMIN, SERVER_ADMIN。" + }, + "app.skin-site.admin.users.no-players": { "message": "没有绑定的角色。" }, + "app.skin-site.admin.users.registered": { "message": "注册时间" }, + "app.skin-site.admin.review.title": { "message": "玩家审核" }, + "app.skin-site.admin.review.loading": { "message": "加载中…" }, + "app.skin-site.admin.review.failed": { "message": "无法加载申请列表。" }, + "app.skin-site.admin.review.empty": { "message": "没有找到申请。" }, + "app.skin-site.admin.review.total": { "message": "共 {count} 份申请" }, + "app.skin-site.admin.review.filter-all": { "message": "全部" }, + "app.skin-site.admin.review.filter-pending": { "message": "待处理" }, + "app.skin-site.admin.review.filter-passed": { "message": "已通过" }, + "app.skin-site.admin.review.filter-rejected": { "message": "已拒绝" }, + "app.skin-site.admin.review.result-pending": { "message": "待处理" }, + "app.skin-site.admin.review.result-passed": { "message": "已通过" }, + "app.skin-site.admin.review.result-rejected": { "message": "已拒绝" }, + "app.skin-site.admin.review.mode-auto": { "message": "自动" }, + "app.skin-site.admin.review.mode-manual": { "message": "人工" }, + "app.skin-site.admin.review.applicant": { "message": "申请人" }, + "app.skin-site.admin.review.submitted-at": { "message": "提交时间" }, + "app.skin-site.admin.review.score": { "message": "得分" }, + "app.skin-site.admin.review.reviewer": { "message": "审核人" }, + "app.skin-site.admin.review.details": { "message": "详情" }, + "app.skin-site.admin.review.hide": { "message": "收起" }, + "app.skin-site.admin.review.answers": { "message": "作答内容" }, + "app.skin-site.admin.review.no-answers": { "message": "没有作答记录。" }, + "app.skin-site.admin.review.auto-checks": { "message": "自动检查" }, + "app.skin-site.admin.review.context": { "message": "提交环境" }, + "app.skin-site.admin.review.ip": { "message": "IP" }, + "app.skin-site.admin.review.user-agent": { "message": "浏览器标识" }, + "app.skin-site.admin.review.activity": { "message": "行为记录" }, + "app.skin-site.admin.review.duration": { "message": "时长" }, + "app.skin-site.admin.review.hidden-time": { "message": "切出时长" }, + "app.skin-site.admin.review.screen-switches": { "message": "切屏次数" }, + "app.skin-site.admin.review.history": { "message": "审核记录" }, + "app.skin-site.admin.review.accept": { "message": "通过" }, + "app.skin-site.admin.review.reject": { "message": "拒绝" }, + "app.skin-site.admin.review.reason": { "message": "理由" }, + "app.skin-site.admin.review.accept-done": { "message": "申请已通过。" }, + "app.skin-site.admin.review.reject-done": { "message": "申请已拒绝。" }, + "app.skin-site.admin.review.regen-invite": { "message": "重发邀请码" }, + "app.skin-site.admin.review.invite-regenerated": { + "message": "邀请码已重新生成。" + }, + "app.skin-site.admin.review.invite-sent": { "message": "邮件已发送。" }, + "app.skin-site.admin.review.invite-not-sent": { "message": "邮件未发送。" }, + "app.skin-site.admin.review.prev": { "message": "上一页" }, + "app.skin-site.admin.review.next": { "message": "下一页" }, + "app.skin-site.admin.review.page": { "message": "第 {page} 页" }, + "app.skin-site.admin.launcher.title": { "message": "启动器版本" }, + "app.skin-site.admin.launcher.loading": { "message": "加载中…" }, + "app.skin-site.admin.launcher.failed": { "message": "无法加载版本列表。" }, + "app.skin-site.admin.launcher.empty": { "message": "尚未上传任何版本。" }, + "app.skin-site.admin.launcher.latest-url": { "message": "最新版本地址" }, + "app.skin-site.admin.launcher.version": { "message": "版本" }, + "app.skin-site.admin.launcher.channel": { "message": "渠道" }, + "app.skin-site.admin.launcher.uploader": { "message": "上传者" }, + "app.skin-site.admin.launcher.uploaded-at": { "message": "上传时间" }, + "app.skin-site.admin.launcher.published-at": { "message": "发布时间" }, + "app.skin-site.admin.launcher.force-update": { "message": "强制更新" }, + "app.skin-site.admin.launcher.active": { "message": "当前版本" }, + "app.skin-site.admin.launcher.activate": { "message": "设为当前" }, + "app.skin-site.admin.launcher.activated": { "message": "已设为当前版本。" }, + "app.skin-site.admin.launcher.upload": { "message": "上传版本" }, + "app.skin-site.admin.launcher.upload-hint": { + "message": "选择发布包 ZIP(最大 4 GiB)。" + }, + "app.skin-site.admin.launcher.uploading": { "message": "上传中…" }, + "app.skin-site.admin.launcher.upload-done": { "message": "版本已上传。" }, + "app.skin-site.admin.launcher.force-flag": { "message": "强制更新" }, + "app.skin-site.admin.launcher.prev": { "message": "上一页" }, + "app.skin-site.admin.launcher.next": { "message": "下一页" }, + "app.skin-site.admin.launcher.page": { "message": "第 {page} 页" }, + "app.skin-site.admin.launcher.total": { "message": "共 {count} 个版本" }, + "app.skin-site.admin.downloads.title": { "message": "资源下载" }, + "app.skin-site.admin.downloads.loading": { "message": "加载中…" }, + "app.skin-site.admin.downloads.failed": { "message": "无法加载资源列表。" }, + "app.skin-site.admin.downloads.empty": { "message": "没有下载条目。" }, + "app.skin-site.admin.downloads.categories": { "message": "分类" }, + "app.skin-site.admin.downloads.all": { "message": "全部条目" }, + "app.skin-site.admin.downloads.add-category": { "message": "添加" }, + "app.skin-site.admin.downloads.category-name": { "message": "分类名称" }, + "app.skin-site.admin.downloads.remove-category": { "message": "移除" }, + "app.skin-site.admin.downloads.total": { "message": "共 {count} 个条目" }, + "app.skin-site.admin.downloads.add-item": { "message": "新建条目" }, + "app.skin-site.admin.downloads.save": { "message": "保存" }, + "app.skin-site.admin.downloads.cancel": { "message": "取消" }, + "app.skin-site.admin.downloads.edit": { "message": "编辑" }, + "app.skin-site.admin.downloads.remove": { "message": "删除" }, + "app.skin-site.admin.downloads.filename": { "message": "文件名" }, + "app.skin-site.admin.downloads.description": { "message": "描述" }, + "app.skin-site.admin.downloads.routes": { "message": "下载线路" }, + "app.skin-site.admin.downloads.route-name": { "message": "名称" }, + "app.skin-site.admin.downloads.route-url": { "message": "URL" }, + "app.skin-site.admin.downloads.add-route": { "message": "添加线路" }, + "app.skin-site.admin.downloads.remove-route": { "message": "移除" }, + "app.skin-site.admin.downloads.statuses": { "message": "状态" }, + "app.skin-site.admin.downloads.bind-category": { "message": "所属分类" }, + "app.skin-site.admin.downloads.uploader": { "message": "由 {name} 上传" }, + "app.skin-site.admin.downloads.created": { "message": "条目已创建。" }, + "app.skin-site.admin.downloads.updated": { "message": "条目已更新。" }, + "app.skin-site.admin.downloads.deleted": { "message": "条目已删除。" }, + "app.skin-site.admin.downloads.category-added": { "message": "分类已添加。" }, + "app.skin-site.admin.downloads.category-deleted": { + "message": "分类已删除。" + }, + "app.skin-site.admin.downloads.confirm-delete-item": { + "message": "确定删除「{name}」吗?此操作不可撤销。" + }, + "app.skin-site.admin.downloads.confirm-delete-category": { + "message": "确定删除分类「{name}」吗?" + }, + "app.skin-site.admin.servers.title": { "message": "地图服务器" }, + "app.skin-site.admin.servers.loading": { "message": "加载中…" }, + "app.skin-site.admin.servers.failed": { "message": "无法加载服务器列表。" }, + "app.skin-site.admin.servers.empty": { "message": "尚未配置地图服务器。" }, + "app.skin-site.admin.servers.server-name": { "message": "服务器名称" }, + "app.skin-site.admin.servers.internal": { "message": "内部" }, + "app.skin-site.admin.servers.maps": { "message": "地图" }, + "app.skin-site.admin.servers.map-name": { "message": "地图名称" }, + "app.skin-site.admin.servers.map-url": { "message": "地图地址" }, + "app.skin-site.admin.servers.add-map": { "message": "添加地图" }, + "app.skin-site.admin.servers.remove-map": { "message": "移除" }, + "app.skin-site.admin.servers.add-server": { "message": "添加服务器" }, + "app.skin-site.admin.servers.remove-server": { "message": "删除服务器" }, + "app.skin-site.admin.servers.save": { "message": "保存更改" }, + "app.skin-site.admin.servers.saving": { "message": "保存中…" }, + "app.skin-site.admin.servers.saved": { "message": "地图服务器已保存。" }, + "app.skin-site.admin.audit.title": { "message": "登录与认证审计" }, + "app.skin-site.admin.audit.loading": { "message": "加载中…" }, + "app.skin-site.admin.audit.failed": { "message": "无法加载审计日志。" }, + "app.skin-site.admin.audit.empty": { "message": "没有审计记录。" }, + "app.skin-site.admin.audit.subject": { "message": "主体" }, + "app.skin-site.admin.audit.action": { "message": "操作" }, + "app.skin-site.admin.audit.source-ip": { "message": "来源 IP" }, + "app.skin-site.admin.audit.reported-ip": { "message": "上报玩家 IP" }, + "app.skin-site.admin.audit.count": { "message": "次数" }, + "app.skin-site.admin.audit.first-at": { "message": "首次出现" }, + "app.skin-site.admin.audit.last-at": { "message": "最近出现" }, + "app.skin-site.admin.audit.proxy": { "message": "代理" }, + "app.skin-site.admin.audit.internal": { "message": "内部" }, + "app.skin-site.admin.audit.search-subject": { "message": "主体" }, + "app.skin-site.admin.audit.search-ip": { "message": "IP" }, + "app.skin-site.admin.audit.search-action": { "message": "操作" }, + "app.skin-site.admin.audit.search": { "message": "搜索" }, + "app.skin-site.admin.audit.clear": { "message": "清空" }, + "app.skin-site.admin.audit.load-more": { "message": "加载更多" }, + "app.skin-site.admin.audit.retention": { "message": "保留天数" }, + "app.skin-site.admin.audit.record-limit": { "message": "记录上限" }, + "app.skin-site.admin.audit.save-retention": { "message": "保存" }, + "app.skin-site.admin.audit.retention-saved": { + "message": "保留天数已更新。" + }, + "app.skin-site.admin.donations.title": { "message": "捐赠管理" }, + "app.skin-site.admin.donations.loading": { "message": "加载中…" }, + "app.skin-site.admin.donations.failed": { "message": "无法加载捐赠列表。" }, + "app.skin-site.admin.donations.empty": { "message": "暂无捐赠记录。" }, + "app.skin-site.admin.donations.total-amount": { "message": "总额" }, + "app.skin-site.admin.donations.donor-count": { "message": "捐赠人数" }, + "app.skin-site.admin.donations.donor": { "message": "捐赠者" }, + "app.skin-site.admin.donations.amount": { "message": "金额" }, + "app.skin-site.admin.donations.updated-at": { "message": "更新时间" }, + "app.skin-site.admin.donations.external": { "message": "外部" }, + "app.skin-site.admin.donations.linked": { "message": "已绑定" }, + "app.skin-site.admin.donations.create": { "message": "新增捐赠" }, + "app.skin-site.admin.donations.save": { "message": "保存" }, + "app.skin-site.admin.donations.cancel": { "message": "取消" }, + "app.skin-site.admin.donations.edit": { "message": "编辑" }, + "app.skin-site.admin.donations.remove": { "message": "删除" }, + "app.skin-site.admin.donations.amount-field": { "message": "金额" }, + "app.skin-site.admin.donations.display-name": { "message": "显示名称" }, + "app.skin-site.admin.donations.user-uuid": { "message": "用户 UUID" }, + "app.skin-site.admin.donations.qq": { "message": "QQ" }, + "app.skin-site.admin.donations.append-mode": { + "message": "在原有金额上累加" + }, + "app.skin-site.admin.donations.created": { "message": "捐赠记录已创建。" }, + "app.skin-site.admin.donations.updated": { "message": "捐赠记录已更新。" }, + "app.skin-site.admin.donations.deleted": { "message": "捐赠记录已删除。" }, + "app.skin-site.admin.donations.confirm-delete": { + "message": "确定删除「{name}」的捐赠记录吗?此操作不可撤销。" + }, + "app.skin-site.admin.donations.load-more": { "message": "加载更多" }, + "app.skin-site.admin.mods.title": { "message": "Mod 管理" }, + "app.skin-site.admin.mods.tab-mods": { "message": "Mod 文件" }, + "app.skin-site.admin.mods.tab-tags": { "message": "规则标签" }, + "app.skin-site.admin.mods.tab-runtime": { "message": "运行时" }, + "app.skin-site.admin.mods.loading": { "message": "加载中…" }, + "app.skin-site.admin.mods.failed": { "message": "无法加载 Mod 列表。" }, + "app.skin-site.admin.mods.empty": { "message": "没有找到 Mod。" }, + "app.skin-site.admin.mods.total": { "message": "共 {count} 个 Mod" }, + "app.skin-site.admin.mods.search": { "message": "按名称搜索" }, + "app.skin-site.admin.mods.filter-all": { "message": "全部标签" }, + "app.skin-site.admin.mods.prev": { "message": "上一页" }, + "app.skin-site.admin.mods.next": { "message": "下一页" }, + "app.skin-site.admin.mods.page": { "message": "第 {page} 页" }, + "app.skin-site.admin.mods.name": { "message": "名称" }, + "app.skin-site.admin.mods.version": { "message": "版本" }, + "app.skin-site.admin.mods.mod-id": { "message": "Mod ID" }, + "app.skin-site.admin.mods.size": { "message": "大小" }, + "app.skin-site.admin.mods.loaders": { "message": "加载器" }, + "app.skin-site.admin.mods.domain-rules": { "message": "域名规则" }, + "app.skin-site.admin.mods.edit": { "message": "编辑" }, + "app.skin-site.admin.mods.save": { "message": "保存" }, + "app.skin-site.admin.mods.cancel": { "message": "取消" }, + "app.skin-site.admin.mods.remove": { "message": "删除" }, + "app.skin-site.admin.mods.updated": { "message": "Mod 已更新。" }, + "app.skin-site.admin.mods.deleted": { "message": "Mod 已删除。" }, + "app.skin-site.admin.mods.confirm-delete": { + "message": "确定删除 Mod「{name}」吗?此操作不可撤销。" + }, + "app.skin-site.admin.mods.rule-tags": { "message": "规则标签" }, + "app.skin-site.admin.mods.tag-name": { "message": "标签名称" }, + "app.skin-site.admin.mods.tag-rules": { "message": "规则" }, + "app.skin-site.admin.mods.tag-rules-hint": { "message": "每行一条规则。" }, + "app.skin-site.admin.mods.new-tag": { "message": "新建标签" }, + "app.skin-site.admin.mods.tag-created": { "message": "标签已创建。" }, + "app.skin-site.admin.mods.tag-updated": { "message": "标签已更新。" }, + "app.skin-site.admin.mods.tag-deleted": { "message": "标签已删除。" }, + "app.skin-site.admin.mods.confirm-delete-tag": { + "message": "确定删除标签「{name}」吗?" + }, + "app.skin-site.admin.mods.runtime": { "message": "运行时版本" }, + "app.skin-site.admin.mods.manifest-url": { "message": "清单地址" }, + "app.skin-site.admin.mods.upload-runtime": { "message": "上传运行时" }, + "app.skin-site.admin.mods.runtime-version": { "message": "版本" }, + "app.skin-site.admin.mods.uploading": { "message": "上传中…" }, + "app.skin-site.admin.mods.runtime-uploaded": { "message": "运行时已上传。" }, + "app.skin-site.admin.mods.activate": { "message": "设为当前" }, + "app.skin-site.admin.mods.activated": { "message": "运行时已启用。" }, + "app.skin-site.admin.mods.available": { "message": "可用" }, + "app.skin-site.admin.mods.unavailable": { "message": "不可用" }, + "app.skin-site.admin.mods.active": { "message": "当前版本" }, + "app.skin-site.admin.mods.uploader": { "message": "由 {name} 上传" }, + "app.skin-site.home.nav.title": { "message": "皮肤站" }, + "app.skin-site.home.nav.download": { "message": "资源下载" }, + "app.skin-site.home.nav.download-desc": { + "message": "浏览服务器分享的文件。" + }, + "app.skin-site.home.nav.review": { "message": "玩家审核" }, + "app.skin-site.home.nav.review-desc": { "message": "提交并审核玩家申请。" }, + "app.skin-site.home.nav.wiki": { "message": "服务器维基" }, + "app.skin-site.home.nav.wiki-desc": { "message": "服务器的指南与文档。" }, + "app.skin-site.home.nav.bugs": { "message": "问题追踪" }, + "app.skin-site.home.nav.bugs-desc": { "message": "反馈问题并跟踪处理进度。" }, + "app.skin-site.home.nav.map": { "message": "服务器地图" }, + "app.skin-site.home.nav.map-desc": { "message": "探索实时世界地图。" }, + "app.skin-site.home.nav.profile": { "message": "个人资料" }, + "app.skin-site.home.nav.profile-desc": { "message": "管理你的账号与皮肤。" }, + "app.skin-site.admin.wiki.title": { "message": "维基管理" }, + "app.skin-site.admin.wiki.tab-page": { "message": "页面" }, + "app.skin-site.admin.wiki.tab-audit": { "message": "审计日志" }, + "app.skin-site.admin.wiki.tab-settings": { "message": "设置" }, + "app.skin-site.admin.wiki.loading": { "message": "加载中…" }, + "app.skin-site.admin.wiki.page-path": { "message": "页面路径" }, + "app.skin-site.admin.wiki.load-page": { "message": "加载页面" }, + "app.skin-site.admin.wiki.page-not-found": { "message": "未找到该页面。" }, + "app.skin-site.admin.wiki.revision": { "message": "版本" }, + "app.skin-site.admin.wiki.author": { "message": "作者" }, + "app.skin-site.admin.wiki.updated-at": { "message": "更新时间" }, + "app.skin-site.admin.wiki.categories": { "message": "分类" }, + "app.skin-site.admin.wiki.read-policy": { "message": "阅读权限" }, + "app.skin-site.admin.wiki.edit-policy": { "message": "编辑权限" }, + "app.skin-site.admin.wiki.save-protect": { "message": "保存权限" }, + "app.skin-site.admin.wiki.protect-saved": { "message": "页面权限已更新。" }, + "app.skin-site.admin.wiki.set-home": { "message": "设为首页" }, + "app.skin-site.admin.wiki.home-set": { "message": "首页已更新。" }, + "app.skin-site.admin.wiki.set-deleted": { "message": "标记为已删除" }, + "app.skin-site.admin.wiki.set-restored": { "message": "恢复页面" }, + "app.skin-site.admin.wiki.deleted-updated": { + "message": "页面删除状态已更新。" + }, + "app.skin-site.admin.wiki.audit-empty": { "message": "没有审计记录。" }, + "app.skin-site.admin.wiki.audit-target": { "message": "目标" }, + "app.skin-site.admin.wiki.load-more": { "message": "加载更多" }, + "app.skin-site.admin.wiki.settings-hint": { "message": "以逗号分隔域名。" }, + "app.skin-site.admin.wiki.domains": { "message": "域名" }, + "app.skin-site.admin.wiki.home-page": { "message": "首页" }, + "app.skin-site.admin.wiki.default-skin": { "message": "默认皮肤" }, + "app.skin-site.admin.wiki.max-page-bytes": { "message": "页面最大字节数" }, + "app.skin-site.admin.wiki.max-attachment-bytes": { + "message": "附件最大字节数" + }, + "app.skin-site.admin.wiki.save-settings": { "message": "保存设置" }, + "app.skin-site.admin.wiki.settings-saved": { "message": "设置已保存。" }, + "app.skin-site.admin.review.tab-list": { "message": "申请列表" }, + "app.skin-site.admin.review.tab-papers": { "message": "试卷管理" }, + "app.skin-site.admin.review.papers-empty": { "message": "还没有审核试卷。" }, + "app.skin-site.admin.review.new-paper": { "message": "新建试卷" }, + "app.skin-site.admin.review.edit-paper": { "message": "编辑" }, + "app.skin-site.admin.review.paper-title": { "message": "试卷标题" }, + "app.skin-site.admin.review.paper-annotation": { "message": "描述" }, + "app.skin-site.admin.review.paper-enabled": { "message": "启用" }, + "app.skin-site.admin.review.paper-mode": { "message": "阅卷模式" }, + "app.skin-site.admin.review.paper-pass-score": { "message": "通过分数" }, + "app.skin-site.admin.review.paper-target-permissions": { + "message": "通过后获得的身份" + }, + "app.skin-site.admin.review.paper-questions": { "message": "题目" }, + "app.skin-site.admin.review.add-question": { "message": "添加题目" }, + "app.skin-site.admin.review.question-title": { "message": "题目" }, + "app.skin-site.admin.review.question-type": { "message": "题型" }, + "app.skin-site.admin.review.question-score": { "message": "分数" }, + "app.skin-site.admin.review.question-annotation": { "message": "题目注解" }, + "app.skin-site.admin.review.add-option": { "message": "添加选项" }, + "app.skin-site.admin.review.option-description": { "message": "选项文本" }, + "app.skin-site.admin.review.option-correct": { "message": "正确" }, + "app.skin-site.admin.review.remove-question": { "message": "删除" }, + "app.skin-site.admin.review.question-no": { "message": "第 {n} 题" }, + "app.skin-site.admin.review.paper-created": { "message": "试卷已创建。" }, + "app.skin-site.admin.review.paper-saved": { "message": "试卷已保存。" }, + "app.skin-site.admin.review.mail-title": { "message": "通过邮件" }, + "app.skin-site.admin.review.mail-enabled": { + "message": "审核通过后发送邮件" + }, + "app.skin-site.admin.review.mail-subject": { "message": "邮件标题" }, + "app.skin-site.admin.review.mail-message": { "message": "邮件正文" }, + "app.skin-site.admin.review.mail-extra": { "message": "邮件补充说明" }, + "app.skin-site.admin.review.mail-include-invite": { "message": "附带邀请码" }, + "app.skin-site.admin.review.prevent-duplicate": { "message": "禁止重复作答" }, + "app.skin-site.admin.review.copy-link": { "message": "复制直链" }, + "app.skin-site.admin.review.link-copied": { "message": "直链已复制。" }, + "app.skin-site.admin.review.move-up": { "message": "上移" }, + "app.skin-site.admin.review.move-down": { "message": "下移" }, + "app.skin-site.admin.review.mail-subject-required": { + "message": "开启邮件后必须填写邮件标题。" + }, + "app.skin-site.admin.review.mail-message-required": { + "message": "开启邮件后必须填写邮件正文。" + }, + "app.skin-site.admin.users.actions": { "message": "操作" }, + "app.skin-site.admin.users.audit": { "message": "审计" }, + "app.skin-site.admin.users.audit-empty": { "message": "暂无管理审计记录。" }, + "app.skin-site.admin.users.audit-more": { "message": "加载更早记录" }, + "app.skin-site.admin.users.audit-title": { "message": "用户管理审计" }, + "app.skin-site.admin.users.bot": { "message": "机器人" }, + "app.skin-site.admin.users.bot-active": { "message": "Token 有效" }, + "app.skin-site.admin.users.bot-ban": { "message": "封禁 Bot 创建" }, + "app.skin-site.admin.users.bot-creation-banned": { + "message": "禁止创建 Token" + }, + "app.skin-site.admin.users.bot-exists": { "message": "已创建 Bot" }, + "app.skin-site.admin.users.bot-inactive": { "message": "Token 已失效" }, + "app.skin-site.admin.users.bot-invalidate": { "message": "使 Token 失效" }, + "app.skin-site.admin.users.bot-not-exists": { "message": "尚未创建 Bot" }, + "app.skin-site.admin.users.bot-save-username": { + "message": "保存机器人用户名" + }, + "app.skin-site.admin.users.bot-title": { "message": "Bot 管理" }, + "app.skin-site.admin.users.bot-token-hint": { "message": "Token 标识" }, + "app.skin-site.admin.users.bot-unban": { "message": "解除 Bot 封禁" }, + "app.skin-site.admin.users.bot-username": { "message": "机器人用户名" }, + "app.skin-site.admin.users.bound-players-title": { "message": "绑定玩家" }, + "app.skin-site.admin.users.frequency": { "message": "频率限制" }, + "app.skin-site.admin.users.frequency-clear": { "message": "取消限制" }, + "app.skin-site.admin.users.frequency-current": { + "message": "当前 {percent}%" + }, + "app.skin-site.admin.users.frequency-hint": { + "message": "以下列频率对指定用户进行丢包处理,限制其进入服务器的频率。" + }, + "app.skin-site.admin.users.frequency-none": { "message": "当前未启用" }, + "app.skin-site.admin.users.frequency-range": { + "message": "仅允许设置 10% 至 100% 的整数。" + }, + "app.skin-site.admin.users.frequency-save": { "message": "保存限制" }, + "app.skin-site.admin.users.frequency-title": { "message": "频率限制" }, + "app.skin-site.admin.users.invite": { "message": "邀请用户" }, + "app.skin-site.admin.users.invite-copied": { "message": "邀请码已复制。" }, + "app.skin-site.admin.users.invite-copy": { "message": "复制" }, + "app.skin-site.admin.users.invite-email": { "message": "邮箱" }, + "app.skin-site.admin.users.invite-email-invalid": { + "message": "请输入正确格式的邮箱。" + }, + "app.skin-site.admin.users.invite-generate": { "message": "生成邀请码" }, + "app.skin-site.admin.users.invite-title": { "message": "邀请用户" }, + "app.skin-site.admin.users.legacy-adapted": { "message": "已适配旧内服" }, + "app.skin-site.admin.users.registered-at": { "message": "注册时间" }, + "app.skin-site.admin.users.status": { "message": "状态" }, + "app.skin-site.admin.users.status-banned": { "message": "已封禁" }, + "app.skin-site.admin.users.status-normal": { "message": "正常" }, + "app.skin-site.admin.mods.upload": { "message": "上传" }, + "app.skin-site.admin.mods.select-mode": { "message": "选择多个" }, + "app.skin-site.admin.mods.exit-select": { "message": "完成选择" }, + "app.skin-site.admin.mods.select-all": { "message": "全选本页" }, + "app.skin-site.admin.mods.clear-selection": { "message": "取消全选" }, + "app.skin-site.admin.mods.selected-count": { "message": "已选择 {count} 项" }, + "app.skin-site.admin.mods.delete-selected": { "message": "删除所选" }, + "app.skin-site.admin.mods.confirm-delete-multi": { + "message": "确定删除选中的 {count} 个 Mod?此操作不可撤销。" + }, + "app.skin-site.admin.mods.deleted-multi": { "message": "已删除所选 Mod。" }, + "app.skin-site.admin.mods.server-address": { "message": "服务器地址" }, + "app.skin-site.admin.mods.server-address-hint": { + "message": "按服务器地址过滤" + }, + "app.skin-site.admin.mods.apply": { "message": "应用" }, + "app.skin-site.admin.mods.clear": { "message": "清除" }, + "app.skin-site.admin.mods.upload-title": { "message": "上传 Mod" }, + "app.skin-site.admin.mods.upload-drop": { + "message": "拖拽 .jar 文件到此处,或点击选择" + }, + "app.skin-site.admin.mods.upload-sub": { + "message": "支持多文件,单个不超过 {size} MB" + }, + "app.skin-site.admin.mods.upload-tags": { "message": "附加规则标签" }, + "app.skin-site.admin.mods.upload-queue": { "message": "文件列表" }, + "app.skin-site.admin.mods.upload-pending": { "message": "等待中" }, + "app.skin-site.admin.mods.upload-checking": { "message": "检查中…" }, + "app.skin-site.admin.mods.upload-uploading": { "message": "上传中…" }, + "app.skin-site.admin.mods.upload-conflict": { "message": "名称已存在" }, + "app.skin-site.admin.mods.upload-success": { "message": "完成" }, + "app.skin-site.admin.mods.upload-error": { "message": "失败" }, + "app.skin-site.admin.mods.upload-exists": { + "message": "该文件已存在于服务器。" + }, + "app.skin-site.admin.mods.upload-name-conflict": { + "message": "已存在同名 Mod" + }, + "app.skin-site.admin.mods.upload-name-conflict-hint": { + "message": "覆盖已有 Mod,或跳过此文件。" + }, + "app.skin-site.admin.mods.upload-overwrite": { "message": "覆盖" }, + "app.skin-site.admin.mods.upload-skip": { "message": "跳过" }, + "app.skin-site.admin.mods.upload-close": { "message": "关闭" }, + "app.skin-site.admin.mods.upload-all-done": { + "message": "全部文件处理完成。" + }, + "app.skin-site.admin.mods.upload-too-large": { + "message": "文件过大:{name}" + }, + "app.skin-site.chat.avatar.label": { "message": "房间头像" }, + "app.skin-site.chat.avatar.upload": { "message": "上传头像" }, + "app.skin-site.chat.avatar.replace": { "message": "更换头像" }, + "app.skin-site.chat.avatar.remove": { "message": "移除头像" }, + "app.skin-site.chat.avatar.hint": { + "message": "支持 PNG、JPEG、WebP,单边不超过 8192 像素,文件不超过 5 MB。" + }, + "app.skin-site.chat.avatar.bad-format": { + "message": "房间头像仅支持 PNG、JPEG 和 WebP 格式。" + }, + "app.skin-site.chat.avatar.too-large": { + "message": "房间头像大小不能超过 5 MB。" + }, + "app.skin-site.chat.avatar.not-image": { "message": "房间头像必须是图片。" }, + "app.skin-site.chat.admin.title": { "message": "聊天室管理" }, + "app.skin-site.chat.admin.subtitle": { + "message": "创建公开或私有房间,并将日常管理权限交给合适的成员。" + }, + "app.skin-site.chat.admin.create-room": { "message": "创建房间" }, + "app.skin-site.chat.admin.loading": { "message": "加载中…" }, + "app.skin-site.chat.admin.failed": { "message": "无法加载房间列表。" }, + "app.skin-site.chat.admin.empty": { "message": "还没有聊天室。" }, + "app.skin-site.chat.admin.edit": { "message": "编辑" }, + "app.skin-site.chat.admin.remove": { "message": "删除" }, + "app.skin-site.chat.admin.save": { "message": "保存" }, + "app.skin-site.chat.admin.cancel": { "message": "取消" }, + "app.skin-site.chat.admin.name": { "message": "房间名称" }, + "app.skin-site.chat.admin.description": { "message": "房间简介" }, + "app.skin-site.chat.admin.visibility": { "message": "可见性" }, + "app.skin-site.chat.admin.visibility-public": { "message": "公开" }, + "app.skin-site.chat.admin.visibility-private": { "message": "私有" }, + "app.skin-site.chat.admin.kind": { "message": "房间类型" }, + "app.skin-site.chat.admin.kind-standard": { "message": "普通房间" }, + "app.skin-site.chat.admin.kind-settlement": { "message": "聚落房间" }, + "app.skin-site.chat.admin.prerequisite-mode": { "message": "前置条件判定" }, + "app.skin-site.chat.admin.prerequisite-any": { "message": "满足任一" }, + "app.skin-site.chat.admin.prerequisite-all": { "message": "满足全部" }, + "app.skin-site.chat.admin.prerequisite-permissions": { + "message": "所需权限" + }, + "app.skin-site.chat.admin.prerequisite-hint": { + "message": "以逗号分隔;留空表示无限制。" + }, + "app.skin-site.chat.admin.created": { "message": "房间已创建。" }, + "app.skin-site.chat.admin.updated": { "message": "房间资料已更新。" }, + "app.skin-site.chat.admin.deleted": { "message": "房间已删除。" }, + "app.skin-site.chat.admin.confirm-delete": { + "message": "确认永久删除房间“{name}”及其中全部消息吗?" + }, + "app.skin-site.chat.admin.manager-badge": { "message": "管理" }, + "app.skin-site.chat.admin.voice-badge": { "message": "语音" }, + "app.skin-site.chat.user-picker.label": { "message": "选择用户" }, + "app.skin-site.chat.user-picker.placeholder": { + "message": "搜索用户名、邮箱或账号 ID" + }, + "app.skin-site.chat.user-picker.by-user": { "message": "按用户信息" }, + "app.skin-site.chat.user-picker.by-player": { "message": "按玩家 ID" }, + "app.skin-site.chat.user-picker.hint": { + "message": "支持按用户名、邮箱模糊搜索,也可输入账号 ID 搜索。" + }, + "app.skin-site.chat.user-picker.player-hint": { + "message": "输入玩家名称,查找绑定该玩家 ID 的用户。" + }, + "app.skin-site.chat.user-picker.empty": { "message": "没有找到匹配的用户" }, + "app.skin-site.chat.user-picker.banned": { + "message": "该账号已被封禁,不能执行此操作。" + }, + "app.skin-site.chat.user-picker.guest": { + "message": "游客不能成为房管、聚落主或聚落成员。" + }, + "app.skin-site.chat.user-picker.already-in": { + "message": "该用户已经在当前房间中。" + }, + "app.skin-site.chat.user-picker.not-allowed": { + "message": "该用户不在当前允许选择的范围内" + }, + "app.skin-site.chat.user-picker.select": { "message": "选择此用户" }, + "app.skin-site.chat.user-picker.selected": { "message": "已选择此用户" }, + "app.skin-site.chat.user-picker.id": { "message": "ID" }, + "app.skin-site.chat.admin.managers": { "message": "房管" }, + "app.skin-site.chat.admin.manager-title": { "message": "任命房管" }, + "app.skin-site.chat.admin.manager-hint": { + "message": "房管可以管理该房间的成员、消息与文件。" + }, + "app.skin-site.chat.admin.grant": { "message": "任命" }, + "app.skin-site.chat.admin.revoke": { "message": "撤销" }, + "app.skin-site.chat.admin.manager-granted": { "message": "已任命房管。" }, + "app.skin-site.chat.admin.manager-revoked": { "message": "已撤销房管。" }, + "app.skin-site.chat.admin.owner-title": { "message": "聚落主" }, + "app.skin-site.chat.admin.owner-hint": { + "message": "为该房间任命首任聚落主。" + }, + "app.skin-site.chat.admin.owner-assigned": { "message": "已任命聚落主。" }, + "app.skin-site.chat.admin.owner-current": { "message": "当前聚落主:{name}" }, + "app.skin-site.chat.admin.owner-none": { "message": "尚未设置聚落主。" }, + "app.skin-site.chat.admin.audit": { "message": "审计" }, + "app.skin-site.chat.admin.audit-title": { "message": "房间审计" }, + "app.skin-site.chat.admin.audit-empty": { "message": "暂无审计记录。" }, + "app.skin-site.chat.admin.audit-target": { "message": "对象:{name}" }, + "app.skin-site.chat.admin.close": { "message": "关闭" }, + "app.skin-site.chat.voice-runtime-title": { "message": "实时语音库" }, + "app.skin-site.chat.voice-logs": { "message": "查看日志" }, + "app.skin-site.chat.voice-config": { "message": "配置语音库" }, + "app.skin-site.chat.voice-retry": { "message": "重新部署" }, + "app.skin-site.chat.voice-config-title": { "message": "语音库配置" }, + "app.skin-site.chat.voice-logs-title": { "message": "LiveKit 日志" }, + "app.skin-site.chat.voice-capacity-title": { "message": "语音容量" }, + "app.skin-site.chat.voice-capacity": { "message": "语音容量" }, + "app.skin-site.chat.participant-limit": { "message": "参与人数上限" }, + "app.skin-site.chat.listener-limit": { "message": "旁听人数上限" }, + "app.skin-site.chat.voice-capacity-updated": { + "message": "语音容量已更新。" + }, + "app.skin-site.chat.voice-enabled": { "message": "启用实时语音" }, + "app.skin-site.chat.voice-runtime-path": { "message": "语音库目录" }, + "app.skin-site.chat.voice-mirror": { "message": "镜像基础地址" }, + "app.skin-site.chat.voice-public-url": { "message": "公网访问地址" }, + "app.skin-site.chat.voice-bind": { "message": "监听地址" }, + "app.skin-site.chat.voice-signal-port": { "message": "信令端口" }, + "app.skin-site.chat.voice-rtc-tcp-port": { "message": "媒体 TCP 端口" }, + "app.skin-site.chat.voice-rtc-udp-port": { "message": "媒体 UDP 端口" }, + "app.skin-site.chat.voice-startup-timeout": { + "message": "启动等待时间(秒)" + }, + "app.skin-site.chat.voice-use-external-ip": { "message": "使用外部 IP" }, + "app.skin-site.chat.voice-node-ip": { "message": "节点 IP" }, + "app.skin-site.chat.voice-saved": { "message": "语音库配置已保存。" }, + "app.skin-site.chat.voice-logs-auto-refresh": { "message": "自动刷新" }, + "app.skin-site.chat.voice-logs-refresh": { "message": "刷新" }, + "app.skin-site.chat.voice-logs-empty": { "message": "暂无日志。" }, + "app.skin-site.chat.admin.voice-runtime-title": { "message": "实时语音库" }, + "app.skin-site.chat.admin.voice-logs": { "message": "查看日志" }, + "app.skin-site.chat.admin.voice-config": { "message": "配置语音库" }, + "app.skin-site.chat.admin.voice-retry": { "message": "重新部署" }, + "app.skin-site.chat.admin.voice-config-title": { "message": "语音库配置" }, + "app.skin-site.chat.admin.voice-logs-title": { "message": "LiveKit 日志" }, + "app.skin-site.chat.admin.voice-capacity-title": { "message": "语音容量" }, + "app.skin-site.chat.admin.voice-capacity": { "message": "语音容量" }, + "app.skin-site.chat.admin.participant-limit": { "message": "参与人数上限" }, + "app.skin-site.chat.admin.listener-limit": { "message": "旁听人数上限" }, + "app.skin-site.chat.admin.voice-capacity-updated": { + "message": "语音容量已更新。" + }, + "app.skin-site.chat.admin.voice-enabled": { "message": "启用实时语音" }, + "app.skin-site.chat.admin.voice-runtime-path": { "message": "语音库目录" }, + "app.skin-site.chat.admin.voice-mirror": { "message": "镜像基础地址" }, + "app.skin-site.chat.admin.voice-public-url": { "message": "公网访问地址" }, + "app.skin-site.chat.admin.voice-bind": { "message": "监听地址" }, + "app.skin-site.chat.admin.voice-signal-port": { "message": "信令端口" }, + "app.skin-site.chat.admin.voice-rtc-tcp-port": { "message": "媒体 TCP 端口" }, + "app.skin-site.chat.admin.voice-rtc-udp-port": { "message": "媒体 UDP 端口" }, + "app.skin-site.chat.admin.voice-startup-timeout": { + "message": "启动等待时间(秒)" + }, + "app.skin-site.chat.admin.voice-use-external-ip": { + "message": "使用外部 IP" + }, + "app.skin-site.chat.admin.voice-node-ip": { "message": "节点 IP" }, + "app.skin-site.chat.admin.voice-saved": { "message": "语音库配置已保存。" }, + "app.skin-site.chat.admin.voice-logs-auto-refresh": { "message": "自动刷新" }, + "app.skin-site.chat.admin.voice-logs-refresh": { "message": "刷新" }, + "app.skin-site.chat.admin.voice-logs-empty": { "message": "暂无日志。" }, + "app.skin-site.admin.wiki.tab-migration": { "message": "迁移中心" }, + "app.skin-site.admin.wiki.migration-title": { "message": "MediaWiki 迁移" }, + "app.skin-site.admin.wiki.migration-hint": { + "message": "迁移内容会先进入不可见暂存区,所有页面完成导入或排除审核后才能整批发布。" + }, + "app.skin-site.admin.wiki.auto-title": { + "message": "从 MediaWiki API 自动读取" + }, + "app.skin-site.admin.wiki.api-url": { "message": "api.php 地址" }, + "app.skin-site.admin.wiki.bot-username": { "message": "Bot 用户名(选填)" }, + "app.skin-site.admin.wiki.bot-password": { "message": "Bot 密码(选填)" }, + "app.skin-site.admin.wiki.credentials-hint": { + "message": "凭据只存在于本次后台任务的内存中,携带凭据时必须使用 HTTPS。" + }, + "app.skin-site.admin.wiki.allow-private": { + "message": "明确允许访问内网来源" + }, + "app.skin-site.admin.wiki.allow-private-hint": { + "message": "内网来源可能访问皮肤站所在网络中的服务,请只对可信地址启用。" + }, + "app.skin-site.admin.wiki.start-auto": { "message": "创建自动迁移任务" }, + "app.skin-site.admin.wiki.xml-title": { "message": "导入 MediaWiki XML" }, + "app.skin-site.admin.wiki.xml-file": { "message": "导出文件" }, + "app.skin-site.admin.wiki.start-xml": { "message": "上传并建立暂存任务" }, + "app.skin-site.admin.wiki.stale-rules": { "message": "疑似过时页面筛选规则" }, + "app.skin-site.admin.wiki.stale-days": { "message": "未更新天数" }, + "app.skin-site.admin.wiki.maintainer-window": { "message": "维护者统计天数" }, + "app.skin-site.admin.wiki.max-maintainers": { "message": "维护者人数不超过" }, + "app.skin-site.admin.wiki.stale-markers": { + "message": "过时分类、模板或文字标记" + }, + "app.skin-site.admin.wiki.stale-markers-hint": { "message": "使用逗号分隔" }, + "app.skin-site.admin.wiki.jobs": { "message": "迁移任务" }, + "app.skin-site.admin.wiki.refresh-jobs": { "message": "刷新" }, + "app.skin-site.admin.wiki.jobs-empty": { "message": "还没有迁移任务。" }, + "app.skin-site.admin.wiki.reviewed-count": { + "message": "已审核 {reviewed} / {total}" + }, + "app.skin-site.admin.wiki.imported-excluded": { + "message": "导入 {imported} · 排除 {excluded}" + }, + "app.skin-site.admin.wiki.view-job": { "message": "查看任务" }, + "app.skin-site.admin.wiki.delete-job": { "message": "删除迁移记录" }, + "app.skin-site.admin.wiki.confirm-delete-job": { + "message": "确认删除迁移任务 #{id}?此操作不可恢复。" + }, + "app.skin-site.admin.wiki.candidates": { "message": "候选页面" }, + "app.skin-site.admin.wiki.candidates-empty": { + "message": "选择任务后查看其页面。" + }, + "app.skin-site.admin.wiki.last-updated": { "message": "更新于 {date}" }, + "app.skin-site.admin.wiki.maintainer-count": { + "message": "{count} 位维护者" + }, + "app.skin-site.admin.wiki.conflict": { "message": "标题冲突" }, + "app.skin-site.admin.wiki.target-title": { "message": "目标标题" }, + "app.skin-site.admin.wiki.overwrite": { "message": "覆盖" }, + "app.skin-site.admin.wiki.import": { "message": "导入" }, + "app.skin-site.admin.wiki.exclude": { "message": "排除" }, + "app.skin-site.admin.wiki.publish": { "message": "整批发布" }, + "app.skin-site.admin.wiki.rollback": { "message": "回滚" }, + "app.skin-site.admin.wiki.confirm-rollback": { + "message": "确认回滚本批迁移?已发布的页面将被还原。" + }, + "app.skin-site.admin.wiki.state-importing": { "message": "正在读取" }, + "app.skin-site.admin.wiki.state-review": { "message": "等待审核" }, + "app.skin-site.admin.wiki.state-publishing": { "message": "正在发布" }, + "app.skin-site.admin.wiki.state-published": { "message": "已发布" }, + "app.skin-site.admin.wiki.state-failed": { "message": "失败" }, + "app.skin-site.admin.wiki.state-rolled-back": { "message": "已回滚" }, + "app.skin-site.admin.wiki.decision-pending": { "message": "待审核" }, + "app.skin-site.admin.wiki.decision-import": { "message": "导入" }, + "app.skin-site.admin.wiki.decision-exclude": { "message": "排除" }, + "app.skin-site.admin.wiki.migration-started": { + "message": "迁移任务已创建。" + }, + "app.skin-site.admin.wiki.xml-staged": { + "message": "XML 已解析并进入逐页审核阶段。" + }, + "app.skin-site.admin.wiki.published": { + "message": "已发布 {imported} 个页面,覆盖 {overwritten} 个。" + }, + "app.skin-site.admin.wiki.rolled-back": { "message": "迁移批次已安全回滚。" }, + "app.skin-site.admin.wiki.job-deleted": { "message": "迁移记录已删除。" }, + "app.skin-site.admin.wiki.select-xml": { + "message": "请先选择 MediaWiki XML 文件。" + }, + "app.skin-site.wiki.title": { "message": "StarLight 维基" }, + "app.skin-site.wiki.search-placeholder": { "message": "输入页面标题" }, + "app.skin-site.wiki.open": { "message": "打开" }, + "app.skin-site.wiki.loading": { "message": "加载中…" }, + "app.skin-site.wiki.not-found": { "message": "页面不存在。" }, + "app.skin-site.wiki.empty": { "message": "在上方输入页面标题开始阅读。" }, + "app.skin-site.wiki.updated-at": { + "message": "最后编辑于 {date},编辑者 {author}" + }, + "app.skin-site.wiki.revision": { "message": "修订 #{id}" }, + "app.skin-site.wiki.categories": { "message": "分类" }, + "app.skin-site.wiki.back": { "message": "返回" } } diff --git a/scripts/axolotl/i18n-check.mjs b/scripts/axolotl/i18n-check.mjs index 1f52425..f8b9a1d 100644 --- a/scripts/axolotl/i18n-check.mjs +++ b/scripts/axolotl/i18n-check.mjs @@ -1,5 +1,6 @@ import { readFile } from 'node:fs/promises' import { parse, TYPE } from '@formatjs/icu-messageformat-parser' +import { collectSkinSiteMessages } from './skin-site-messages.mjs' const localePairs = [ [ @@ -10,6 +11,22 @@ const localePairs = [ ] const failures = [] +const skinSiteCatalogs = Object.fromEntries( + await Promise.all( + ['en-US', 'zh-CN'].map(async (locale) => [ + locale, + JSON.parse(await readFile(`apps/app-frontend/src/locales/${locale}/index.json`, 'utf8')), + ]), + ), +) +for (const { id, file } of collectSkinSiteMessages()) { + for (const locale of ['en-US', 'zh-CN']) { + const catalogPath = `apps/app-frontend/src/locales/${locale}/index.json` + const catalog = skinSiteCatalogs[locale] + if (!catalog[id]?.message) + failures.push(`${catalogPath}: missing source message ${id} (${file})`) + } +} const allowedUntranslatedMessages = new Set([ 'Chaos Cubed', 'MINECON Earth 2017', diff --git a/scripts/axolotl/skin-site-i18n.test.mjs b/scripts/axolotl/skin-site-i18n.test.mjs new file mode 100644 index 0000000..c733dba --- /dev/null +++ b/scripts/axolotl/skin-site-i18n.test.mjs @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import test from 'node:test' + +import { collectSkinSiteMessages } from './skin-site-messages.mjs' + +test('all skin-site messages used in source exist in both release language catalogs', () => { + const messages = collectSkinSiteMessages() + assert.ok(messages.length > 100, 'The check must scan the actual UI') + for (const locale of ['en-US', 'zh-CN']) { + const catalog = JSON.parse( + fs.readFileSync(`apps/app-frontend/src/locales/${locale}/index.json`, 'utf8'), + ) + const missing = messages.filter(({ id }) => !catalog[id]?.message) + assert.deepEqual( + missing.map(({ id }) => id), + [], + `${locale} has uncatalogued UI messages`, + ) + } +}) + +test('Chinese login and announcement labels do not fall back to English', () => { + const catalog = JSON.parse( + fs.readFileSync('apps/app-frontend/src/locales/zh-CN/index.json', 'utf8'), + ) + for (const id of [ + 'app.skin-site.login.title', + 'app.skin-site.login.username', + 'app.skin-site.login.password-hint', + 'app.skin-site.home.announcements.title', + 'app.skin-site.home.announcements.failed', + ]) { + assert.match(catalog[id]?.message ?? '', /\p{Script=Han}/u, id) + } +}) diff --git a/scripts/axolotl/skin-site-messages.mjs b/scripts/axolotl/skin-site-messages.mjs new file mode 100644 index 0000000..776cf72 --- /dev/null +++ b/scripts/axolotl/skin-site-messages.mjs @@ -0,0 +1,59 @@ +import fs from 'node:fs' +import path from 'node:path' +import { createRequire } from 'node:module' + +const require = createRequire(new URL('../../apps/app-frontend/package.json', import.meta.url)) +const ts = require('typescript') +const { parse } = require('vue/compiler-sfc') + +/** Read the message IDs actually used by skin-site UI, including uncatalogued messages. */ +export function collectSkinSiteMessages(root = 'apps/app-frontend/src') { + const messages = new Map() + function walk(directory) { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const file = path.join(directory, entry.name) + if (entry.isDirectory()) { + if (entry.name !== 'locales') walk(file) + continue + } + if (!/\.(vue|ts)$/.test(file) || file.endsWith('.test.ts')) continue + let source = fs.readFileSync(file, 'utf8') + if (file.endsWith('.vue')) { + const { descriptor } = parse(source) + source = [descriptor.script?.content, descriptor.scriptSetup?.content] + .filter(Boolean) + .join('\n') + } + const ast = ts.createSourceFile(`${file}.ts`, source, ts.ScriptTarget.Latest, true) + function visit(node) { + if (ts.isObjectLiteralExpression(node)) { + const properties = Object.fromEntries( + node.properties + .filter(ts.isPropertyAssignment) + .map((property) => [ + property.name.getText(ast).replace(/^['"]|['"]$/g, ''), + property.initializer, + ]), + ) + if ( + properties.id && + properties.defaultMessage && + ts.isStringLiteralLike(properties.id) && + ts.isStringLiteralLike(properties.defaultMessage) && + properties.id.text.startsWith('app.skin-site.') + ) { + messages.set(properties.id.text, { + id: properties.id.text, + defaultMessage: properties.defaultMessage.text, + file, + }) + } + } + ts.forEachChild(node, visit) + } + visit(ast) + } + } + walk(root) + return [...messages.values()] +} From 1838afce5614ecc0bdf686fbbf90da8efa9ba906 Mon Sep 17 00:00:00 2001 From: Disy Date: Tue, 20 Oct 2026 20:36:53 +0800 Subject: [PATCH 10/11] chore: release 1.0.2-beta11 --- Cargo.lock | 4 +- apps/app-frontend/package.json | 150 +++++++++--------- .../app-frontend/src/announcements/catalog.ts | 43 +++++ apps/app/Cargo.toml | 2 +- packages/app-lib/Cargo.toml | 2 +- 5 files changed, 122 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0bac7a9..eac8fb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6841,7 +6841,7 @@ dependencies = [ [[package]] name = "theseus" -version = "1.0.2-beta10" +version = "1.0.2-beta11" dependencies = [ "ariadne", "async-minecraft-ping", @@ -6945,7 +6945,7 @@ dependencies = [ [[package]] name = "theseus_gui" -version = "1.0.2-beta10" +version = "1.0.2-beta11" dependencies = [ "ashpd", "async_zip", diff --git a/apps/app-frontend/package.json b/apps/app-frontend/package.json index 372548f..08cca45 100644 --- a/apps/app-frontend/package.json +++ b/apps/app-frontend/package.json @@ -1,77 +1,77 @@ { - "name": "@modrinth/app-frontend", - "private": true, - "version": "1.0.2-beta10", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vue-tsc --noEmit && vite build", - "tsc:check": "vue-tsc --noEmit", - "lint": "eslint . && prettier --check .", - "fix": "eslint . --fix && prettier --write .", - "intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace", - "intl:prune-local": "pnpm -w scripts i18n-icu-contract prune-local --scope apps/app-frontend", - "test": "vue-tsc --noEmit && node --test \"src/**/*.test.ts\"" - }, - "dependencies": { - "@dnd-kit/core": "6.3.1", - "@dnd-kit/utilities": "3.2.2", - "@dnd-kit/vue": "^0.5.0", - "@lobehub/icons-static-svg": "1.94.0", - "@modrinth/api-client": "workspace:^", - "@modrinth/assets": "workspace:*", - "@modrinth/server": "workspace:*", - "@modrinth/ui": "workspace:*", - "@modrinth/utils": "workspace:*", - "@sfirew/minecraft-motd-parser": "^1.1.6", - "@tanstack/vue-query": "5.90.7", - "@tauri-apps/api": "^2.5.0", - "@tauri-apps/plugin-dialog": "^2.2.1", - "@tauri-apps/plugin-fs": "^2.4.5", - "@tauri-apps/plugin-http": "~2.5.7", - "@tauri-apps/plugin-opener": "^2.2.6", - "@tauri-apps/plugin-os": "^2.2.1", - "@tauri-apps/plugin-updater": "^2.7.1", - "@tauri-apps/plugin-window-state": "^2.2.2", - "@types/three": "^0.172.0", - "@vueuse/core": "^11.1.0", - "dayjs": "^1.11.10", - "deepslate": "0.26.0", - "fflate": "0.8.2", - "floating-vue": "^5.2.2", - "fuse.js": "^6.6.2", - "intl-messageformat": "^10.7.7", - "monaco-editor": "^0.56.0", - "ofetch": "^1.3.4", - "overlayscrollbars": "^2.15.1", - "pinia": "^3.0.0", - "three": "^0.172.0", - "vite-svg-loader": "^5.1.0", - "vue": "^3.5.13", - "vue-i18n": "^10.0.0", - "vue-router": "^4.6.0", - "vuedraggable": "^4.1.0", - "xxhash-wasm": "1.1.0" - }, - "devDependencies": { - "@eslint/compat": "^1.1.1", - "@formatjs/cli": "^6.2.12", - "@modrinth/tooling-config": "workspace:*", - "@nuxt/eslint-config": "^0.5.6", - "@vitejs/plugin-vue": "^6.0.3", - "autoprefixer": "^10.4.19", - "eslint": "^9.9.1", - "eslint-plugin-turbo": "^2.5.4", - "postcss": "^8.4.39", - "prettier": "^3.2.5", - "sass": "^1.74.1", - "tailwindcss": "^3.4.4", - "typescript": "^5.5.4", - "vite": "^8.0.0", - "vite-plugin-vue-devtools": "^8.2.1", - "vue-component-type-helpers": "^3.1.8", - "vue-tsc": "^2.1.6" - }, - "packageManager": "pnpm@9.4.0", - "web-types": "../../web-types.json" + "name": "@modrinth/app-frontend", + "private": true, + "version": "1.0.2-beta11", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "tsc:check": "vue-tsc --noEmit", + "lint": "eslint . && prettier --check .", + "fix": "eslint . --fix && prettier --write .", + "intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace", + "intl:prune-local": "pnpm -w scripts i18n-icu-contract prune-local --scope apps/app-frontend", + "test": "vue-tsc --noEmit && node --test \"src/**/*.test.ts\"" + }, + "dependencies": { + "@dnd-kit/core": "6.3.1", + "@dnd-kit/utilities": "3.2.2", + "@dnd-kit/vue": "^0.5.0", + "@lobehub/icons-static-svg": "1.94.0", + "@modrinth/api-client": "workspace:^", + "@modrinth/assets": "workspace:*", + "@modrinth/server": "workspace:*", + "@modrinth/ui": "workspace:*", + "@modrinth/utils": "workspace:*", + "@sfirew/minecraft-motd-parser": "^1.1.6", + "@tanstack/vue-query": "5.90.7", + "@tauri-apps/api": "^2.5.0", + "@tauri-apps/plugin-dialog": "^2.2.1", + "@tauri-apps/plugin-fs": "^2.4.5", + "@tauri-apps/plugin-http": "~2.5.7", + "@tauri-apps/plugin-opener": "^2.2.6", + "@tauri-apps/plugin-os": "^2.2.1", + "@tauri-apps/plugin-updater": "^2.7.1", + "@tauri-apps/plugin-window-state": "^2.2.2", + "@types/three": "^0.172.0", + "@vueuse/core": "^11.1.0", + "dayjs": "^1.11.10", + "deepslate": "0.26.0", + "fflate": "0.8.2", + "floating-vue": "^5.2.2", + "fuse.js": "^6.6.2", + "intl-messageformat": "^10.7.7", + "monaco-editor": "^0.56.0", + "ofetch": "^1.3.4", + "overlayscrollbars": "^2.15.1", + "pinia": "^3.0.0", + "three": "^0.172.0", + "vite-svg-loader": "^5.1.0", + "vue": "^3.5.13", + "vue-i18n": "^10.0.0", + "vue-router": "^4.6.0", + "vuedraggable": "^4.1.0", + "xxhash-wasm": "1.1.0" + }, + "devDependencies": { + "@eslint/compat": "^1.1.1", + "@formatjs/cli": "^6.2.12", + "@modrinth/tooling-config": "workspace:*", + "@nuxt/eslint-config": "^0.5.6", + "@vitejs/plugin-vue": "^6.0.3", + "autoprefixer": "^10.4.19", + "eslint": "^9.9.1", + "eslint-plugin-turbo": "^2.5.4", + "postcss": "^8.4.39", + "prettier": "^3.2.5", + "sass": "^1.74.1", + "tailwindcss": "^3.4.4", + "typescript": "^5.5.4", + "vite": "^8.0.0", + "vite-plugin-vue-devtools": "^8.2.1", + "vue-component-type-helpers": "^3.1.8", + "vue-tsc": "^2.1.6" + }, + "packageManager": "pnpm@9.4.0", + "web-types": "../../web-types.json" } diff --git a/apps/app-frontend/src/announcements/catalog.ts b/apps/app-frontend/src/announcements/catalog.ts index 9a88a76..9df0a57 100644 --- a/apps/app-frontend/src/announcements/catalog.ts +++ b/apps/app-frontend/src/announcements/catalog.ts @@ -32,6 +32,49 @@ export const ANNOUNCEMENT_CHANGE_TYPES: readonly AnnouncementChangeType[] = [ ] export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ + { + id: 'launcher-1.0.2-beta11', + version: '1.0.2-beta11', + publishedAt: '2026-10-20', + title: { + 'en-US': 'Starlight Launcher 1.0.2-beta11', + 'zh-CN': 'Starlight Launcher 1.0.2-beta11', + }, + changes: { + fixed: [ + { + 'en-US': + 'Filled missing Chinese translations across skin site sign-in, announcements, verification, registration, account settings, downloads and maps, and localized remaining user and Wiki labels.', + 'zh-CN': + '补齐皮肤站登录、公告、验证、注册、账号设置、下载和地图等界面的中文翻译,并修复用户信息与维基权限选项中的英文标签。', + }, + { + 'en-US': + 'Renew the saved skin site JWT on launcher startup before loading the user and announcements. Fixed overlapping refresh, sign-in and sign-out requests that could leave the account in an incorrect state.', + 'zh-CN': + '启动启动器时先刷新已保存的皮肤站 JWT,再加载用户与公告;修复刷新、登录和退出请求交错时可能导致的账号状态异常。', + }, + { + 'en-US': + 'Announcements now reload after sign-in, show a sign-in hint when signed out, and offer a retry action after a loading failure.', + 'zh-CN': '公告在登录后自动重新加载,未登录时显示登录提示,加载失败时可直接重试。', + }, + ], + changed: [ + { + 'en-US': + 'Skin site release publishing now uses chunked uploads and reports synchronization failures explicitly. Localization checks also scan message IDs used in source to catch missing catalog entries.', + 'zh-CN': + '皮肤站自动发布改用分片上传,同步失败会明确报错;国际化检查新增源码词条扫描,防止再次遗漏语言包内容。', + }, + ], + }, + notes: { + 'en-US': + 'If the previously saved JWT has already expired, sign in again once to restore the session.', + 'zh-CN': '如果此前保存的 JWT 已经过期,需要重新登录一次以恢复会话。', + }, + }, { id: 'launcher-1.0.2-beta10', version: '1.0.2-beta10', diff --git a/apps/app/Cargo.toml b/apps/app/Cargo.toml index 624d7d6..c17cdde 100644 --- a/apps/app/Cargo.toml +++ b/apps/app/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "theseus_gui" # The actual version is set by the theseus-build workflow on tagging -version = "1.0.2-beta10" +version = "1.0.2-beta11" edition.workspace = true description = "Axolotl Launcher is a cross-platform Minecraft launcher" homepage = "https://www.ghs.red" diff --git a/packages/app-lib/Cargo.toml b/packages/app-lib/Cargo.toml index 6714417..663be6d 100644 --- a/packages/app-lib/Cargo.toml +++ b/packages/app-lib/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "theseus" # The actual version is set by the theseus-build workflow on tagging -version = "1.0.2-beta10" +version = "1.0.2-beta11" edition.workspace = true [dependencies] From bf95d715a478cd4a4e53cb5ea00dd2d749a666cc Mon Sep 17 00:00:00 2001 From: Disy Date: Tue, 20 Oct 2026 21:25:19 +0800 Subject: [PATCH 11/11] fix: make hosted downloads controllable and failures diagnosable --- .../helpers/hosted-download-controls.test.ts | 96 +++++ .../src/helpers/hosted-download-controls.ts | 45 +++ .../helpers/hosted-download-failures.test.ts | 21 ++ apps/app-frontend/src/helpers/hosted-packs.ts | 6 + apps/app-frontend/src/helpers/state.ts | 10 +- .../app-frontend/src/locales/en-US/index.json | 4 + .../app-frontend/src/locales/zh-CN/index.json | 4 + apps/app-frontend/src/pages/Downloads.vue | 84 ++++- apps/app/build.rs | 3 + apps/app/src/api/install.rs | 18 + packages/app-lib/src/api/pack/hosted.rs | 52 ++- .../app-lib/src/api/pack/hosted/control.rs | 329 ++++++++++++++++++ .../app-lib/src/api/pack/hosted/progress.rs | 3 + .../app-lib/src/api/pack/hosted/tagged.rs | 11 +- packages/app-lib/src/event/emit.rs | 81 ++++- packages/app-lib/src/event/mod.rs | 6 + packages/app-lib/src/util/fetch.rs | 147 +++++++- 17 files changed, 848 insertions(+), 72 deletions(-) create mode 100644 apps/app-frontend/src/helpers/hosted-download-controls.test.ts create mode 100644 apps/app-frontend/src/helpers/hosted-download-controls.ts create mode 100644 packages/app-lib/src/api/pack/hosted/control.rs diff --git a/apps/app-frontend/src/helpers/hosted-download-controls.test.ts b/apps/app-frontend/src/helpers/hosted-download-controls.test.ts new file mode 100644 index 0000000..9271252 --- /dev/null +++ b/apps/app-frontend/src/helpers/hosted-download-controls.test.ts @@ -0,0 +1,96 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { + hostedDownloadActions, + hostedDownloadState, + runHostedDownloadAction, + runHostedDownloadControl, +} from './hosted-download-controls.ts' + +test('only a controllable parent hosted task offers pause, resume and stop', () => { + const bar = { + bar_type: { + type: 'hosted_pack_sync', + instance_id: 'one', + state: 'running' as const, + can_pause: true, + can_cancel: true, + }, + } + assert.deepEqual(hostedDownloadActions(bar), ['pause', 'cancel']) + assert.deepEqual(hostedDownloadActions({ bar_type: { ...bar.bar_type, state: 'paused' } }), [ + 'resume', + 'cancel', + ]) + for (const state of ['finalizing', 'canceling'] as const) { + assert.deepEqual(hostedDownloadActions({ bar_type: { ...bar.bar_type, state } }), []) + } + assert.deepEqual( + hostedDownloadActions({ bar_type: { ...bar.bar_type, type: 'hosted_mod_download' } }), + [], + ) + assert.deepEqual( + hostedDownloadActions({ bar_type: { type: 'hosted_pack_sync', instance_id: 'one' } }), + [], + ) +}) + +test('failed and canceled hosted tasks offer direct retry, preserving terminal state', () => { + for (const state of ['failed', 'canceled'] as const) { + const bar = { + bar_type: { type: 'hosted_pack_sync', instance_id: 'one', state, error: 'failure' }, + } + assert.deepEqual(hostedDownloadActions(bar), ['retry']) + assert.equal(hostedDownloadState(bar), state) + } + assert.equal(hostedDownloadState({ bar_type: { error: 'old backend failure' } }), 'failed') +}) + +test('resume refreshes the session before invoking controls; pause and stop need no session', async () => { + const calls: string[] = [] + const deps = { + prepareSession: async () => { + calls.push('session') + }, + invoke: async (command: string, args: { instanceId: string }) => { + calls.push(`${command}:${args.instanceId}`) + }, + } + await runHostedDownloadControl('resume', 'one', deps) + await runHostedDownloadControl('pause', 'one', deps) + await runHostedDownloadControl('cancel', 'one', deps) + assert.deepEqual(calls, [ + 'session', + 'plugin:install|hosted_resume:one', + 'plugin:install|hosted_pause:one', + 'plugin:install|hosted_cancel:one', + ]) +}) + +test('session refresh failures prevent resume', async () => { + await assert.rejects( + runHostedDownloadControl('resume', 'one', { + prepareSession: async () => { + throw new Error('signed out') + }, + invoke: async () => { + assert.fail('resume must not run') + }, + }), + /signed out/, + ) +}) + +test('retry immediately restarts the existing hosted instance through the manager', async () => { + const calls: string[] = [] + await runHostedDownloadAction('retry', 'existing-instance', { + retryHosted: async (instanceId) => { + calls.push(instanceId) + }, + control: async () => { + assert.fail('retry must use the full hosted transaction') + }, + }) + assert.deepEqual(calls, ['existing-instance']) +}) diff --git a/apps/app-frontend/src/helpers/hosted-download-controls.ts b/apps/app-frontend/src/helpers/hosted-download-controls.ts new file mode 100644 index 0000000..af80619 --- /dev/null +++ b/apps/app-frontend/src/helpers/hosted-download-controls.ts @@ -0,0 +1,45 @@ +import type { LoadingBar } from './state.ts' + +export type HostedDownloadControl = 'pause' | 'resume' | 'cancel' +export type HostedDownloadAction = HostedDownloadControl | 'retry' + +export function runHostedDownloadAction( + action: HostedDownloadAction, + instanceId: string, + operations: { + retryHosted: (instanceId: string) => Promise + control: (action: HostedDownloadControl, instanceId: string) => Promise + }, +) { + return action === 'retry' + ? operations.retryHosted(instanceId) + : operations.control(action, instanceId) +} + +export function hostedDownloadState(bar: LoadingBar) { + return bar.bar_type?.state ?? (bar.bar_type?.error ? 'failed' : 'running') +} + +export function hostedDownloadActions(bar: LoadingBar): HostedDownloadAction[] { + const type = bar.bar_type + if (type?.type !== 'hosted_pack_sync' || !type.instance_id) return [] + const state = hostedDownloadState(bar) + if (state === 'failed' || state === 'canceled') return ['retry'] + if (state !== 'running' && state !== 'paused') return [] + const actions: HostedDownloadAction[] = [] + if (type.can_pause) actions.push(state === 'paused' ? 'resume' : 'pause') + if (type.can_cancel) actions.push('cancel') + return actions +} + +export async function runHostedDownloadControl( + action: HostedDownloadControl, + instanceId: string, + dependencies: { + prepareSession: () => Promise + invoke: (command: string, args: { instanceId: string }) => Promise + }, +) { + if (action === 'resume') await dependencies.prepareSession() + await dependencies.invoke(`plugin:install|hosted_${action}`, { instanceId }) +} diff --git a/apps/app-frontend/src/helpers/hosted-download-failures.test.ts b/apps/app-frontend/src/helpers/hosted-download-failures.test.ts index 5c1b4fb..7055141 100644 --- a/apps/app-frontend/src/helpers/hosted-download-failures.test.ts +++ b/apps/app-frontend/src/helpers/hosted-download-failures.test.ts @@ -3,6 +3,27 @@ import test from 'node:test' import { createHostedDownloadFailures } from './hosted-download-failures.ts' +test('stopped hosted downloads retain their state and instance for direct retry', () => { + const state = createHostedDownloadFailures() + state.update({ + fraction: null, + loader_uuid: 'stopped', + event: { + type: 'hosted_pack_sync', + instance_id: 'existing-instance', + state: 'canceled', + error: 'Stopped by user', + can_pause: false, + can_cancel: false, + }, + }) + assert.equal(state.values()[0]?.bar_type?.state, 'canceled') + assert.equal(state.values()[0]?.bar_type?.instance_id, 'existing-instance') + state.begin('existing-instance', state.values()) + assert.equal(state.has('existing-instance'), false) + assert.equal(state.isRetired('stopped'), true) +}) + test('retry clears only its own failure before progress arrives and ignores late old events', () => { const state = createHostedDownloadFailures() const failed = (instanceId: string, loader: string, error: string) => ({ diff --git a/apps/app-frontend/src/helpers/hosted-packs.ts b/apps/app-frontend/src/helpers/hosted-packs.ts index 9904d00..5aa937a 100644 --- a/apps/app-frontend/src/helpers/hosted-packs.ts +++ b/apps/app-frontend/src/helpers/hosted-packs.ts @@ -8,6 +8,7 @@ import { skinSiteUser, waitForSkinSiteSession, } from '../composables/skin-site-session.ts' +import { type HostedDownloadControl, runHostedDownloadControl } from './hosted-download-controls.ts' import { authenticateInstancePlayer } from './instance-player.ts' export const HOSTED_ACCOUNT_REQUIRED_MESSAGE = '您需要登录正版或皮肤站账号后才能操作' @@ -146,6 +147,11 @@ export const hostedCreate = async (gameDirRoot?: string | null) => { } export const hostedBinding = (instanceId: string) => invokeHosted('plugin:install|hosted_binding', { instanceId }) +export const controlHostedDownload = (action: HostedDownloadControl, instanceId: string) => + runHostedDownloadControl(action, instanceId, { + prepareSession: prepareHostedSession, + invoke: invokeHosted, + }) export const hostedSync = async (instanceId: string) => { await requireHostedGameAccount() startHostedPackAttempt(instanceId) diff --git a/apps/app-frontend/src/helpers/state.ts b/apps/app-frontend/src/helpers/state.ts index 011fd72..ba14c3b 100644 --- a/apps/app-frontend/src/helpers/state.ts +++ b/apps/app-frontend/src/helpers/state.ts @@ -9,6 +9,9 @@ export interface LoadingBarType { batch_id?: string file_name?: string error?: string | null + state?: 'running' | 'paused' | 'canceling' | 'canceled' | 'failed' | 'finalizing' + can_pause?: boolean + can_cancel?: boolean type?: string version?: string instance_id?: string @@ -28,7 +31,12 @@ export interface LoadingBar { } export type OpeningCommandEvent = - 'RunMRPack' | 'InstallServer' | 'InstallVersion' | 'InstallMod' | 'InstallModpack' | string + | 'RunMRPack' + | 'InstallServer' + | 'InstallVersion' + | 'InstallMod' + | 'InstallModpack' + | string export interface OpeningCommand { event: OpeningCommandEvent diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index 13e7ee9..30685a3 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -1,4 +1,8 @@ { + "app.hosted-packs.progress.pause": { "message": "Pause" }, + "app.hosted-packs.progress.resume": { "message": "Resume" }, + "app.hosted-packs.progress.stop": { "message": "Stop" }, + "app.downloads.status.paused": { "message": "Paused" }, "app.skin-site.captcha.loading": { "message": "Loading captcha…" }, "app.skin-site.captcha.submit": { "message": "Submit" }, "app.skin-site.captcha.subtitle": { diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json index 0ea5f84..525348e 100644 --- a/apps/app-frontend/src/locales/zh-CN/index.json +++ b/apps/app-frontend/src/locales/zh-CN/index.json @@ -1,4 +1,8 @@ { + "app.hosted-packs.progress.pause": { "message": "暂停" }, + "app.hosted-packs.progress.resume": { "message": "继续" }, + "app.hosted-packs.progress.stop": { "message": "停止" }, + "app.downloads.status.paused": { "message": "已暂停" }, "app.skin-site.captcha.loading": { "message": "正在加载验证码…" }, "app.skin-site.captcha.submit": { "message": "验证" }, "app.skin-site.captcha.subtitle": { diff --git a/apps/app-frontend/src/pages/Downloads.vue b/apps/app-frontend/src/pages/Downloads.vue index dd58120..d5541c9 100644 --- a/apps/app-frontend/src/pages/Downloads.vue +++ b/apps/app-frontend/src/pages/Downloads.vue @@ -84,7 +84,7 @@ - - {{ formatMessage(messages.hostedRetry) }} - + + + + + {{ formatMessage(messages.openInstance) }} + + focusedDownloadJobId(route.query.job)) const focusState = ref(createDownloadFocusState(focusedJobId.value)) const messages = defineMessages({ - hostedRetry: { - id: 'app.hosted-packs.progress.retry', - defaultMessage: 'Return to the instance to retry', - }, newDownload: { id: 'app.downloads.new-download', defaultMessage: 'New download' }, inProgress: { id: 'app.downloads.in-progress', defaultMessage: 'In progress' }, history: { id: 'app.downloads.history', defaultMessage: 'History' }, @@ -653,7 +675,15 @@ const messages = defineMessages({ unknownPhase: { id: 'app.downloads.phase.unknown', defaultMessage: 'Working' }, }) +const hostedActionMessages = defineMessages({ + pause: { id: 'app.hosted-packs.progress.pause', defaultMessage: 'Pause' }, + resume: { id: 'app.hosted-packs.progress.resume', defaultMessage: 'Resume' }, + cancel: { id: 'app.hosted-packs.progress.stop', defaultMessage: 'Stop' }, + retry: { id: 'app.downloads.retry', defaultMessage: 'Retry' }, +}) + const statusMessages = defineMessages({ + paused: { id: 'app.downloads.status.paused', defaultMessage: 'Paused' }, queued: { id: 'app.downloads.status.queued', defaultMessage: 'Queued' }, running: { id: 'app.downloads.status.running', defaultMessage: 'Running' }, canceling: { id: 'app.downloads.status.canceling', defaultMessage: 'Canceling' }, @@ -1262,6 +1292,7 @@ function toggleExpanded(jobId: string) { } async function withBusy(jobId: string, action: () => Promise) { + if (busy.value.has(jobId)) return busy.value = new Set([...busy.value, jobId]) try { await action() @@ -1278,6 +1309,21 @@ async function cancel(job: InstallJobSnapshot) { await withBusy(job.job_id, () => manager.cancel(job.job_id)) } +function hostedBusyKey(bar: LoadingBar, action: HostedDownloadAction) { + return `hosted-${action === 'retry' ? 'retry' : 'control'}:${bar.bar_type?.instance_id}` +} + +async function hostedAction(bar: LoadingBar, action: HostedDownloadAction) { + const instanceId = bar.bar_type?.instance_id + if (!instanceId || !hostedDownloadActions(bar).includes(action)) return + await withBusy(hostedBusyKey(bar, action), () => + runHostedDownloadAction(action, instanceId, { + retryHosted: manager.retryHosted, + control: controlHostedDownload, + }), + ) +} + async function retry(job: InstallJobSnapshot) { await withBusy(job.job_id, () => manager.retry(job.job_id)) } @@ -1293,12 +1339,14 @@ async function resolveMissing(job: InstallJobSnapshot) { (item) => item.status === 'skipped' && item.manual_url && item.project_id && item.version_id, ) - .map((item): CurseForgeManualDownloadItem => ({ - projectId: Number(item.project_id), - fileId: Number(item.version_id), - fileName: item.name, - websiteUrl: item.manual_url ?? undefined, - })) + .map( + (item): CurseForgeManualDownloadItem => ({ + projectId: Number(item.project_id), + fileId: Number(item.version_id), + fileName: item.name, + websiteUrl: item.manual_url ?? undefined, + }), + ) const instanceId = job.instance_id const hasGeneralMissingItems = job.items.some((item) => item.status === 'failed') if (instanceId && (job.provider === 'curse_forge' || fallbackCurseForgeItems.length > 0)) { diff --git a/apps/app/build.rs b/apps/app/build.rs index 5cef161..cb2cdd7 100644 --- a/apps/app/build.rs +++ b/apps/app/build.rs @@ -380,6 +380,9 @@ fn main() { "hosted_create", "hosted_binding", "hosted_sync", + "hosted_pause", + "hosted_resume", + "hosted_cancel", "hosted_instance_mode", "hosted_set_instance_mode", "install_pack_to_existing_instance", diff --git a/apps/app/src/api/install.rs b/apps/app/src/api/install.rs index d5d6b8d..1b5652c 100644 --- a/apps/app/src/api/install.rs +++ b/apps/app/src/api/install.rs @@ -20,6 +20,9 @@ pub fn init() -> tauri::plugin::TauriPlugin { hosted_create, hosted_binding, hosted_sync, + hosted_pause, + hosted_resume, + hosted_cancel, hosted_instance_mode, hosted_set_instance_mode, install_get_modpack_preview, @@ -105,6 +108,21 @@ pub async fn hosted_sync( Ok(theseus::pack::hosted::synchronize(&instance_id).await?) } +#[tauri::command] +pub async fn hosted_pause(instance_id: String) -> Result<()> { + Ok(theseus::pack::hosted::pause(&instance_id).await?) +} + +#[tauri::command] +pub async fn hosted_resume(instance_id: String) -> Result<()> { + Ok(theseus::pack::hosted::resume(&instance_id).await?) +} + +#[tauri::command] +pub async fn hosted_cancel(instance_id: String) -> Result<()> { + Ok(theseus::pack::hosted::cancel(&instance_id).await?) +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct InstallCreateInstanceRequest { diff --git a/packages/app-lib/src/api/pack/hosted.rs b/packages/app-lib/src/api/pack/hosted.rs index 04f42e9..7c1e1f9 100644 --- a/packages/app-lib/src/api/pack/hosted.rs +++ b/packages/app-lib/src/api/pack/hosted.rs @@ -1,5 +1,7 @@ //! Administrator-approved skin-site packs. Only content-addressed changed files cross the network. +mod control; mod progress; +pub use control::{HostedTaskState, cancel, pause, resume}; mod tagged; mod transport; use crate::{ @@ -938,10 +940,20 @@ async fn synchronize_locked(instance_id: &str) -> crate::Result { let metadata = crate::instance::get(instance_id) .await? .ok_or_else(|| invalid("Unknown instance"))?; + let task = control::Registration::new(instance_id)?; let progress = PackProgress::new(instance_id, &metadata.instance.name).await?; - let result = - synchronize_with_progress(instance_id, metadata, &progress).await; + let result = task + .control + .run(|| { + synchronize_with_progress( + instance_id, + metadata.clone(), + &progress, + &task.control, + ) + }) + .await; if let Err(error) = &result { progress.fail(error); } @@ -952,6 +964,7 @@ async fn synchronize_with_progress( instance_id: &str, metadata: crate::state::InstanceMetadata, progress: &PackProgress, + control: &control::Control, ) -> crate::Result { if metadata.instance.linked_launcher.is_some() || metadata.instance.symlink_target.is_some() @@ -1275,7 +1288,7 @@ async fn synchronize_with_progress( &format!("文件 0/{total_files}"), true, ); - let results = stream::iter(pending_downloads.into_values().map( + downloaded += control::collect_downloads(stream::iter(pending_downloads.into_values().map( |(file, url, object)| { let current = ¤t; let completed = &completed; @@ -1342,34 +1355,14 @@ async fn synchronize_with_progress( request = request .with_header("Authorization", auth.clone()); } - let first_result = download_to_path( + download_to_path( request.clone(), &object, &state.fetch_semaphore, &state.pool, Some(&mut on_progress), ) - .await; - if let Err(error) = first_result { - tracing::warn!( - path = %file_path, - %error, - "Parallel StarLight file download failed; retrying after the batch load has eased" - ); - ensure_session(auth).await?; - tokio::time::sleep(std::time::Duration::from_millis( - 750, - )) - .await; - download_to_path( - request, - &object, - &state.fetch_semaphore, - &state.pool, - Some(&mut on_progress), - ) - .await?; - } + .await.map_err(|error| error.with_context(format!("下载整合包文件失败:{file_path}")))?; let completed_files = completed.fetch_add(1, Ordering::Relaxed) + 1; let completed_bytes = { @@ -1391,12 +1384,7 @@ async fn synchronize_with_progress( } }, )) - .buffer_unordered(concurrency) - .collect::>() - .await; - downloaded += results.into_iter().try_fold(0, |sum, result| { - Ok::(sum + result?) - })?; + .buffer_unordered(concurrency)).await?; } ensure_session(&auth).await?; downloaded += tagged::download( @@ -1446,6 +1434,8 @@ async fn synchronize_with_progress( previous, metadata, }; + // From this atomic boundary onward the rollback/runtime transaction must finish. + control.begin_finalizing()?; write_json(&root, JOURNAL, &journal)?; let result = async { progress.update(0, 0, "正在安装游戏组件", true); diff --git a/packages/app-lib/src/api/pack/hosted/control.rs b/packages/app-lib/src/api/pack/hosted/control.rs new file mode 100644 index 0000000..9768ba8 --- /dev/null +++ b/packages/app-lib/src/api/pack/hosted/control.rs @@ -0,0 +1,329 @@ +use futures::{Stream, StreamExt}; +use serde::{Deserialize, Serialize}; +use std::{ + future::Future, + sync::{Arc, LazyLock, Mutex}, +}; +use tokio::sync::watch; + +#[derive( + Clone, Copy, Debug, Default, Deserialize, Serialize, Hash, PartialEq, Eq, +)] +#[serde(rename_all = "snake_case")] +pub enum HostedTaskState { + #[default] + Running, + Paused, + Canceling, + Canceled, + Failed, + Finalizing, +} + +static CONTROLS: LazyLock>> = + LazyLock::new(dashmap::DashMap::new); + +pub(super) struct Control { + instance_id: String, + state: Mutex, + changes: watch::Sender, +} + +fn stopped() -> crate::Error { + super::invalid("已停止整合包下载;已下载的缓存会保留,可重试继续") +} + +impl Control { + fn new(instance_id: &str) -> Self { + Self { + instance_id: instance_id.to_owned(), + state: Mutex::new(HostedTaskState::Running), + changes: watch::channel(HostedTaskState::Running).0, + } + } + + fn state(&self) -> HostedTaskState { + *self.state.lock().unwrap_or_else(|error| error.into_inner()) + } + + fn transition( + &self, + allowed: &[HostedTaskState], + next: HostedTaskState, + ) -> crate::Result<()> { + let mut state = + self.state.lock().unwrap_or_else(|error| error.into_inner()); + if !allowed.contains(&state) { + return Err(super::invalid( + "任务状态已变化或正在应用安装文件,当前操作不可用", + )); + } + *state = next; + // Publish under the transition lock, so a late pause cannot overwrite finalization. + crate::event::emit::set_hosted_loading_state(&self.instance_id, next); + self.changes.send_replace(next); + Ok(()) + } + + fn pause(&self) -> crate::Result<()> { + self.transition(&[HostedTaskState::Running], HostedTaskState::Paused) + } + + fn resume(&self) -> crate::Result<()> { + self.transition(&[HostedTaskState::Paused], HostedTaskState::Running) + } + + fn cancel(&self) -> crate::Result<()> { + self.transition( + &[HostedTaskState::Running, HostedTaskState::Paused], + HostedTaskState::Canceling, + ) + } + + pub(super) fn begin_finalizing(&self) -> crate::Result<()> { + self.transition( + &[HostedTaskState::Running], + HostedTaskState::Finalizing, + ) + } + + /// The operation may be dropped only before begin_finalizing. Restarting + /// re-plans from verified cache objects and the downloader's partial files. + pub(super) async fn run( + &self, + mut operation: F, + ) -> crate::Result + where + F: FnMut() -> Fut, + Fut: Future>, + { + let mut changes = self.changes.subscribe(); + loop { + let state = *changes.borrow_and_update(); + match state { + HostedTaskState::Paused => { + changes.changed().await.map_err(|_| stopped())?; + continue; + } + HostedTaskState::Canceling => return Err(stopped()), + _ => {} + } + let work = operation(); + tokio::pin!(work); + loop { + tokio::select! { + biased; + changed = changes.changed() => { + changed.map_err(|_| stopped())?; + match self.state() { + HostedTaskState::Paused => break, + HostedTaskState::Canceling => return Err(stopped()), + _ => {}, + } + } + result = &mut work => { + match self.state() { + HostedTaskState::Paused => break, + HostedTaskState::Canceling => return Err(stopped()), + _ => return result, + } + } + } + } + } + } +} + +pub(super) struct Registration { + instance_id: String, + pub(super) control: Arc, +} + +impl Registration { + pub(super) fn new(instance_id: &str) -> crate::Result { + use dashmap::mapref::entry::Entry; + let control = Arc::new(Control::new(instance_id)); + match CONTROLS.entry(instance_id.to_owned()) { + Entry::Occupied(_) => { + return Err(super::invalid("此实例已有整合包下载任务")); + } + Entry::Vacant(entry) => { + entry.insert(control.clone()); + } + } + Ok(Self { + instance_id: instance_id.to_owned(), + control, + }) + } +} + +impl Drop for Registration { + fn drop(&mut self) { + CONTROLS.remove_if(&self.instance_id, |_, value| { + Arc::ptr_eq(value, &self.control) + }); + } +} + +fn lookup(instance_id: &str) -> crate::Result> { + CONTROLS + .get(instance_id) + .map(|entry| entry.value().clone()) + .ok_or_else(|| super::invalid("整合包下载任务已结束,请重试")) +} + +pub async fn pause(instance_id: &str) -> crate::Result<()> { + lookup(instance_id)?.pause() +} +pub async fn resume(instance_id: &str) -> crate::Result<()> { + lookup(instance_id)?.resume() +} +pub async fn cancel(instance_id: &str) -> crate::Result<()> { + lookup(instance_id)?.cancel() +} + +/// Stop the batch on its first error so stalled peers cannot hide failure. +pub(super) async fn collect_downloads(downloads: S) -> crate::Result +where + S: Stream>, +{ + tokio::pin!(downloads); + let mut total = 0; + while let Some(result) = downloads.next().await { + total += result?; + } + Ok(total) +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::FutureExt; + use std::sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }; + use tokio::sync::{Notify, Semaphore}; + + struct TransferGuard(Arc, Arc); + + impl Drop for TransferGuard { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + self.1.notify_one(); + } + } + + #[tokio::test] + async fn pause_drops_live_transfers_and_resume_restarts_before_stop() { + let control = Arc::new(Control::new("pause-resume-test")); + let starts = Arc::new(Semaphore::new(0)); + let drops = Arc::new(AtomicUsize::new(0)); + let dropped = Arc::new(Notify::new()); + let task = { + let control = control.clone(); + let starts = starts.clone(); + let drops = drops.clone(); + let dropped = dropped.clone(); + tokio::spawn(async move { + control + .run(|| { + let guard = + TransferGuard(drops.clone(), dropped.clone()); + starts.add_permits(1); + async move { + let _guard = guard; + std::future::pending::>().await + } + }) + .await + }) + }; + starts.acquire().await.unwrap().forget(); + control.pause().unwrap(); + dropped.notified().await; + assert_eq!(drops.load(Ordering::SeqCst), 1); + assert_eq!(control.state(), HostedTaskState::Paused); + control.resume().unwrap(); + starts.acquire().await.unwrap().forget(); + control.cancel().unwrap(); + let result = task.await.unwrap(); + assert!(result.unwrap_err().to_string().contains("停止")); + assert_eq!(drops.load(Ordering::SeqCst), 2); + } + + #[tokio::test] + async fn stopping_an_already_paused_task_never_starts_a_transfer() { + let control = Arc::new(Control::new("paused-stop-test")); + control.pause().unwrap(); + let task = { + let control = control.clone(); + tokio::spawn(async move { + control + .run(|| async { + panic!("paused work was polled"); + Ok::<(), crate::Error>(()) + }) + .await + }) + }; + control.cancel().unwrap(); + assert!(task.await.unwrap().is_err()); + } + + #[test] + fn finalization_rejects_pause_and_stop() { + let control = Control::new("finalization-test"); + control.begin_finalizing().unwrap(); + assert_eq!(control.state(), HostedTaskState::Finalizing); + assert!(control.pause().is_err()); + assert!(control.cancel().is_err()); + } + + #[test] + fn pause_and_finalization_have_only_one_winner() { + for _ in 0..32 { + let control = Arc::new(Control::new("finalization-race-test")); + let barrier = Arc::new(std::sync::Barrier::new(2)); + let task = { + let control = control.clone(); + let barrier = barrier.clone(); + std::thread::spawn(move || { + barrier.wait(); + control.pause().is_ok() + }) + }; + barrier.wait(); + let finalized = control.begin_finalizing().is_ok(); + assert_ne!(finalized, task.join().unwrap()); + } + } + + #[tokio::test] + async fn first_failure_drops_stalled_peer_downloads() { + let drops = Arc::new(AtomicUsize::new(0)); + let guard = TransferGuard(drops.clone(), Arc::new(Notify::new())); + let downloads = futures::stream::iter(vec![ + async move { + let _guard = guard; + std::future::pending::>().await + } + .boxed(), + async { Err(super::super::invalid("failed file")) }.boxed(), + ]) + .buffer_unordered(2); + let result = collect_downloads(downloads).await; + assert!(result.unwrap_err().to_string().contains("failed file")); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn registration_is_unique_and_released_on_drop() { + let id = uuid::Uuid::new_v4().to_string(); + let registration = Registration::new(&id).unwrap(); + assert!(Registration::new(&id).is_err()); + drop(registration); + assert!(Registration::new(&id).is_ok()); + } +} diff --git a/packages/app-lib/src/api/pack/hosted/progress.rs b/packages/app-lib/src/api/pack/hosted/progress.rs index a412be1..254aa67 100644 --- a/packages/app-lib/src/api/pack/hosted/progress.rs +++ b/packages/app-lib/src/api/pack/hosted/progress.rs @@ -59,6 +59,9 @@ impl PackProgress { instance_id: instance_id.to_owned(), instance_name: name.to_owned(), error: None, + state: super::HostedTaskState::Running, + can_pause: true, + can_cancel: true, }, 1.0, "正在获取整合包信息", diff --git a/packages/app-lib/src/api/pack/hosted/tagged.rs b/packages/app-lib/src/api/pack/hosted/tagged.rs index 8f9b8ba..6545f8a 100644 --- a/packages/app-lib/src/api/pack/hosted/tagged.rs +++ b/packages/app-lib/src/api/pack/hosted/tagged.rs @@ -681,7 +681,7 @@ pub(super) async fn download( set_loading(&bar, 0, file.size, "等待下载")?; work.push((file, url, object, bar)); } - let results = + control::collect_downloads( stream::iter(work.into_iter().map(|(file, url, object, bar)| { let current = ¤t; let state = &state; @@ -740,10 +740,7 @@ pub(super) async fn download( result } })) - .buffer_unordered(concurrency) - .collect::>() - .await; - results - .into_iter() - .try_fold(0, |sum, result| Ok(sum + result?)) + .buffer_unordered(concurrency), + ) + .await } diff --git a/packages/app-lib/src/event/emit.rs b/packages/app-lib/src/event/emit.rs index e2f0c5d..251ea73 100644 --- a/packages/app-lib/src/event/emit.rs +++ b/packages/app-lib/src/event/emit.rs @@ -232,8 +232,24 @@ pub fn fail_hosted_loading(key: &LoadingBarId, error: &str) { && let Some(mut bar) = state.loading_bars.get_mut(&key.0) { match &mut bar.bar_type { - LoadingBarType::HostedPackSync { error: failure, .. } - | LoadingBarType::HostedModDownload { error: failure, .. } => { + LoadingBarType::HostedPackSync { + error: failure, + state, + can_pause, + can_cancel, + .. + } => { + use crate::api::pack::hosted::HostedTaskState; + *state = if *state == HostedTaskState::Canceling { + HostedTaskState::Canceled + } else { + HostedTaskState::Failed + }; + *can_pause = false; + *can_cancel = false; + *failure = Some(error.to_owned()); + } + LoadingBarType::HostedModDownload { error: failure, .. } => { *failure = Some(error.to_owned()) } _ => {} @@ -241,6 +257,67 @@ pub fn fail_hosted_loading(key: &LoadingBarId, error: &str) { } } +/// Update the authoritative snapshot and notify observers even while all +/// transfer futures are paused and no byte-progress event can be produced. +pub fn set_hosted_loading_state( + instance_id: &str, + next: crate::api::pack::hosted::HostedTaskState, +) { + use crate::api::pack::hosted::HostedTaskState; + let Ok(event_state) = crate::EventState::get() else { + return; + }; + for mut bar in event_state.loading_bars.iter_mut() { + let LoadingBarType::HostedPackSync { + instance_id: id, + state, + can_pause, + can_cancel, + .. + } = &mut bar.bar_type + else { + continue; + }; + if id != instance_id { + continue; + } + *state = next; + let controllable = + matches!(next, HostedTaskState::Running | HostedTaskState::Paused); + *can_pause = controllable; + *can_cancel = controllable; + match next { + HostedTaskState::Paused => { + bar.message = "已暂停,点击继续以恢复下载".into() + } + HostedTaskState::Canceling => { + bar.message = "正在停止整合包下载".into() + } + HostedTaskState::Running => { + bar.message = "正在继续整合包下载".into() + } + _ => {} + } + #[cfg(feature = "tauri")] + { + let _ = event_state.app.emit( + "loading", + LoadingPayload { + fraction: Some(if bar.total > 0.0 { + bar.current / bar.total + } else { + 0.0 + }), + message: bar.message.clone(), + event: bar.bar_type.clone(), + loader_uuid: bar.loading_bar_uuid, + total: Some(bar.total), + }, + ); + } + } +} + // emit_warning(message) pub async fn emit_warning(message: &str) -> crate::Result<()> { #[cfg(feature = "tauri")] diff --git a/packages/app-lib/src/event/mod.rs b/packages/app-lib/src/event/mod.rs index 52e5e1c..037e664 100644 --- a/packages/app-lib/src/event/mod.rs +++ b/packages/app-lib/src/event/mod.rs @@ -142,6 +142,12 @@ pub enum LoadingBarType { instance_id: String, instance_name: String, error: Option, + #[serde(default)] + state: crate::api::pack::hosted::HostedTaskState, + #[serde(default)] + can_pause: bool, + #[serde(default)] + can_cancel: bool, }, HostedModDownload { instance_id: String, diff --git a/packages/app-lib/src/util/fetch.rs b/packages/app-lib/src/util/fetch.rs index 72df1ba..3363433 100644 --- a/packages/app-lib/src/util/fetch.rs +++ b/packages/app-lib/src/util/fetch.rs @@ -689,6 +689,46 @@ fn bounded_diagnostic_text(value: impl AsRef, max_chars: usize) -> String { value.as_ref().chars().take(max_chars).collect() } +fn sanitize_download_error_text(value: &str) -> String { + static URLS: LazyLock = LazyLock::new(|| { + regex::Regex::new(r"(?i)\b(?:https?|socks4a?|socks5h?)://[^\s]+") + .expect("download diagnostic URL pattern should be valid") + }); + let sanitized = URLS.replace_all(value, |matched: ®ex::Captures<'_>| { + let url = &matched[0]; + // A malformed URL must not fall back to logging its userinfo. The + // normal helper also strips signed query strings and fragments. + if Url::parse(url).is_ok() { + sanitize_url_for_log(url) + } else { + "[redacted URL]".to_string() + } + }); + bounded_diagnostic_text(sanitized, 256) +} + +fn download_transport_error_detail(source: &reqwest::Error) -> String { + // reqwest's Display stops before the useful DNS/TLS/socket cause and can + // include a signed request URL. Walk only its underlying causes instead. + let mut details = Vec::new(); + let mut cause = source.source(); + while let Some(error) = cause { + let detail = sanitize_download_error_text(&error.to_string()); + if details.last() != Some(&detail) { + details.push(detail); + } + if details.len() == 8 { + break; + } + cause = error.source(); + } + if details.is_empty() { + sanitize_download_error_text(&source.to_string()) + } else { + bounded_diagnostic_text(details.join(": "), 256) + } +} + fn download_error_category(error: &crate::Error) -> &'static str { match error.raw.as_ref() { ErrorKind::FetchError(source) => { @@ -750,7 +790,16 @@ fn download_error_category(error: &crate::Error) -> &'static str { fn download_error_detail(error: &crate::Error) -> String { match error.raw.as_ref() { ErrorKind::FetchError(source) => source.status().map_or_else( - || format!("{} failure", download_error_category(error)), + || { + bounded_diagnostic_text( + format!( + "{} failure: {}", + download_error_category(error), + download_transport_error_detail(source) + ), + 256, + ) + }, |status| format!("HTTP {}", status.as_u16()), ), ErrorKind::LabrinthError(error) => error.status.map_or_else( @@ -929,8 +978,10 @@ fn add_starlight_direct_recovery_route(routes: &mut Vec) { .filter(|route| { route.proxy == ProxyPolicy::System && Url::parse(&route.url).ok().is_some_and(|url| { - url.host_str() == Some("skin.starlight.cool") - && url.path().starts_with("/starlight/mod/packs/files/") + matches!( + url.host_str(), + Some("skin.starlight.cool" | "download.starlight.cool") + ) && url.path().starts_with("/starlight/mod/packs/files/") }) }) .cloned() @@ -6308,7 +6359,9 @@ async fn download_to_path_inner( source = route.source.as_str(), attempt = attempts, max_attempts = file_attempt_budget, - error = %error, + proxy = ?route.proxy, + category = download_error_category(&error), + error = %download_error_detail(&error), "File download request failed; trying the next source or retry" ); last_error = Some(error); @@ -8857,18 +8910,86 @@ mod tests { #[test] fn starlight_hosted_files_have_a_direct_recovery_route() { - let mut routes = vec![official_route( + for url in [ "https://skin.starlight.cool/starlight/mod/packs/files/release/hash", - ResourceClass::Modpack, - )]; + "https://download.starlight.cool:20443/starlight/mod/packs/files/4/hash", + ] { + let mut routes = vec![official_route(url, ResourceClass::Modpack)]; + add_starlight_direct_recovery_route(&mut routes); + add_starlight_direct_recovery_route(&mut routes); - add_starlight_direct_recovery_route(&mut routes); + assert_eq!(routes.len(), 2, "{url}"); + assert_eq!(routes[0].proxy, ProxyPolicy::System); + assert_eq!(routes[1].proxy, ProxyPolicy::Direct); + assert_eq!(routes[0].url, routes[1].url); + assert!(routes[1].allow_sensitive_headers); + } + } - assert_eq!(routes.len(), 2); - assert_eq!(routes[0].proxy, ProxyPolicy::System); - assert_eq!(routes[1].proxy, ProxyPolicy::Direct); - assert_eq!(routes[0].url, routes[1].url); - assert!(routes[1].allow_sensitive_headers); + #[test] + fn starlight_direct_recovery_excludes_other_paths_and_lookalike_hosts() { + for url in [ + "https://download.starlight.cool:20443/starlight/mod/packs", + "https://download.starlight.cool.evil.example/starlight/mod/packs/files/4/hash", + "https://evil.example/starlight/mod/packs/files/4/hash", + "https://skin.starlight.cool/yggdrasil", + ] { + let mut routes = vec![official_route(url, ResourceClass::Modpack)]; + add_starlight_direct_recovery_route(&mut routes); + assert_eq!(routes.len(), 1, "{url}"); + } + } + + #[tokio::test] + async fn download_transport_diagnostic_preserves_causes_without_url_secrets() + { + let listener = + tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + drop(listener); + let url = format!( + "http://username:password@{address}/file?token=secret#fragment" + ); + let error: crate::Error = reqwest::Client::builder() + .no_proxy() + .build() + .unwrap() + .get(url) + .send() + .await + .unwrap_err() + .into(); + let detail = download_error_detail(&error); + assert_eq!(download_error_category(&error), "connect"); + assert!(detail.contains("connect"), "{detail}"); + assert!( + detail.contains("tcp") || detail.contains("os error"), + "{detail}" + ); + for secret in ["username", "password", "token", "secret", "fragment"] { + assert!(!detail.contains(secret), "{detail}"); + } + } + + #[test] + fn download_transport_diagnostic_scrubs_embedded_urls() { + let detail = sanitize_download_error_text( + "request https://username:password@example.com/file?token=secret#fragment failed: connecting to http://proxyuser:proxypass@proxy.example:8080/?key=proxysecret: connection refused", + ); + assert!(detail.contains("https://example.com/file"), "{detail}"); + assert!(detail.contains("http://proxy.example:8080/"), "{detail}"); + assert!(detail.contains("connection refused"), "{detail}"); + for secret in [ + "username", + "password", + "token", + "secret", + "fragment", + "proxyuser", + "proxypass", + ] { + assert!(!detail.contains(secret), "{detail}"); + } } #[test]