From b16c50b03a0ef877c18a8c3d8b71655cb6c4cc1a Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 6 Jul 2026 19:13:25 +0800 Subject: [PATCH] feat: add backend i18n response support --- app/api/endpoints/agent.py | 40 +- app/api/endpoints/search.py | 24 +- app/api/endpoints/system.py | 27 +- app/factory.py | 45 +- app/helper/locale.py | 280 ++++++++ app/helper/progress.py | 35 +- app/locales/en-US.json | 1303 +++++++++++++++++++++++++++++++++++ app/locales/zh-CN.json | 186 +++++ app/locales/zh-TW.json | 1303 +++++++++++++++++++++++++++++++++++ app/schemas/dashboard.py | 56 +- app/schemas/response.py | 19 +- docs/mcp-api.md | 6 + tests/test_locale_helper.py | 366 ++++++++++ tests/test_system_i18n.py | 111 +++ 14 files changed, 3776 insertions(+), 25 deletions(-) create mode 100644 app/helper/locale.py create mode 100644 app/locales/en-US.json create mode 100644 app/locales/zh-CN.json create mode 100644 app/locales/zh-TW.json create mode 100644 tests/test_locale_helper.py create mode 100644 tests/test_system_i18n.py diff --git a/app/api/endpoints/agent.py b/app/api/endpoints/agent.py index 4afe8f945..74d2d328d 100644 --- a/app/api/endpoints/agent.py +++ b/app/api/endpoints/agent.py @@ -34,6 +34,7 @@ from app.db.models.agentchat import AgentChat from app.db.user_oper import UserOper, get_current_active_user from app.helper.agent import attach_web_agent_edit_queue, detach_web_agent_edit_queue from app.helper.interaction import agent_interaction_manager, media_interaction_manager +from app.helper.locale import LocaleHelper from app.log import logger from app.schemas.types import EventType, MessageChannel @@ -326,15 +327,25 @@ def _save_web_agent_display_snapshot( logger.debug(f"保存WebAgent展示历史失败: {e}") -def _build_web_agent_sse(event_type: str, data: Optional[dict] = None) -> str: +def _build_web_agent_sse( + event_type: str, + data: Optional[dict] = None, + locale: Optional[str] = None, +) -> str: """ 构建 Web Agent SSE 消息。 :param event_type: 前端事件类型 :param data: 事件数据 + :param locale: 当前请求语言 :return: 符合 SSE 格式的字符串 """ payload = {"type": event_type, **(data or {})} + message = payload.get("message") + if event_type == "error" and isinstance(message, str): + payload["message_i18n"] = LocaleHelper.translate_text( + message, locale=locale + ) return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n" @@ -1597,6 +1608,7 @@ async def web_agent_stream( :return: SSE 流式响应 """ prompt = payload.text.strip() + locale = LocaleHelper.get_locale_from_request(request) display_prompt = (payload.display_text or payload.text).strip() is_traditional_message = ( _is_web_agent_traditional_message(prompt) @@ -1610,6 +1622,7 @@ async def web_agent_stream( _build_web_agent_sse( "error", {"message": denied_message}, + locale=locale, ) ]), media_type="text/event-stream", @@ -1621,6 +1634,7 @@ async def web_agent_stream( _build_web_agent_sse( "error", {"message": unknown_command_message}, + locale=locale, ) ]), media_type="text/event-stream", @@ -1649,7 +1663,11 @@ async def web_agent_stream( """ 生成传统消息链路的 WebAgent SSE 事件。 """ - yield _build_web_agent_sse("start", {"session_id": session_id}) + yield _build_web_agent_sse( + "start", + {"session_id": session_id}, + locale=locale, + ) events = await _collect_web_agent_traditional_events( text=prompt, current_user=current_user, @@ -1660,7 +1678,11 @@ async def web_agent_stream( display_messages.append(assistant_message) for event in events: event_payload = copy.deepcopy(event) - yield _build_web_agent_sse(event_payload.pop("type"), event_payload) + yield _build_web_agent_sse( + event_payload.pop("type"), + event_payload, + locale=locale, + ) if await request.is_disconnected(): break await run_in_threadpool( @@ -1670,7 +1692,7 @@ async def web_agent_stream( messages=display_messages, client_session_id=payload.session_id or session_id, ) - yield _build_web_agent_sse("done", {}) + yield _build_web_agent_sse("done", {}, locale=locale) return StreamingResponse( traditional_event_generator(), @@ -1688,6 +1710,7 @@ async def web_agent_stream( _build_web_agent_sse( "error", {"message": "智能助手未启用,请先在系统设置中开启。"}, + locale=locale, ) ]), media_type="text/event-stream", @@ -1703,6 +1726,7 @@ async def web_agent_stream( _build_web_agent_sse( "error", {"message": "语音识别失败,请稍后重试。"}, + locale=locale, ) ]), media_type="text/event-stream", @@ -1713,6 +1737,7 @@ async def web_agent_stream( _build_web_agent_sse( "error", {"message": "请输入要发送给智能助手的内容或选择附件。"}, + locale=locale, ) ]), media_type="text/event-stream", @@ -1825,6 +1850,7 @@ async def web_agent_stream( yield _build_web_agent_sse( "start", {"session_id": session_id}, + locale=locale, ) disconnected = False while not global_vars.is_system_stopped: @@ -1832,7 +1858,11 @@ async def web_agent_stream( disconnected = True break event = await event_queue.get() - yield _build_web_agent_sse(event.pop("type"), event) + yield _build_web_agent_sse( + event.pop("type"), + event, + locale=locale, + ) if task.done() and event_queue.empty(): break except asyncio.CancelledError: diff --git a/app/api/endpoints/search.py b/app/api/endpoints/search.py index dff33965c..5199311a0 100644 --- a/app/api/endpoints/search.py +++ b/app/api/endpoints/search.py @@ -12,6 +12,7 @@ from app.core.config import settings from app.core.event import eventmanager from app.core.metainfo import MetaInfo from app.core.security import verify_resource_token, verify_token +from app.helper.locale import LocaleHelper from app.log import logger from app.schemas import MediaRecognizeConvertEventData from app.schemas.types import MediaType, ChainEventType @@ -39,11 +40,22 @@ def _parse_media_type(mtype: Optional[str]) -> Optional[MediaType]: return MediaType.from_agent(mtype) or MediaType(mtype) -def _sse_event(data: dict) -> str: +def _sse_event(data: dict, locale: Optional[str] = None) -> str: """ 转换为SSE事件 """ - return f"data: {json.dumps(data, ensure_ascii=False)}\n\n" + payload = data + message = payload.get("message") + text = payload.get("text") + if isinstance(message, str) or isinstance(text, str): + payload = data.copy() + if isinstance(message, str): + payload["message_i18n"] = LocaleHelper.translate_text( + message, locale=locale + ) + if isinstance(text, str): + payload["text_i18n"] = LocaleHelper.translate_text(text, locale=locale) + return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n" def _serialize_signed_subtitle_result(subtitle: Any) -> dict: @@ -167,6 +179,7 @@ async def _stream_search_events(request: Request, event_source: AsyncIterator[di """ 输出搜索SSE事件 """ + locale = LocaleHelper.get_locale_from_request(request) try: has_sent_final_replace = False async for event in _iter_batched_search_events(event_source): @@ -182,10 +195,13 @@ async def _stream_search_events(request: Request, event_source: AsyncIterator[di and event.get("items") ): event = {key: value for key, value in event.items() if key != "items"} - yield _sse_event(event) + yield _sse_event(event, locale=locale) except Exception as err: logger.error(f"渐进式搜索出错:{err}", exc_info=True) - yield _sse_event({"type": "error", "success": False, "message": str(err)}) + yield _sse_event( + {"type": "error", "success": False, "message": str(err)}, + locale=locale, + ) @router.get("/last", summary="查询搜索结果", response_model=List[schemas.Context]) diff --git a/app/api/endpoints/system.py b/app/api/endpoints/system.py index cc18678b5..bc1a57b4c 100644 --- a/app/api/endpoints/system.py +++ b/app/api/endpoints/system.py @@ -35,10 +35,11 @@ from app.db.user_oper import ( get_current_active_user_async, ) from app.helper.image import ImageHelper +from app.helper.locale import LocaleHelper from app.helper.message import MessageHelper -from app.helper.server import MoviePilotServerHelper from app.helper.progress import ProgressHelper from app.helper.rule import RuleHelper +from app.helper.server import MoviePilotServerHelper from app.helper.system import SystemHelper from app.log import logger from app.scheduler import Scheduler @@ -797,13 +798,14 @@ async def get_progress( 实时获取处理进度,返回格式为SSE """ progress = ProgressHelper(process_type) + locale = LocaleHelper.get_current_locale() async def event_generator(): try: while not global_vars.is_system_stopped: if await request.is_disconnected(): break - detail = progress.get() + detail = progress.get(locale=locale) yield f"data: {json.dumps(detail)}\n\n" await asyncio.sleep(0.5) except asyncio.CancelledError: @@ -1271,13 +1273,20 @@ def modulelist(_: schemas.TokenPayload = Depends(verify_token)): """ 查询已加载的模块ID列表 """ - modules = [ - { - "id": k, - "name": v.get_name(), - } - for k, v in ModuleManager().get_modules().items() - ] + modules = [] + for module_id, module in ModuleManager().get_modules().items(): + name = module.get_name() + modules.append( + { + "id": module_id, + "name": name, + "name_i18n": LocaleHelper.translate( + f"system.modules.{module_id}.name", + default=name, + ), + "name_key": f"system.modules.{module_id}.name", + } + ) return schemas.Response(success=True, data={"modules": modules}) diff --git a/app/factory.py b/app/factory.py index c4c43c1c9..403f29de0 100644 --- a/app/factory.py +++ b/app/factory.py @@ -1,10 +1,35 @@ -from fastapi import FastAPI +from typing import Awaitable, Callable + +from fastapi import FastAPI, HTTPException, Request, Response from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse from app.core.config import settings +from app.helper.locale import LocaleHelper from app.startup.lifecycle import lifespan +async def localized_http_exception_handler( + _request: Request, + exc: HTTPException, +) -> JSONResponse: + """ + 为 HTTPException 响应补充多语言错误详情。 + + :param _request: 当前 HTTP 请求 + :param exc: FastAPI HTTP 异常 + :return: 带 detail_i18n 的 JSON 错误响应 + """ + content = {"detail": exc.detail} + if isinstance(exc.detail, str): + content["detail_i18n"] = LocaleHelper.translate_text(exc.detail) + return JSONResponse( + status_code=exc.status_code, + content=content, + headers=exc.headers, + ) + + def create_app() -> FastAPI: """ 创建并配置 FastAPI 应用实例。 @@ -15,6 +40,8 @@ def create_app() -> FastAPI: lifespan=lifespan ) + _app.add_exception_handler(HTTPException, localized_http_exception_handler) + # 配置 CORS 中间件 _app.add_middleware( CORSMiddleware, # noqa @@ -24,6 +51,22 @@ def create_app() -> FastAPI: allow_headers=["*"], ) + @_app.middleware("http") + async def locale_context_middleware( + request: Request, + call_next: Callable[[Request], Awaitable[Response]], + ) -> Response: + """ + 为每个请求设置后端多语言上下文。 + """ + token = LocaleHelper.set_current_locale( + LocaleHelper.get_locale_from_request(request) + ) + try: + return await call_next(request) + finally: + LocaleHelper.reset_current_locale(token) + return _app diff --git a/app/helper/locale.py b/app/helper/locale.py new file mode 100644 index 000000000..d7e0fd094 --- /dev/null +++ b/app/helper/locale.py @@ -0,0 +1,280 @@ +import json +import re +from contextvars import ContextVar, Token +from functools import lru_cache +from pathlib import Path +from typing import Any, Optional + + +class LocaleHelper: + """ + 后端多语言文本辅助器。 + + 该类只为需要返回给前端展示的文本生成并行多语言字段,旧有中文字段仍由调用方保留。 + """ + + DEFAULT_LOCALE = "zh-CN" + SUPPORTED_LOCALES = ("zh-CN", "zh-TW", "en-US") + HEADER_NAMES = ("x-moviepilot-locale", "x-locale") + _PATTERN_FIELD = re.compile(r"\{([A-Za-z_][A-Za-z0-9_]*)\}") + _CURRENT_LOCALE: ContextVar[str] = ContextVar("moviepilot_locale", default=DEFAULT_LOCALE) + _LOCALES_DIR = Path(__file__).resolve().parents[1] / "locales" + _LOCALE_ALIASES = { + "zh": "zh-CN", + "zh-cn": "zh-CN", + "zh-hans": "zh-CN", + "zh-hans-cn": "zh-CN", + "zh-tw": "zh-TW", + "zh-hant": "zh-TW", + "zh-hant-tw": "zh-TW", + "en": "en-US", + "en-us": "en-US", + } + + @classmethod + def normalize_locale(cls, locale: Optional[str]) -> str: + """ + 规范化语言标识,无法识别时返回默认简体中文。 + + :param locale: 原始语言标识,如 zh-CN、zh_CN、en-US + :return: 项目支持的语言标识 + """ + return cls._match_locale(locale) or cls.DEFAULT_LOCALE + + @classmethod + def get_locale_from_request(cls, request: Any) -> str: + """ + 从请求参数或请求头解析前端期望语言。 + + :param request: FastAPI Request 或带 headers 属性的兼容对象 + :return: 项目支持的语言标识 + """ + query_params = getattr(request, "query_params", {}) or {} + query_locale = query_params.get("locale") if hasattr(query_params, "get") else None + if query_locale: + return cls.normalize_locale(query_locale) + + headers = getattr(request, "headers", {}) or {} + for header_name in cls.HEADER_NAMES: + value = headers.get(header_name) + if value: + return cls.normalize_locale(value) + + accept_language = headers.get("accept-language") + if not accept_language: + return cls.DEFAULT_LOCALE + + choices = [] + for index, item in enumerate(accept_language.split(",")): + parts = [part.strip() for part in item.split(";") if part.strip()] + if not parts: + continue + quality = 1.0 + for part in parts[1:]: + if part.startswith("q="): + try: + quality = float(part[2:]) + except ValueError: + quality = 0.0 + choices.append((-quality, index, parts[0])) + + for _, _, candidate in sorted(choices): + locale = cls._match_locale(candidate) + if locale: + return locale + return cls.DEFAULT_LOCALE + + @classmethod + def get_current_locale(cls) -> str: + """ + 获取当前请求上下文中的语言标识。 + + :return: 项目支持的语言标识 + """ + return cls._CURRENT_LOCALE.get() + + @classmethod + def set_current_locale(cls, locale: Optional[str]) -> Token[str]: + """ + 设置当前请求上下文中的语言标识。 + + :param locale: 原始语言标识 + :return: 用于恢复上下文的令牌 + """ + return cls._CURRENT_LOCALE.set(cls.normalize_locale(locale)) + + @classmethod + def reset_current_locale(cls, token: Token[str]) -> None: + """ + 恢复当前请求上下文中的语言标识。 + + :param token: set_current_locale 返回的上下文令牌 + """ + cls._CURRENT_LOCALE.reset(token) + + @classmethod + def translate( + cls, + key: str, + locale: Optional[str] = None, + default: Optional[str] = None, + **kwargs: Any, + ) -> str: + """ + 根据翻译键获取多语言文本。 + + :param key: 点分隔翻译键 + :param locale: 目标语言,未传入或无法识别时使用默认语言 + :param default: 翻译缺失时返回的默认文本 + :param kwargs: 字符串格式化参数 + :return: 翻译后的文本 + """ + normalized_locale = cls.normalize_locale(locale) if locale else cls.get_current_locale() + template = cls._lookup(cls._load_catalog(normalized_locale), key) + if template is None and normalized_locale != cls.DEFAULT_LOCALE: + template = cls._lookup(cls._load_catalog(cls.DEFAULT_LOCALE), key) + if template is None: + template = default or key + return cls._format(template, kwargs) + + @classmethod + def translate_text(cls, text: Optional[str], locale: Optional[str] = None) -> str: + """ + 翻译存量接口返回的中文文本。 + + :param text: 原始中文文本 + :param locale: 目标语言,未传入或无法识别时使用默认语言 + :return: 翻译后的文本,缺失翻译时返回原文 + """ + if not text: + return "" + normalized_locale = cls.normalize_locale(locale) if locale else cls.get_current_locale() + translated = cls._lookup_message(cls._load_catalog(normalized_locale), text) + if translated is None and cls._contains_chinese(text): + translated = cls._lookup_pattern(normalized_locale, text) + if translated is None and normalized_locale != cls.DEFAULT_LOCALE: + translated = cls._lookup_message(cls._load_catalog(cls.DEFAULT_LOCALE), text) + if ( + translated is None + and normalized_locale != cls.DEFAULT_LOCALE + and cls._contains_chinese(text) + ): + translated = cls._lookup_pattern(cls.DEFAULT_LOCALE, text) + return translated or text + + @classmethod + def _match_locale(cls, locale: Optional[str]) -> Optional[str]: + """ + 将原始语言标识匹配为项目支持的语言。 + """ + if not locale: + return None + normalized = locale.strip().replace("_", "-").lower() + if not normalized: + return None + return cls._LOCALE_ALIASES.get(normalized) + + @staticmethod + @lru_cache(maxsize=16) + def _load_catalog(locale: str) -> dict[str, Any]: + """ + 加载指定语言的翻译表。 + """ + catalog_path = LocaleHelper._LOCALES_DIR / f"{locale}.json" + try: + with catalog_path.open("r", encoding="utf-8") as file: + return json.load(file) + except (FileNotFoundError, json.JSONDecodeError): + return {} + + @staticmethod + def _lookup(catalog: dict[str, Any], key: str) -> Optional[str]: + """ + 按点分隔键从结构化翻译表中查找文本。 + """ + current: Any = catalog + for part in key.split("."): + if not isinstance(current, dict) or part not in current: + return None + current = current[part] + return current if isinstance(current, str) else None + + @staticmethod + def _lookup_message(catalog: dict[str, Any], text: str) -> Optional[str]: + """ + 从精确消息表中查找存量中文文本。 + """ + messages = catalog.get("messages") + if not isinstance(messages, dict): + return None + translated = messages.get(text) + return translated if isinstance(translated, str) else None + + @classmethod + def _lookup_pattern(cls, locale: str, text: str) -> Optional[str]: + """ + 使用动态模板匹配存量中文文本。 + """ + for pattern, target in cls._load_pattern_matchers(locale): + matched = pattern.fullmatch(text) + if matched: + return cls._format(target, matched.groupdict()) + return None + + @staticmethod + @lru_cache(maxsize=16) + def _load_pattern_matchers(locale: str) -> list[tuple[re.Pattern[str], str]]: + """ + 加载并缓存指定语言的动态文本匹配器。 + """ + catalog = LocaleHelper._load_catalog(locale) + patterns = catalog.get("message_patterns") + if not isinstance(patterns, list): + return [] + matchers = [] + for item in patterns: + if not isinstance(item, dict): + continue + source = item.get("source") + target = item.get("target") + if not isinstance(source, str) or not isinstance(target, str): + continue + pattern = LocaleHelper._compile_pattern(source) + if pattern is None: + continue + matchers.append((pattern, target)) + return matchers + + @classmethod + def _compile_pattern(cls, source: str) -> Optional[re.Pattern[str]]: + """ + 将带命名占位符的中文模板编译为正则。 + """ + field_names = cls._PATTERN_FIELD.findall(source) + if not field_names: + return None + + pattern = cls._PATTERN_FIELD.sub( + lambda match: f"(?P<{match.group(1)}>.+?)", + re.escape(source).replace(r"\{", "{").replace(r"\}", "}"), + ) + return re.compile(pattern) + + @staticmethod + def _contains_chinese(text: str) -> bool: + """ + 判断文本是否包含中文字符。 + """ + return any("\u4e00" <= char <= "\u9fff" for char in text) + + @staticmethod + def _format(template: str, kwargs: dict[str, Any]) -> str: + """ + 格式化翻译模板,参数缺失时保留模板原文。 + """ + if not kwargs: + return template + try: + return template.format(**kwargs) + except (KeyError, AttributeError, IndexError): + return template diff --git a/app/helper/progress.py b/app/helper/progress.py index ef4ffef1e..957facc06 100644 --- a/app/helper/progress.py +++ b/app/helper/progress.py @@ -1,7 +1,8 @@ from enum import Enum -from typing import Union, Optional +from typing import Optional, Union from app.core.cache import TTLCache +from app.helper.locale import LocaleHelper from app.schemas.types import ProgressKey @@ -82,8 +83,34 @@ class ProgressHelper: current['data'].update(data) self._progress[self._key] = current - def get(self) -> Optional[dict]: + def get(self, locale: Optional[str] = None) -> Optional[dict]: """ - 获取当前进度 + 获取当前进度,并按语言补充前端展示字段。 + + :param locale: 目标语言,未传入时使用当前请求上下文语言 + :return: 当前进度字典 """ - return self._progress.get(self._key) + current = self._progress.get(self._key) + if not current: + return current + + detail = current.copy() + text = detail.get("text") + if isinstance(text, str): + detail["text_i18n"] = LocaleHelper.translate_text(text, locale=locale) + + data = detail.get("data") + if isinstance(data, dict): + localized_data = data.copy() + error = localized_data.get("error") + message = localized_data.get("message") + if isinstance(error, str): + localized_data["error_i18n"] = LocaleHelper.translate_text( + error, locale=locale + ) + if isinstance(message, str): + localized_data["message_i18n"] = LocaleHelper.translate_text( + message, locale=locale + ) + detail["data"] = localized_data + return detail diff --git a/app/locales/en-US.json b/app/locales/en-US.json new file mode 100644 index 000000000..aabe3bec4 --- /dev/null +++ b/app/locales/en-US.json @@ -0,0 +1,1303 @@ +{ + "system": { + "modules": { + "BangumiModule": { + "name": "Bangumi" + }, + "DiscordModule": { + "name": "Discord" + }, + "DoubanModule": { + "name": "Douban" + }, + "EmbyModule": { + "name": "Emby" + }, + "FanartModule": { + "name": "Fanart" + }, + "FeishuModule": { + "name": "Feishu" + }, + "FileManagerModule": { + "name": "File Organization" + }, + "FilterModule": { + "name": "Filter" + }, + "IndexerModule": { + "name": "Site Indexer" + }, + "JellyfinModule": { + "name": "Jellyfin" + }, + "PlexModule": { + "name": "Plex" + }, + "PostgreSQLModule": { + "name": "PostgreSQL" + }, + "QbittorrentModule": { + "name": "qBittorrent" + }, + "QQBotModule": { + "name": "QQ" + }, + "RedisModule": { + "name": "Redis Cache" + }, + "RtorrentModule": { + "name": "rTorrent" + }, + "SlackModule": { + "name": "Slack" + }, + "SubtitleModule": { + "name": "Site Subtitle" + }, + "SynologyChatModule": { + "name": "Synology Chat" + }, + "TelegramModule": { + "name": "Telegram" + }, + "TheMovieDbModule": { + "name": "TheMovieDb" + }, + "TheTvDbModule": { + "name": "TheTvDb" + }, + "TransmissionModule": { + "name": "Transmission" + }, + "TrimeMediaModule": { + "name": "Trime Media" + }, + "UgreenModule": { + "name": "UGREEN" + }, + "VoceChatModule": { + "name": "VoceChat" + }, + "WebPushModule": { + "name": "WebPush" + }, + "WechatModule": { + "name": "WeCom" + }, + "WechatClawBotModule": { + "name": "WeChat ClawBot" + }, + "ZSpaceModule": { + "name": "Zspace" + } + }, + "module_test": { + "unsupported": "Module does not support testing" + } + }, + "messages": { + "模块不支持测试": "Module does not support testing", + "网络请求失败": "Network request failed", + "附件保存失败": "Failed to save attachment", + "该选择已失效,请重新发起选择": "This selection has expired. Please start the selection again", + "会话不存在或无权访问": "The conversation does not exist or you do not have access", + "会话保存失败": "Failed to save conversation", + "后台服务不存在": "Background service does not exist", + "任务添加失败": "Failed to add task", + "无法识别媒体信息": "Unable to recognize media information", + "未识别到媒体信息": "Unable to recognize media information", + "记录不存在": "Record does not exist", + "MoviePilot智能助手未启用": "MoviePilot Assistant is not enabled", + "整理记录不存在": "Organization record does not exist", + "未提供有效的整理记录": "No valid organization record was provided", + "请配置LLM提供商和模型": "Please configure the LLM provider and model", + "请先配置 LLM 模型": "Please configure the LLM model first", + "请先启用智能助手": "Please enable Assistant first", + "请先配置 LLM API Key": "Please configure the LLM API Key first", + "模型响应为空": "Model response is empty", + "LLM 调用超时": "LLM call timed out", + "刮削路径无效": "Scraping path is invalid", + "刮削失败,无法识别媒体信息": "Scraping failed: unable to recognize media information", + "刮削路径不存在": "Scraping path does not exist", + "保存成功": "Saved successfully", + "保存失败": "Failed to save", + "参数错误": "Invalid parameters", + "未配置媒体服务器": "Media server is not configured", + "未找到播放地址": "Playback URL not found", + "验证码错误": "Verification code is incorrect", + "您已注册通行密钥,为了防止域名配置变更导致无法登录,请先删除所有通行密钥再关闭 OTP 验证": "You have registered a passkey. To prevent login issues after domain configuration changes, delete all passkeys before disabling OTP verification", + "密码错误": "Incorrect password", + "为了确保在域名配置错误时仍能找回访问权限,请先启用 OTP 验证码再注册通行密钥": "To ensure access can be recovered when domain configuration is incorrect, enable OTP verification before registering a passkey", + "通行密钥注册成功": "Passkey registered successfully", + "认证失败": "Authentication failed", + "通行密钥已删除": "Passkey deleted", + "通行密钥不存在或无权删除": "The passkey does not exist or you do not have permission to delete it", + "验证失败": "Verification failed", + "通行密钥不存在或不属于当前用户": "The passkey does not exist or does not belong to the current user", + "通行密钥验证失败": "Passkey verification failed", + "二次验证成功": "Secondary verification succeeded", + "没有传入仓库地址,无法正确安装插件,请检查配置": "No repository URL was provided, so the plugin cannot be installed. Please check the configuration", + "插件分身创建成功": "Plugin clone created successfully", + "未识别到豆瓣媒体信息": "Unable to recognize Douban media information", + "未识别到TMDB媒体信息": "Unable to recognize TMDB media information", + "未知的媒体ID": "Unknown media ID", + "未搜索到任何资源": "No resources found", + "未搜索到任何字幕": "No subtitles found", + "没有可用的搜索结果": "No available search results", + "站点地址不能为空": "Site URL cannot be empty", + "用户未通过认证,无法使用站点功能!": "User authentication has not passed, so site features cannot be used.", + "该站点不支持,请检查站点域名是否正确": "This site is not supported. Please check whether the site domain is correct", + "站点不存在": "Site does not exist", + "CookieCloud同步任务已启动!": "CookieCloud sync task has started!", + "站点已重置!": "Site has been reset!", + "站点不支持索引或未通过用户认证!": "The site does not support indexing or user authentication has not passed!", + "站点图标不存在!": "Site icon does not exist!", + "请输入认证站点和认证参数": "Please enter the authentication site and authentication parameters", + "新名称为空": "New name is empty", + "订阅不存在": "Subscription does not exist", + "无效的订阅状态": "Invalid subscription status", + "不支持的通知类型": "Unsupported notification type", + "请求参数不正确": "Request parameters are incorrect", + "所有配置项更新成功": "All configuration items were updated successfully", + "不支持的 Wiki 同步地址": "Unsupported Wiki sync URL", + "无法访问 Wiki 插件仓库清单": "Unable to access the Wiki plugin repository list", + "未在 Wiki 中识别到插件仓库地址": "No plugin repository URL was found in the Wiki", + "未识别到媒体信息!": "Unable to recognize media information!", + "不符合过滤规则!": "Does not match the filter rules!", + "测试目标不存在": "Test target does not exist", + "测试目标发生了未授权跳转": "Test target performed an unauthorized redirect", + "测试目标重定向次数过多": "Test target redirected too many times", + "当前运行环境不支持重启操作!": "The current runtime environment does not support restart!", + "当前运行环境不支持升级操作!": "The current runtime environment does not support upgrade!", + "命令不能为空!": "Command cannot be empty!", + "未找到指定的种子": "Specified torrent not found", + "种子删除成功": "Torrent deleted successfully", + "种子缓存清理完成": "Torrent cache cleanup completed", + "重新识别完成": "Re-recognition completed", + "未识别到新名称": "Unable to recognize new name", + "缺少参数": "Missing parameters", + "用户已存在": "User already exists", + "密码需要同时包含字母、数字、特殊字符中的至少两项,且长度大于6位": "Password must contain at least two of letters, numbers, and special characters, and be longer than 6 characters", + "用户名不能为空": "Username cannot be empty", + "用户名已被使用": "Username is already in use", + "用户不存在": "User does not exist", + "已存在相同名称的工作流": "A workflow with the same name already exists", + "创建工作流成功": "Workflow created successfully", + "请填写工作流ID、分享标题和分享人": "Please fill in workflow ID, share title, and sharer", + "工作流名称不能为空": "Workflow name cannot be empty", + "actions字段JSON格式错误": "The actions field contains invalid JSON", + "flows字段JSON格式错误": "The flows field contains invalid JSON", + "context字段JSON格式错误": "The context field contains invalid JSON", + "event_conditions字段JSON格式错误": "The event_conditions field contains invalid JSON", + "复用成功": "Reused successfully", + "工作流不存在": "Workflow does not exist", + "定时工作流缺少定时器配置": "Scheduled workflow is missing timer configuration", + "工作流触发类型不支持": "Unsupported workflow trigger type", + "工作流ID不能为空": "Workflow ID cannot be empty", + "更新成功": "Updated successfully", + "删除成功": "Deleted successfully", + "豆瓣网络连接失败": "Douban network connection failed", + "Bangumi网络连接失败": "Bangumi network connection failed", + "fanart网络连接失败": "fanart network connection failed", + "未配置站点或未通过用户认证": "No sites are configured or user authentication has not passed", + "Redis连接失败,请检查配置": "Redis connection failed. Please check the configuration", + "无法打开网站!": "Unable to open the site!", + "无法获取Token": "Unable to get token", + "连接成功": "Connection successful", + "Cookie已失效": "Cookie has expired", + "鉴权已过期或无效": "Authentication has expired or is invalid", + "Cookie已过期": "Cookie has expired", + "APIKEY已过期": "API key has expired", + "无法通过Cloudflare!": "Unable to pass Cloudflare!", + "仿真登录失败,Cookie已失效!": "Simulated login failed. Cookie has expired!", + "系统正在停止,同步被中断": "The system is stopping, and sync was interrupted", + "未找到下载目录": "Download directory not found", + "下载字幕文件失败": "Failed to download subtitle file", + "字幕文件保存成功": "Subtitle file saved successfully", + "未保存任何字幕文件": "No subtitle files were saved", + "字幕下载链接为空": "Subtitle download URL is empty", + "下载字幕文件失败:未收到站点响应": "Failed to download subtitle file: no site response was received", + "字幕下载成功": "Subtitle downloaded successfully", + "下载被事件取消": "Download was cancelled by an event", + "下载种子内容为空": "Torrent content is empty", + "媒体信息识别失败": "Media information recognition failed", + "媒体信息中没有季集信息": "Media information does not contain season and episode details", + "文件整理模块运行失败": "File organization module failed to run", + "缺少目录参数": "Directory parameter is missing", + "目录不存在": "Directory does not exist", + "没有可用于识别的样本文件": "No sample files are available for recognition", + "当前选择不满足智能识别条件": "The current selection does not meet intelligent recognition requirements", + "工作流无动作": "Workflow has no actions", + "工作流无流程": "Workflow has no flows", + "工作流已停止": "Workflow has stopped", + "未设置任何目录": "No directories are configured", + "网络错误": "Network error", + "生成二维码失败": "Failed to generate QR code", + "无法连接到授权服务器": "Unable to connect to the authorization server", + "授权服务器返回数据不完整": "The authorization server returned incomplete data", + "获取授权URL失败": "Failed to get authorization URL", + "state为空": "State is empty", + "授权成功": "Authorization successful", + "授权已过期": "Authorization has expired", + "等待用户授权": "Waiting for user authorization", + "此存储不支持 OAuth2 授权": "This storage does not support OAuth2 authorization", + "未知错误": "Unknown error", + "下载内容为空": "Download content is empty", + "添加种子任务失败:无法读取种子文件": "Failed to add torrent task: unable to read torrent file", + "无法连接qbittorrent下载器": "Unable to connect to qBittorrent downloader", + "无法连接transmission下载器": "Unable to connect to Transmission downloader", + "无法连接rTorrent下载器": "Unable to connect to rTorrent downloader", + "下载任务已存在": "Download task already exists", + "获取种子文件失败,下载任务可能在暂停状态": "Failed to get torrent file. The download task may be paused", + "添加下载成功": "Download added successfully", + "添加下载任务成功": "Download task added successfully", + "消息客户端未就绪": "Message client is not ready", + "Github Token已失效,请检查配置": "GitHub token has expired. Please check the configuration", + "触发限流,请配置Github Token": "Rate limit triggered. Please configure a GitHub token", + "删除失败": "Deletion failed", + "已停止": "Stopped", + "当前没有正在执行的任务": "There is currently no running task", + "无效响应": "Invalid response", + "字幕站点信息为空": "Subtitle site information is empty", + "字幕下载链接签名无效": "Subtitle download URL signature is invalid", + "字幕站点信息不存在": "Subtitle site information does not exist", + "附件不存在或已过期": "Attachment does not exist or has expired", + "附件超过 32MB,无法发送给智能助手": "The attachment exceeds 32MB and cannot be sent to the assistant", + "附件保存失败": "Failed to save attachment", + "认证票据无效或已过期": "Authentication ticket is invalid or has expired", + "用户不存在或已禁用": "The user does not exist or has been disabled", + "用户权限不足": "Insufficient user permissions", + "用户名或密码错误": "Incorrect username or password", + "需要双重验证,请提供验证码或使用通行密钥": "Two-factor verification is required. Provide a verification code or use a passkey", + "图片读取出错": "Failed to read image", + "授权失败": "Authorization failed", + "报文内容为空": "Request payload is empty", + "配置项不存在": "Configuration item does not exist", + "智能助手未启用,请先在系统设置中开启。": "The assistant is not enabled. Enable it in system settings first.", + "语音识别失败,请稍后重试。": "Speech recognition failed. Please try again later.", + "请输入要发送给智能助手的内容或选择附件。": "Enter content to send to the assistant or choose an attachment.", + "微信 ClawBot 通知未启用或配置尚未保存,请先保存并启用当前渠道": "WeChat ClawBot notification is not enabled or the configuration has not been saved. Please save and enable this channel first", + "请输入至少一个有效的站点 ID": "Enter at least one valid site ID", + "所有订阅搜索完成": "All subscription searches are complete", + "请输入订阅 ID,多个 ID 用空格分隔,或输入 all": "Enter subscription IDs separated by spaces, or enter all", + "请输入至少一个有效的订阅 ID": "Enter at least one valid subscription ID", + "格式错误,请输入:cookie [2fa_code/secret]": "Invalid format. Enter: cookie [2fa_code/secret]", + "认证凭证无效": "Authentication credentials are invalid", + "不支持的认证类型": "Unsupported authentication type", + "CookieCloud参数不正确": "CookieCloud parameters are incorrect", + "未获取到cookie密文": "Cookie ciphertext was not obtained", + "cookie解密为空": "Cookie decryption result is empty", + "未从本地CookieCloud服务加载到cookie数据,请检查服务器设置、用户KEY及加密密码是否正确": "No cookie data was loaded from the local CookieCloud service. Please check the server settings, user key, and encryption password", + "CookieCloud请求失败,请检查服务器地址、用户KEY及加密密码是否正确": "CookieCloud request failed. Please check the server address, user key, and encryption password", + "目录中没有可用于识别的媒体文件": "No media files are available for recognition in the directory", + "无匹配自定义定位规则,智能生成失败": "No matching custom locating rule was found, and intelligent generation failed", + "样本命名与原生识别结果冲突,建议补充集数定位规则": "Sample naming conflicts with the native recognition result. Add an episode locating rule", + "有效正片样本覆盖率不足,建议补充集数定位规则": "Valid main-video sample coverage is insufficient. Add an episode locating rule", + "样本命名差异过大,建议补充集数定位规则": "Sample naming differences are too large. Add an episode locating rule", + "样本不足,仅基于单文件智能生成(仅供参考)": "Insufficient samples. Generated intelligently from a single file only (for reference)", + "已放宽体积限制智能生成模板(仅供参考)": "Generated template after relaxing the size limit (for reference)", + "已结合原生集数识别智能生成模板(仅供参考)": "Generated template using native episode recognition (for reference)", + "已根据多数派样本智能生成模板(仅供参考)": "Generated template from the majority of samples (for reference)", + "无匹配自定义定位规则,已智能生成(仅供参考)": "No matching custom locating rule was found. Generated intelligently (for reference)", + "样本未识别到有效集数,智能生成失败": "No valid episode number was recognized from the sample, and intelligent generation failed", + "不存在依赖": "No dependencies exist", + "主运行环境已恢复": "Main runtime environment has been restored", + "[PIP] 所有策略均安装依赖失败,请检查网络连接、PIP 配置或插件依赖约束": "[PIP] All dependency installation strategies failed. Please check the network connection, PIP configuration, or plugin dependency constraints", + "可执行文件模式下,只能安装本地插件": "In executable mode, only local plugins can be installed", + "不支持的插件仓库地址格式": "Unsupported plugin repository URL format", + "本地插件来源与插件ID不匹配": "Local plugin source does not match the plugin ID", + "本地插件来源不能与运行目录相同": "Local plugin source cannot be the same as the runtime directory", + "本地插件来源路径无效": "Local plugin source path is invalid", + "连接仓库失败": "Failed to connect to repository", + "文件列表为空": "File list is empty", + "requirements.txt 文件下载失败": "Failed to download requirements.txt", + "插件在仓库中不存在或返回数据格式不正确": "The plugin does not exist in the repository or the returned data format is invalid", + "插件数据解析失败": "Failed to parse plugin data", + "没有传入需要安装的依赖项": "No dependencies to install were provided", + "资产缺少ID信息": "Asset is missing ID information", + "压缩包内容为空": "Archive content is empty", + "压缩包中无可写入文件": "Archive contains no writable files", + "连接MoviePilot服务器失败": "Failed to connect to MoviePilot server", + "当前没有开启订阅数据共享功能": "Subscription data sharing is not enabled", + "当前没有开启工作流数据共享功能": "Workflow data sharing is not enabled", + "请分享有动作和流程的工作流": "Please share a workflow that contains actions and flows", + "升级模式仅支持 release 或 dev": "Upgrade mode only supports release or dev", + "当前实例不是由 moviepilot CLI 启动,无法执行内建重启!": "The current instance was not started by moviepilot CLI, so built-in restart cannot be performed!", + "已检测到自动升级模式 dev,正在重启并执行升级": "Auto-upgrade mode dev detected. Restarting and upgrading", + "已检测到自动升级已开启,正在重启并执行升级": "Auto-upgrade is enabled. Restarting and upgrading", + "获取容器ID失败!": "Failed to get container ID!", + "未登录,缺少 bot token": "Not logged in, missing bot token", + "连接正常": "Connection is normal", + "未登录,请先扫码完成绑定": "Not logged in. Scan the QR code to complete binding first", + "旧名称或新名称不能为空": "Old name and new name cannot be empty", + "通知名称未变化,无需迁移登录缓存": "Notification name has not changed. No login cache migration is needed", + "未找到可迁移的微信 ClawBot 登录缓存": "No WeChat ClawBot login cache available for migration was found", + "新名称下已存在登录缓存,跳过迁移": "Login cache already exists under the new name. Migration skipped", + "[系统]": "[System]", + "工作流": "Workflow", + "正在运行": "Running", + "等待": "Waiting", + "同步CookieCloud站点": "Sync CookieCloud Sites", + "同步媒体服务器": "Sync Media Servers", + "订阅元数据更新": "Update Subscription Metadata", + "订阅搜索补全": "Complete Subscription Search", + "新增订阅搜索": "New Subscription Search", + "订阅刷新": "Refresh Subscriptions", + "关注的订阅分享": "Followed Subscription Sharing", + "下载文件整理": "Organize Downloaded Files", + "缓存清理": "Cache Cleanup", + "数据表清理": "Data Table Cleanup", + "用户认证检查": "User Authentication Check", + "公共定时服务": "Common Scheduled Service", + "壁纸缓存": "Wallpaper Cache", + "站点数据刷新": "Refresh Site Data", + "推荐缓存": "Recommendation Cache", + "插件市场缓存": "Plugin Market Cache", + "订阅日历缓存": "Subscription Calendar Cache", + "主动内存回收": "Manual Memory Reclamation", + "智能体定时任务": "Agent Scheduled Task", + "安装版本统计上报": "Installation Version Statistics Report", + "开始清理数据表 ...": "Starting data table cleanup ...", + "站点资源刷新完成": "Site resource refresh completed", + "未配置媒体服务器,跳过同步": "No media server configured, skipping sync", + "没有已启用的媒体服务器": "No enabled media servers", + "媒体服务器同步完成": "Media server sync completed", + "站点数据刷新完成": "Site data refresh completed", + "开始下载 CookieCloud 数据 ...": "Starting CookieCloud data download ...", + "推荐数据刷新完成,正在缓存海报 ...": "Recommendation data refresh completed, caching posters ...", + "推荐缓存刷新完成": "Recommendation cache refresh completed", + "未配置下载器监控目录,跳过整理": "No downloader watch directory configured, skipping organization", + "正在查询已完成下载任务 ...": "Querying completed download tasks ...", + "没有已完成下载但未整理的任务": "No completed but unorganized download tasks", + "未开启任何有效站点,无法搜索资源": "No valid site is enabled, unable to search resources", + "未开启任何支持字幕搜索的有效站点,无法搜索字幕": "No valid subtitle-search site is enabled, unable to search subtitles", + "订阅搜索完成": "Subscription search completed", + "没有订阅需要刷新": "No subscriptions need refreshing", + "订阅刷新完成": "Subscription refresh completed", + "没有缓存资源,跳过订阅匹配": "No cached resources, skipping subscription matching", + "正在预处理订阅资源 ...": "Preprocessing subscription resources ...", + "订阅资源匹配完成": "Subscription resource matching completed", + "订阅元数据更新完成": "Subscription metadata update completed", + "未配置 Follow 订阅用户,跳过刷新": "No Follow subscription users configured, skipping refresh", + "订阅日历预缓存完成": "Subscription calendar precache completed" + }, + "message_patterns": [ + { + "source": "工具 '{tool}' 未找到", + "target": "Tool '{tool}' was not found" + }, + { + "source": "获取工具列表失败: {reason}", + "target": "Failed to get tool list: {reason}" + }, + { + "source": "获取工具信息失败: {reason}", + "target": "Failed to get tool information: {reason}" + }, + { + "source": "获取工具Schema失败: {reason}", + "target": "Failed to get tool schema: {reason}" + }, + { + "source": "插件 {plugin} 不存在或未安装", + "target": "Plugin {plugin} does not exist or is not installed" + }, + { + "source": "插件 {plugin} 不存在或未加载", + "target": "Plugin {plugin} does not exist or is not loaded" + }, + { + "source": "站点 {site} 不存在!", + "target": "Site {site} does not exist!" + }, + { + "source": "站点 {site} 不存在", + "target": "Site {site} does not exist" + }, + { + "source": "站点 {site} 不支持", + "target": "Site {site} is not supported" + }, + { + "source": "{path} 不是文件", + "target": "{path} is not a file" + }, + { + "source": "智能助手执行失败: {reason}", + "target": "Assistant execution failed: {reason}" + }, + { + "source": "无法连接Qbittorrent下载器:{name}", + "target": "Unable to connect to qBittorrent downloader: {name}" + }, + { + "source": "无法连接Transmission下载器:{name}", + "target": "Unable to connect to Transmission downloader: {name}" + }, + { + "source": "无法连接rTorrent下载器:{name}", + "target": "Unable to connect to rTorrent downloader: {name}" + }, + { + "source": "无法连接Emby服务器:{name}", + "target": "Unable to connect to Emby server: {name}" + }, + { + "source": "无法连接Jellyfin服务器:{name}", + "target": "Unable to connect to Jellyfin server: {name}" + }, + { + "source": "无法连接Plex服务器:{name}", + "target": "Unable to connect to Plex server: {name}" + }, + { + "source": "飞牛影视配置不完整:{name}", + "target": "Trime Media configuration is incomplete: {name}" + }, + { + "source": "无法连接飞牛影视:{name}", + "target": "Unable to connect to Trime Media: {name}" + }, + { + "source": "绿联影视配置不完整:{name}", + "target": "UGREEN configuration is incomplete: {name}" + }, + { + "source": "无法连接绿联影视:{name}", + "target": "Unable to connect to UGREEN: {name}" + }, + { + "source": "无法连接极影视服务器:{name}", + "target": "Unable to connect to Zspace server: {name}" + }, + { + "source": "Telegram {name} 未就绪", + "target": "Telegram {name} is not ready" + }, + { + "source": "飞书 {name} 未就绪", + "target": "Feishu {name} is not ready" + }, + { + "source": "Discord {name} Bot 未就绪", + "target": "Discord {name} Bot is not ready" + }, + { + "source": "Slack {name} 未就绪", + "target": "Slack {name} is not ready" + }, + { + "source": "QQ Bot {name} 未就绪", + "target": "QQ Bot {name} is not ready" + }, + { + "source": "VoceChat {name} 未就绪", + "target": "VoceChat {name} is not ready" + }, + { + "source": "企业微信 {name} 未就绪", + "target": "WeCom {name} is not ready" + }, + { + "source": "微信 ClawBot {name} 未就绪:{reason}", + "target": "WeChat ClawBot {name} is not ready: {reason}" + }, + { + "source": "Synology Chat {name} 未就绪", + "target": "Synology Chat {name} is not ready" + }, + { + "source": "无法连接Bangumi,错误码:{code}", + "target": "Unable to connect to Bangumi, error code: {code}" + }, + { + "source": "无法连接fanart,错误码:{code}", + "target": "Unable to connect to fanart, error code: {code}" + }, + { + "source": "无法连接 {domain},错误码:{code}", + "target": "Unable to connect to {domain}, error code: {code}" + }, + { + "source": "{domain} 网络连接失败", + "target": "{domain} network connection failed" + }, + { + "source": "{item} 删除失败", + "target": "{item} deletion failed" + }, + { + "source": "错误:{code} {reason}", + "target": "Error: {code} {reason}" + }, + { + "source": "错误:{code} {reason}!", + "target": "Error: {code} {reason}!" + }, + { + "source": "站点【{url}】不存在", + "target": "Site [{url}] does not exist" + }, + { + "source": "站点编号 {site_id} 不存在", + "target": "Site ID {site_id} does not exist" + }, + { + "source": "未找到站点:{names}", + "target": "Sites not found: {names}" + }, + { + "source": "【{name}】Cookie&UA 更新失败:{reason}", + "target": "[{name}] Cookie & UA update failed: {reason}" + }, + { + "source": "【{name}】Cookie&UA 更新成功", + "target": "[{name}] Cookie & UA updated successfully" + }, + { + "source": "下载目录获取失败,无法保存字幕:{path} - {reason}", + "target": "Failed to get download directory, unable to save subtitle: {path} - {reason}" + }, + { + "source": "下载目录不存在,无法保存字幕:{path}", + "target": "Download directory does not exist, unable to save subtitle: {path}" + }, + { + "source": "保存字幕文件失败:{path}", + "target": "Failed to save subtitle file: {path}" + }, + { + "source": "下载字幕文件失败,状态码:{code}", + "target": "Failed to download subtitle file, status code: {code}" + }, + { + "source": "下载字幕文件失败,状态码:{code} {reason}", + "target": "Failed to download subtitle file, status code: {code} {reason}" + }, + { + "source": "下载字幕文件失败,状态码:{code} {reason}:{detail}", + "target": "Failed to download subtitle file, status code: {code} {reason}: {detail}" + }, + { + "source": "下载链接不是支持的字幕文件:{name}", + "target": "Download URL is not a supported subtitle file: {name}" + }, + { + "source": "字幕压缩包解压失败:{reason}", + "target": "Failed to extract subtitle archive: {reason}" + }, + { + "source": "未获取到第 {season} 季的总集数", + "target": "Unable to get the total episode count for season {season}" + }, + { + "source": "所有订阅搜索完成", + "target": "All subscription searches are complete" + }, + { + "source": "请输入订阅 ID,多个 ID 用空格分隔,或输入 all", + "target": "Enter subscription IDs separated by spaces, or enter all" + }, + { + "source": "未找到订阅:{ids}", + "target": "Subscriptions not found: {ids}" + }, + { + "source": "已完成 {count} 个订阅搜索", + "target": "Completed searches for {count} subscriptions" + }, + { + "source": "请输入至少一个有效的订阅 ID", + "target": "Enter at least one valid subscription ID" + }, + { + "source": "已删除 {count} 个订阅", + "target": "Deleted {count} subscriptions" + }, + { + "source": "{name} 已在整理队列中", + "target": "{name} is already in the organization queue" + }, + { + "source": "{name} 已取消", + "target": "{name} has been cancelled" + }, + { + "source": "{name} 没有找到可整理的媒体文件", + "target": "{name}: no media files available for organization were found" + }, + { + "source": "{name} 已整理过", + "target": "{name} has already been organized" + }, + { + "source": "{name} 无法识别有效信息", + "target": "{name}: unable to recognize valid information" + }, + { + "source": "源目录不存在:{path}", + "target": "Source directory does not exist: {path}" + }, + { + "source": "未识别到媒体信息,类型:{type},id:{id}", + "target": "Unable to recognize media information, type: {type}, id: {id}" + }, + { + "source": "媒体信息识别失败,tmdbid:{tmdbid},doubanid:{doubanid},type: {type}", + "target": "Media information recognition failed, tmdbid: {tmdbid}, doubanid: {doubanid}, type: {type}" + }, + { + "source": "{name} 的下载目录未设置", + "target": "Download directory for {name} is not configured" + }, + { + "source": "{name} 的下载目录 {path} 不存在", + "target": "Download directory for {name} does not exist: {path}" + }, + { + "source": "{name} 的媒体库目录未设置", + "target": "Media library directory for {name} is not configured" + }, + { + "source": "{name} 的媒体库目录 {path} 不存在", + "target": "Media library directory for {name} does not exist: {path}" + }, + { + "source": "{name} 的下载目录 {download_path} 与媒体库目录 {library_path} 不在同一磁盘,无法硬链接", + "target": "Download directory {download_path} and media library directory {library_path} for {name} are not on the same disk, so hard links cannot be created" + }, + { + "source": "{name} 的存储测试不通过", + "target": "Storage test for {name} failed" + }, + { + "source": "{name} 的存储不支持 {transfer_type} 整理方式", + "target": "Storage for {name} does not support the {transfer_type} organization method" + }, + { + "source": "不支持 {storage} 的 OAuth2 授权", + "target": "OAuth2 authorization is not supported for {storage}" + }, + { + "source": "不支持 {source_storage} 到 {target_storage} 的文件整理", + "target": "File organization from {source_storage} to {target_storage} is not supported" + }, + { + "source": "不支持的整理方式:{transfer_type}", + "target": "Unsupported organization method: {transfer_type}" + }, + { + "source": "{path} {transfer_type} 失败", + "target": "{path} {transfer_type} failed" + }, + { + "source": "文件 {path} 不存在", + "target": "File {path} does not exist" + }, + { + "source": "{path} 上传 {storage} 失败", + "target": "Failed to upload {path} to {storage}" + }, + { + "source": "{path} {storage} 下载失败", + "target": "Failed to download {path} from {storage}" + }, + { + "source": "存储 {storage} 不支持 {transfer_type} 整理方式", + "target": "Storage {storage} does not support the {transfer_type} organization method" + }, + { + "source": "【{storage}】{path} 复制文件失败", + "target": "[{storage}] failed to copy file {path}" + }, + { + "source": "【{storage}】{path} 移动文件失败", + "target": "[{storage}] failed to move file {path}" + }, + { + "source": "【{storage}】{path} 创建硬链接失败", + "target": "[{storage}] failed to create hard link for {path}" + }, + { + "source": "【{storage}】{path} 目录获取失败", + "target": "[{storage}] failed to get directory {path}" + }, + { + "source": "获取目标目录失败:{path}", + "target": "Failed to get target directory: {path}" + }, + { + "source": "{path} 已存在", + "target": "{path} already exists" + }, + { + "source": "【{storage}】{path} 已存在", + "target": "[{storage}] {path} already exists" + }, + { + "source": "添加种子任务失败:{reason}", + "target": "Failed to add torrent task: {reason}" + }, + { + "source": "下载任务添加成功,但获取Qbittorrent任务信息失败:{reason}", + "target": "Download task was added, but failed to get qBittorrent task information: {reason}" + }, + { + "source": "添加下载成功,已选择集数:{episodes}", + "target": "Download added successfully, selected episodes: {episodes}" + }, + { + "source": "获取授权 URL 失败: {reason}", + "target": "Failed to get authorization URL: {reason}" + }, + { + "source": "检查授权状态失败: {reason}", + "target": "Failed to check authorization status: {reason}" + }, + { + "source": "{path} 刮削完成", + "target": "{path} scraping completed" + }, + { + "source": "生成注册选项失败: {reason}", + "target": "Failed to generate registration options: {reason}" + }, + { + "source": "注册失败: {reason}", + "target": "Registration failed: {reason}" + }, + { + "source": "获取列表失败: {reason}", + "target": "Failed to get list: {reason}" + }, + { + "source": "删除失败: {reason}", + "target": "Deletion failed: {reason}" + }, + { + "source": "文件夹 '{folder}' 创建成功", + "target": "Folder '{folder}' created successfully" + }, + { + "source": "文件夹 '{folder}' 已存在", + "target": "Folder '{folder}' already exists" + }, + { + "source": "文件夹 '{folder}' 删除成功", + "target": "Folder '{folder}' deleted successfully" + }, + { + "source": "文件夹 '{folder}' 不存在", + "target": "Folder '{folder}' does not exist" + }, + { + "source": "文件夹 '{folder}' 中的插件已更新", + "target": "Plugins in folder '{folder}' have been updated" + }, + { + "source": "创建插件分身失败:{reason}", + "target": "Failed to create plugin clone: {reason}" + }, + { + "source": "{domain} 站点己存在", + "target": "Site {domain} already exists" + }, + { + "source": "获取映射失败:{reason}", + "target": "Failed to get mapping: {reason}" + }, + { + "source": "{name} 未识别到媒体信息", + "target": "{name}: unable to recognize media information" + }, + { + "source": "{name} 未识别到新名称", + "target": "{name}: unable to recognize new name" + }, + { + "source": "{name} 重命名失败!", + "target": "{name} rename failed!" + }, + { + "source": "访问 Wiki 插件仓库清单失败,状态码:{code}", + "target": "Failed to access the Wiki plugin repository list, status code: {code}" + }, + { + "source": "配置项 '{key}' 不存在", + "target": "Configuration item '{key}' does not exist" + }, + { + "source": "过滤规则组 {name} 不存在!", + "target": "Filter rule group {name} does not exist!" + }, + { + "source": "{target}无法连接", + "target": "{target} cannot connect" + }, + { + "source": "站点 {domain} 缓存不存在", + "target": "Site {domain} cache does not exist" + }, + { + "source": "删除失败:{reason}", + "target": "Deletion failed: {reason}" + }, + { + "source": "清理失败:{reason}", + "target": "Cleanup failed: {reason}" + }, + { + "source": "缓存刷新完成,共刷新 {site_count} 个站点,{torrent_count} 个种子", + "target": "Cache refresh completed. Refreshed {site_count} sites and {torrent_count} torrents" + }, + { + "source": "刷新失败:{reason}", + "target": "Refresh failed: {reason}" + }, + { + "source": "重新识别失败:{reason}", + "target": "Re-recognition failed: {reason}" + }, + { + "source": "整理记录不存在,ID:{id}", + "target": "Organization record does not exist, ID: {id}" + }, + { + "source": "不支持的媒体类型:{type}", + "target": "Unsupported media type: {type}" + }, + { + "source": "整理记录不存在: {ids}", + "target": "Organization record does not exist: {ids}" + }, + { + "source": "未找到名为 {name} 的微信 ClawBot 通知配置", + "target": "No WeChat ClawBot notification configuration named {name} was found" + }, + { + "source": "下载字幕文件失败:{reason}", + "target": "Failed to download subtitle file: {reason}" + }, + { + "source": "更新了{updated}个站点,新增了{created}个站点", + "target": "Updated {updated} sites and added {created} sites" + }, + { + "source": "已{action} {count} 个站点", + "target": "Processed {count} sites: {action}" + }, + { + "source": "认证类型:{type},辅助认证事件失败或无效", + "target": "Authentication type: {type}. Auxiliary authentication event failed or is invalid" + }, + { + "source": "远程同步CookieCloud失败,错误码:{code}", + "target": "Remote CookieCloud sync failed, error code: {code}" + }, + { + "source": "未从{server}下载到cookie数据", + "target": "No cookie data was downloaded from {server}" + }, + { + "source": "从{server}下载cookie数据错误:{reason}", + "target": "Error downloading cookie data from {server}: {reason}" + }, + { + "source": "cookie解密失败:{reason}", + "target": "Cookie decryption failed: {reason}" + }, + { + "source": "插件限定的系统版本范围 {range} 必须是字符串,请使用 pip 依赖版本格式,例如 >=2.12.0,<3", + "target": "The plugin system version range {range} must be a string. Use pip dependency version format, for example >=2.12.0,<3" + }, + { + "source": "插件限定的系统版本范围格式不正确:{range},请使用 pip 依赖版本格式,例如 >=2.12.0,<3", + "target": "The plugin system version range format is invalid: {range}. Use pip dependency version format, for example >=2.12.0,<3" + }, + { + "source": "当前 MoviePilot 版本 {version} 无法解析,已拒绝安装带版本限制的插件", + "target": "Current MoviePilot version {version} cannot be parsed. Installation of the version-restricted plugin was rejected" + }, + { + "source": "插件要求 MoviePilot 版本 {range},当前版本 {version} 不满足,已拒绝安装", + "target": "The plugin requires MoviePilot version {range}, but current version {version} does not satisfy it. Installation was rejected" + }, + { + "source": "插件依赖与当前运行环境的{dependency}冲突:{reason}。为避免共享运行环境被污染,已拒绝安装。", + "target": "Plugin dependency conflicts with the current runtime {dependency}: {reason}. Installation was rejected to avoid contaminating the shared runtime." + }, + { + "source": "插件依赖安装失败后主运行环境异常,且恢复失败:{health}; {repair}", + "target": "Main runtime became abnormal after plugin dependency installation failed, and recovery failed: {health}; {repair}" + }, + { + "source": "插件依赖安装失败后主运行环境异常,恢复后仍异常:{reason}", + "target": "Main runtime became abnormal after plugin dependency installation failed and remains abnormal after recovery: {reason}" + }, + { + "source": "{check_name}失败:{message}", + "target": "{check_name} failed: {message}" + }, + { + "source": "恢复依赖文件不存在:{path}", + "target": "Dependency recovery file does not exist: {path}" + }, + { + "source": "恢复{desc}失败", + "target": "Failed to recover {desc}" + }, + { + "source": "[PIP] 所有策略均安装依赖失败:{reason}", + "target": "[PIP] All dependency installation strategies failed: {reason}" + }, + { + "source": "{pid} 未声明 Release 安装,无法安装指定版本", + "target": "{pid} does not declare Release installation, so the specified version cannot be installed" + }, + { + "source": "{pid} 未找到可安装的 Release 版本:{version}", + "target": "{pid}: no installable Release version found: {version}" + }, + { + "source": "未在插件清单中找到 {pid} 的版本号,无法进行 Release 安装", + "target": "No version number for {pid} was found in the plugin manifest, so Release installation cannot proceed" + }, + { + "source": "未找到本地插件:{pid}", + "target": "Local plugin not found: {pid}" + }, + { + "source": "复制本地插件失败:{reason}", + "target": "Failed to copy local plugin: {reason}" + }, + { + "source": "连接仓库失败:{code} - {reason}", + "target": "Failed to connect to repository: {code} - {reason}" + }, + { + "source": "文件 {path} 下载失败!", + "target": "File {path} download failed!" + }, + { + "source": "下载文件 {path} 失败:{code}", + "target": "Failed to download file {path}: {code}" + }, + { + "source": "下载 requirements.txt 文件失败:{code}", + "target": "Failed to download requirements.txt: {code}" + }, + { + "source": "插件依赖预检失败:{reason}", + "target": "Plugin dependency precheck failed: {reason}" + }, + { + "source": "获取 Release 信息失败:{reason}", + "target": "Failed to get Release information: {reason}" + }, + { + "source": "未找到资产文件:{asset}", + "target": "Asset file not found: {asset}" + }, + { + "source": "解析 Release 信息失败:{reason}", + "target": "Failed to parse Release information: {reason}" + }, + { + "source": "下载资产失败:{reason}", + "target": "Failed to download asset: {reason}" + }, + { + "source": "解压 Release 压缩包失败:{reason}", + "target": "Failed to extract Release archive: {reason}" + }, + { + "source": "安装依赖项时发生错误:{reason}", + "target": "An error occurred while installing dependencies: {reason}" + }, + { + "source": "创建运行环境约束文件失败:{reason}", + "target": "Failed to create runtime constraint file: {reason}" + }, + { + "source": "策略 {strategy} 安装依赖失败:{reason};{detail}", + "target": "Dependency installation failed with strategy {strategy}: {reason}; {detail}" + }, + { + "source": "依赖安装后运行环境自检失败,已自动恢复主程序依赖:{reason}", + "target": "Runtime self-check failed after dependency installation. Main program dependencies were automatically restored: {reason}" + }, + { + "source": "依赖安装后运行环境自检失败,恢复主程序依赖后仍异常:{reason}", + "target": "Runtime self-check failed after dependency installation and remains abnormal after restoring main program dependencies: {reason}" + }, + { + "source": "依赖安装后运行环境自检失败,且自动恢复主程序依赖失败:{reason}", + "target": "Runtime self-check failed after dependency installation, and automatic restoration of main program dependencies failed: {reason}" + }, + { + "source": "响应解析失败: {detail}...", + "target": "Response parsing failed: {detail}..." + }, + { + "source": "写入一次性升级标记失败:{reason}", + "target": "Failed to write one-shot upgrade marker: {reason}" + }, + { + "source": "本地 CLI 重启失败:{reason}", + "target": "Local CLI restart failed: {reason}" + }, + { + "source": "重启时发生错误:{reason}", + "target": "Error occurred during restart: {reason}" + }, + { + "source": "已安排一次性 {mode} 升级并重启", + "target": "Scheduled one-shot {mode} upgrade and restart" + }, + { + "source": "错误码:{code}", + "target": "Error code: {code}" + }, + { + "source": "PostgreSQL连接失败:{reason}", + "target": "PostgreSQL connection failed: {reason}" + }, + { + "source": "整理完成,{count} 个文件转移失败!", + "target": "Organization completed, but {count} files failed to transfer!" + }, + { + "source": "下载任务添加成功,但获取rTorrent任务信息失败:{reason}", + "target": "Download task was added, but failed to get rTorrent task information: {reason}" + }, + { + "source": "已将微信 ClawBot 登录缓存从 {old_name} 迁移到 {new_name}", + "target": "Migrated WeChat ClawBot login cache from {old_name} to {new_name}" + }, + { + "source": "数据表 {name} 跳过清理", + "target": "Data table {name} cleanup skipped" + }, + { + "source": "正在清理数据表 {name} ...", + "target": "Cleaning data table {name} ..." + }, + { + "source": "数据表 {name} 清理处理完成", + "target": "Data table {name} cleanup completed" + }, + { + "source": "开始刷新站点资源,共 {count} 个站点 ...", + "target": "Starting site resource refresh, {count} sites ..." + }, + { + "source": "开始同步媒体服务器,共 {count} 个 ...", + "target": "Starting media server sync, {count} servers ..." + }, + { + "source": "媒体服务器 {name} 无可同步媒体库", + "target": "Media server {name} has no libraries to sync" + }, + { + "source": "开始刷新站点数据,共 {count} 个站点 ...", + "target": "Starting site data refresh, {count} sites ..." + }, + { + "source": "正在刷新站点数据({index}/{total}){name} ...", + "target": "Refreshing site data ({index}/{total}) {name} ..." + }, + { + "source": "站点数据({index}/{total})刷新完成", + "target": "Site data ({index}/{total}) refresh completed" + }, + { + "source": "CookieCloud同步失败:{reason}", + "target": "CookieCloud sync failed: {reason}" + }, + { + "source": "正在同步 CookieCloud 站点({index}/{total}){domain} ...", + "target": "Syncing CookieCloud site ({index}/{total}) {domain} ..." + }, + { + "source": "CookieCloud 站点({index}/{total})同步完成", + "target": "CookieCloud site ({index}/{total}) sync completed" + }, + { + "source": "CookieCloud同步成功:{message}", + "target": "CookieCloud sync succeeded: {message}" + }, + { + "source": "开始刷新推荐缓存,共 {count} 个数据分页 ...", + "target": "Starting recommendation cache refresh, {count} data pages ..." + }, + { + "source": "正在缓存推荐海报({index}/{total})...", + "target": "Caching recommendation posters ({index}/{total}) ..." + }, + { + "source": "获取到 {count} 个已完成下载任务", + "target": "Found {count} completed download tasks" + }, + { + "source": "正在整理下载任务({index}/{total}){name} ...", + "target": "Organizing download task ({index}/{total}) {name} ..." + }, + { + "source": "下载任务({index}/{total})整理处理完成", + "target": "Download task ({index}/{total}) organization completed" + }, + { + "source": "搜索完成,共 {count} 个资源", + "target": "Search completed, {count} resources" + }, + { + "source": "搜索完成,共 {count} 个字幕", + "target": "Search completed, {count} subtitles" + }, + { + "source": "正在过滤匹配 {count} 个候选资源 ...", + "target": "Filtering and matching {count} candidate resources ..." + }, + { + "source": "过滤匹配完成,共 {count} 个资源", + "target": "Filtering and matching completed, {count} resources" + }, + { + "source": "正在识别匹配 {count} 个候选字幕 ...", + "target": "Recognizing and matching {count} candidate subtitles ..." + }, + { + "source": "识别匹配完成,共 {count} 个字幕", + "target": "Recognition matching completed, {count} subtitles" + }, + { + "source": "开始搜索字幕,共 {site_count} 个站点,{page_count} 页 ...", + "target": "Starting subtitle search across {site_count} sites and {page_count} pages ..." + }, + { + "source": "正在搜索字幕{keyword},已完成 {finished} / {total} 个请求 ...", + "target": "Searching subtitles {keyword}, completed {finished}/{total} requests ..." + }, + { + "source": "正在搜索字幕,已完成 {finished} / {total} 个请求 ...", + "target": "Searching subtitles, completed {finished}/{total} requests ..." + }, + { + "source": "站点字幕搜索完成,有效字幕数:{count},总耗时 {seconds} 秒", + "target": "Site subtitle search completed, {count} valid subtitles, took {seconds} seconds" + }, + { + "source": "开始搜索,共 {site_count} 个站点,{page_count} 页 ...", + "target": "Starting search across {site_count} sites and {page_count} pages ..." + }, + { + "source": "正在搜索{keyword},已完成 {finished} / {total} 个请求 ...", + "target": "Searching {keyword}, completed {finished}/{total} requests ..." + }, + { + "source": "正在搜索,已完成 {finished} / {total} 个请求 ...", + "target": "Searching, completed {finished}/{total} requests ..." + }, + { + "source": "站点搜索完成,有效资源数:{count},总耗时 {seconds} 秒", + "target": "Site search completed, {count} valid resources, took {seconds} seconds" + }, + { + "source": "开始订阅搜索,共 {count} 个订阅 ...", + "target": "Starting subscription search, {count} subscriptions ..." + }, + { + "source": "正在搜索订阅({index}/{total}){name} ...", + "target": "Searching subscription ({index}/{total}) {name} ..." + }, + { + "source": "订阅搜索随机休眠 {seconds} 秒后继续 ...", + "target": "Subscription search sleeping randomly for {seconds} seconds before continuing ..." + }, + { + "source": "订阅搜索({index}/{total})处理完成", + "target": "Subscription search ({index}/{total}) completed" + }, + { + "source": "开始刷新订阅,共 {count} 个订阅 ...", + "target": "Starting subscription refresh, {count} subscriptions ..." + }, + { + "source": "资源预处理完成,开始匹配 {count} 个订阅 ...", + "target": "Resource preprocessing completed, matching {count} subscriptions ..." + }, + { + "source": "开始更新订阅元数据,共 {count} 个订阅 ...", + "target": "Starting subscription metadata update, {count} subscriptions ..." + }, + { + "source": "正在更新订阅元数据({index}/{total}){name} ...", + "target": "Updating subscription metadata ({index}/{total}) {name} ..." + }, + { + "source": "订阅元数据({index}/{total})更新完成", + "target": "Subscription metadata ({index}/{total}) update completed" + }, + { + "source": "开始刷新 Follow 订阅分享,共 {count} 条 ...", + "target": "Starting Follow subscription sharing refresh, {count} items ..." + }, + { + "source": "正在处理 Follow 订阅分享({index}/{total})...", + "target": "Processing Follow subscription sharing ({index}/{total}) ..." + }, + { + "source": "Follow 订阅分享刷新完成,新增 {count} 个订阅", + "target": "Follow subscription sharing refresh completed, added {count} subscriptions" + }, + { + "source": "开始预缓存订阅日历,共 {count} 个订阅 ...", + "target": "Starting subscription calendar precache, {count} subscriptions ..." + }, + { + "source": "正在预缓存订阅日历({index}/{total}){name} ...", + "target": "Precaching subscription calendar ({index}/{total}) {name} ..." + }, + { + "source": "订阅日历({index}/{total})预缓存完成", + "target": "Subscription calendar ({index}/{total}) precache completed" + }, + { + "source": "{path} 不存在", + "target": "{path} does not exist" + }, + { + "source": "{days}天{hours}小时{minutes}分钟", + "target": "{days}d {hours}h {minutes}m" + }, + { + "source": "{days}天{hours}小时", + "target": "{days}d {hours}h" + }, + { + "source": "{days}天{minutes}分钟", + "target": "{days}d {minutes}m" + }, + { + "source": "{hours}小时{minutes}分钟", + "target": "{hours}h {minutes}m" + }, + { + "source": "{days}天", + "target": "{days}d" + }, + { + "source": "{hours}小时", + "target": "{hours}h" + }, + { + "source": "{minutes}分钟", + "target": "{minutes}m" + }, + { + "source": "{seconds}秒", + "target": "{seconds}s" + } + ] +} diff --git a/app/locales/zh-CN.json b/app/locales/zh-CN.json new file mode 100644 index 000000000..5976952ae --- /dev/null +++ b/app/locales/zh-CN.json @@ -0,0 +1,186 @@ +{ + "system": { + "modules": { + "BangumiModule": { + "name": "Bangumi" + }, + "DiscordModule": { + "name": "Discord" + }, + "DoubanModule": { + "name": "豆瓣" + }, + "EmbyModule": { + "name": "Emby" + }, + "FanartModule": { + "name": "Fanart" + }, + "FeishuModule": { + "name": "飞书" + }, + "FileManagerModule": { + "name": "文件整理" + }, + "FilterModule": { + "name": "过滤器" + }, + "IndexerModule": { + "name": "站点索引" + }, + "JellyfinModule": { + "name": "Jellyfin" + }, + "PlexModule": { + "name": "Plex" + }, + "PostgreSQLModule": { + "name": "PostgreSQL" + }, + "QbittorrentModule": { + "name": "Qbittorrent" + }, + "QQBotModule": { + "name": "QQ" + }, + "RedisModule": { + "name": "Redis缓存" + }, + "RtorrentModule": { + "name": "Rtorrent" + }, + "SlackModule": { + "name": "Slack" + }, + "SubtitleModule": { + "name": "站点字幕" + }, + "SynologyChatModule": { + "name": "Synology Chat" + }, + "TelegramModule": { + "name": "Telegram" + }, + "TheMovieDbModule": { + "name": "TheMovieDb" + }, + "TheTvDbModule": { + "name": "TheTvDb" + }, + "TransmissionModule": { + "name": "Transmission" + }, + "TrimeMediaModule": { + "name": "飞牛影视" + }, + "UgreenModule": { + "name": "绿联影视" + }, + "VoceChatModule": { + "name": "VoceChat" + }, + "WebPushModule": { + "name": "WebPush" + }, + "WechatModule": { + "name": "企业微信" + }, + "WechatClawBotModule": { + "name": "微信 ClawBot" + }, + "ZSpaceModule": { + "name": "极影视" + } + }, + "module_test": { + "unsupported": "模块不支持测试" + } + }, + "messages": { + "模块不支持测试": "模块不支持测试", + "网络请求失败": "网络请求失败", + "豆瓣网络连接失败": "豆瓣网络连接失败", + "Bangumi网络连接失败": "Bangumi网络连接失败", + "fanart网络连接失败": "fanart网络连接失败", + "未配置站点或未通过用户认证": "未配置站点或未通过用户认证", + "Redis连接失败,请检查配置": "Redis连接失败,请检查配置" + }, + "message_patterns": [ + { + "source": "无法连接Qbittorrent下载器:{name}", + "target": "无法连接Qbittorrent下载器:{name}" + }, + { + "source": "无法连接Transmission下载器:{name}", + "target": "无法连接Transmission下载器:{name}" + }, + { + "source": "无法连接rTorrent下载器:{name}", + "target": "无法连接rTorrent下载器:{name}" + }, + { + "source": "无法连接Emby服务器:{name}", + "target": "无法连接Emby服务器:{name}" + }, + { + "source": "无法连接Jellyfin服务器:{name}", + "target": "无法连接Jellyfin服务器:{name}" + }, + { + "source": "无法连接Plex服务器:{name}", + "target": "无法连接Plex服务器:{name}" + }, + { + "source": "飞牛影视配置不完整:{name}", + "target": "飞牛影视配置不完整:{name}" + }, + { + "source": "无法连接飞牛影视:{name}", + "target": "无法连接飞牛影视:{name}" + }, + { + "source": "绿联影视配置不完整:{name}", + "target": "绿联影视配置不完整:{name}" + }, + { + "source": "无法连接绿联影视:{name}", + "target": "无法连接绿联影视:{name}" + }, + { + "source": "无法连接极影视服务器:{name}", + "target": "无法连接极影视服务器:{name}" + }, + { + "source": "Telegram {name} 未就绪", + "target": "Telegram {name} 未就绪" + }, + { + "source": "飞书 {name} 未就绪", + "target": "飞书 {name} 未就绪" + }, + { + "source": "Discord {name} Bot 未就绪", + "target": "Discord {name} Bot 未就绪" + }, + { + "source": "Slack {name} 未就绪", + "target": "Slack {name} 未就绪" + }, + { + "source": "无法连接Bangumi,错误码:{code}", + "target": "无法连接Bangumi,错误码:{code}" + }, + { + "source": "无法连接fanart,错误码:{code}", + "target": "无法连接fanart,错误码:{code}" + }, + { + "source": "无法连接 {domain},错误码:{code}", + "target": "无法连接 {domain},错误码:{code}" + }, + { + "source": "{domain} 网络连接失败", + "target": "{domain} 网络连接失败" + } + ] +} diff --git a/app/locales/zh-TW.json b/app/locales/zh-TW.json new file mode 100644 index 000000000..a4fe6e3a2 --- /dev/null +++ b/app/locales/zh-TW.json @@ -0,0 +1,1303 @@ +{ + "system": { + "modules": { + "BangumiModule": { + "name": "Bangumi" + }, + "DiscordModule": { + "name": "Discord" + }, + "DoubanModule": { + "name": "豆瓣" + }, + "EmbyModule": { + "name": "Emby" + }, + "FanartModule": { + "name": "Fanart" + }, + "FeishuModule": { + "name": "飛書" + }, + "FileManagerModule": { + "name": "檔案整理" + }, + "FilterModule": { + "name": "過濾器" + }, + "IndexerModule": { + "name": "站點索引" + }, + "JellyfinModule": { + "name": "Jellyfin" + }, + "PlexModule": { + "name": "Plex" + }, + "PostgreSQLModule": { + "name": "PostgreSQL" + }, + "QbittorrentModule": { + "name": "Qbittorrent" + }, + "QQBotModule": { + "name": "QQ" + }, + "RedisModule": { + "name": "Redis 快取" + }, + "RtorrentModule": { + "name": "Rtorrent" + }, + "SlackModule": { + "name": "Slack" + }, + "SubtitleModule": { + "name": "站點字幕" + }, + "SynologyChatModule": { + "name": "Synology Chat" + }, + "TelegramModule": { + "name": "Telegram" + }, + "TheMovieDbModule": { + "name": "TheMovieDb" + }, + "TheTvDbModule": { + "name": "TheTvDb" + }, + "TransmissionModule": { + "name": "Transmission" + }, + "TrimeMediaModule": { + "name": "飛牛影視" + }, + "UgreenModule": { + "name": "綠聯影視" + }, + "VoceChatModule": { + "name": "VoceChat" + }, + "WebPushModule": { + "name": "WebPush" + }, + "WechatModule": { + "name": "企業微信" + }, + "WechatClawBotModule": { + "name": "微信 ClawBot" + }, + "ZSpaceModule": { + "name": "極影視" + } + }, + "module_test": { + "unsupported": "模組不支援測試" + } + }, + "messages": { + "模块不支持测试": "模組不支援測試", + "网络请求失败": "網路請求失敗", + "附件保存失败": "附件儲存失敗", + "该选择已失效,请重新发起选择": "此選擇已失效,請重新發起選擇", + "会话不存在或无权访问": "會話不存在或無權存取", + "会话保存失败": "會話儲存失敗", + "后台服务不存在": "背景服務不存在", + "任务添加失败": "任務新增失敗", + "无法识别媒体信息": "無法識別媒體資訊", + "未识别到媒体信息": "未識別到媒體資訊", + "记录不存在": "記錄不存在", + "MoviePilot智能助手未启用": "MoviePilot 智慧助手未啟用", + "整理记录不存在": "整理記錄不存在", + "未提供有效的整理记录": "未提供有效的整理記錄", + "请配置LLM提供商和模型": "請設定 LLM 提供商和模型", + "请先配置 LLM 模型": "請先設定 LLM 模型", + "请先启用智能助手": "請先啟用智慧助手", + "请先配置 LLM API Key": "請先設定 LLM API Key", + "模型响应为空": "模型回應為空", + "LLM 调用超时": "LLM 呼叫逾時", + "刮削路径无效": "刮削路徑無效", + "刮削失败,无法识别媒体信息": "刮削失敗,無法識別媒體資訊", + "刮削路径不存在": "刮削路徑不存在", + "保存成功": "儲存成功", + "保存失败": "儲存失敗", + "参数错误": "參數錯誤", + "未配置媒体服务器": "未設定媒體伺服器", + "未找到播放地址": "未找到播放位址", + "验证码错误": "驗證碼錯誤", + "您已注册通行密钥,为了防止域名配置变更导致无法登录,请先删除所有通行密钥再关闭 OTP 验证": "您已註冊通行密鑰,為避免網域設定變更導致無法登入,請先刪除所有通行密鑰再關閉 OTP 驗證", + "密码错误": "密碼錯誤", + "为了确保在域名配置错误时仍能找回访问权限,请先启用 OTP 验证码再注册通行密钥": "為了確保網域設定錯誤時仍可找回存取權限,請先啟用 OTP 驗證碼再註冊通行密鑰", + "通行密钥注册成功": "通行密鑰註冊成功", + "认证失败": "認證失敗", + "通行密钥已删除": "通行密鑰已刪除", + "通行密钥不存在或无权删除": "通行密鑰不存在或無權刪除", + "验证失败": "驗證失敗", + "通行密钥不存在或不属于当前用户": "通行密鑰不存在或不屬於目前使用者", + "通行密钥验证失败": "通行密鑰驗證失敗", + "二次验证成功": "二次驗證成功", + "没有传入仓库地址,无法正确安装插件,请检查配置": "未傳入倉庫位址,無法正確安裝插件,請檢查設定", + "插件分身创建成功": "插件分身建立成功", + "未识别到豆瓣媒体信息": "未識別到豆瓣媒體資訊", + "未识别到TMDB媒体信息": "未識別到 TMDB 媒體資訊", + "未知的媒体ID": "未知的媒體 ID", + "未搜索到任何资源": "未搜尋到任何資源", + "未搜索到任何字幕": "未搜尋到任何字幕", + "没有可用的搜索结果": "沒有可用的搜尋結果", + "站点地址不能为空": "站點位址不能為空", + "用户未通过认证,无法使用站点功能!": "使用者未通過認證,無法使用站點功能!", + "该站点不支持,请检查站点域名是否正确": "不支援此站點,請檢查站點網域是否正確", + "站点不存在": "站點不存在", + "CookieCloud同步任务已启动!": "CookieCloud 同步任務已啟動!", + "站点已重置!": "站點已重設!", + "站点不支持索引或未通过用户认证!": "站點不支援索引或未通過使用者認證!", + "站点图标不存在!": "站點圖示不存在!", + "请输入认证站点和认证参数": "請輸入認證站點和認證參數", + "新名称为空": "新名稱為空", + "订阅不存在": "訂閱不存在", + "无效的订阅状态": "無效的訂閱狀態", + "不支持的通知类型": "不支援的通知類型", + "请求参数不正确": "請求參數不正確", + "所有配置项更新成功": "所有設定項更新成功", + "不支持的 Wiki 同步地址": "不支援的 Wiki 同步位址", + "无法访问 Wiki 插件仓库清单": "無法存取 Wiki 插件倉庫清單", + "未在 Wiki 中识别到插件仓库地址": "未在 Wiki 中識別到插件倉庫位址", + "未识别到媒体信息!": "未識別到媒體資訊!", + "不符合过滤规则!": "不符合過濾規則!", + "测试目标不存在": "測試目標不存在", + "测试目标发生了未授权跳转": "測試目標發生未授權跳轉", + "测试目标重定向次数过多": "測試目標重新導向次數過多", + "当前运行环境不支持重启操作!": "目前執行環境不支援重啟操作!", + "当前运行环境不支持升级操作!": "目前執行環境不支援升級操作!", + "命令不能为空!": "命令不能為空!", + "未找到指定的种子": "未找到指定的種子", + "种子删除成功": "種子刪除成功", + "种子缓存清理完成": "種子快取清理完成", + "重新识别完成": "重新識別完成", + "未识别到新名称": "未識別到新名稱", + "缺少参数": "缺少參數", + "用户已存在": "使用者已存在", + "密码需要同时包含字母、数字、特殊字符中的至少两项,且长度大于6位": "密碼需同時包含字母、數字、特殊字元中的至少兩項,且長度大於 6 位", + "用户名不能为空": "使用者名稱不能為空", + "用户名已被使用": "使用者名稱已被使用", + "用户不存在": "使用者不存在", + "已存在相同名称的工作流": "已存在相同名稱的工作流", + "创建工作流成功": "建立工作流成功", + "请填写工作流ID、分享标题和分享人": "請填寫工作流 ID、分享標題和分享人", + "工作流名称不能为空": "工作流名稱不能為空", + "actions字段JSON格式错误": "actions 欄位 JSON 格式錯誤", + "flows字段JSON格式错误": "flows 欄位 JSON 格式錯誤", + "context字段JSON格式错误": "context 欄位 JSON 格式錯誤", + "event_conditions字段JSON格式错误": "event_conditions 欄位 JSON 格式錯誤", + "复用成功": "復用成功", + "工作流不存在": "工作流不存在", + "定时工作流缺少定时器配置": "定時工作流缺少定時器設定", + "工作流触发类型不支持": "不支援此工作流觸發類型", + "工作流ID不能为空": "工作流 ID 不能為空", + "更新成功": "更新成功", + "删除成功": "刪除成功", + "豆瓣网络连接失败": "豆瓣網路連線失敗", + "Bangumi网络连接失败": "Bangumi 網路連線失敗", + "fanart网络连接失败": "fanart 網路連線失敗", + "未配置站点或未通过用户认证": "未設定站點或未通過使用者認證", + "Redis连接失败,请检查配置": "Redis 連線失敗,請檢查設定", + "无法打开网站!": "無法開啟網站!", + "无法获取Token": "無法取得 Token", + "连接成功": "連線成功", + "Cookie已失效": "Cookie 已失效", + "鉴权已过期或无效": "鑑權已過期或無效", + "Cookie已过期": "Cookie 已過期", + "APIKEY已过期": "APIKEY 已過期", + "无法通过Cloudflare!": "無法通過 Cloudflare!", + "仿真登录失败,Cookie已失效!": "仿真登入失敗,Cookie 已失效!", + "系统正在停止,同步被中断": "系統正在停止,同步已中斷", + "未找到下载目录": "未找到下載目錄", + "下载字幕文件失败": "下載字幕檔案失敗", + "字幕文件保存成功": "字幕檔案儲存成功", + "未保存任何字幕文件": "未儲存任何字幕檔案", + "字幕下载链接为空": "字幕下載連結為空", + "下载字幕文件失败:未收到站点响应": "下載字幕檔案失敗:未收到站點回應", + "字幕下载成功": "字幕下載成功", + "下载被事件取消": "下載已被事件取消", + "下载种子内容为空": "下載種子內容為空", + "媒体信息识别失败": "媒體資訊識別失敗", + "媒体信息中没有季集信息": "媒體資訊中沒有季集資訊", + "文件整理模块运行失败": "檔案整理模組執行失敗", + "缺少目录参数": "缺少目錄參數", + "目录不存在": "目錄不存在", + "没有可用于识别的样本文件": "沒有可用於識別的樣本檔案", + "当前选择不满足智能识别条件": "目前選擇不滿足智慧識別條件", + "工作流无动作": "工作流沒有動作", + "工作流无流程": "工作流沒有流程", + "工作流已停止": "工作流已停止", + "未设置任何目录": "未設定任何目錄", + "网络错误": "網路錯誤", + "生成二维码失败": "產生 QR Code 失敗", + "无法连接到授权服务器": "無法連線到授權伺服器", + "授权服务器返回数据不完整": "授權伺服器返回資料不完整", + "获取授权URL失败": "取得授權 URL 失敗", + "state为空": "state 為空", + "授权成功": "授權成功", + "授权已过期": "授權已過期", + "等待用户授权": "等待使用者授權", + "此存储不支持 OAuth2 授权": "此儲存不支援 OAuth2 授權", + "未知错误": "未知錯誤", + "下载内容为空": "下載內容為空", + "添加种子任务失败:无法读取种子文件": "新增種子任務失敗:無法讀取種子檔案", + "无法连接qbittorrent下载器": "無法連線 qBittorrent 下載器", + "无法连接transmission下载器": "無法連線 Transmission 下載器", + "无法连接rTorrent下载器": "無法連線 rTorrent 下載器", + "下载任务已存在": "下載任務已存在", + "获取种子文件失败,下载任务可能在暂停状态": "取得種子檔案失敗,下載任務可能處於暫停狀態", + "添加下载成功": "新增下載成功", + "添加下载任务成功": "新增下載任務成功", + "消息客户端未就绪": "訊息用戶端尚未就緒", + "Github Token已失效,请检查配置": "GitHub Token 已失效,請檢查設定", + "触发限流,请配置Github Token": "已觸發限流,請設定 GitHub Token", + "删除失败": "刪除失敗", + "已停止": "已停止", + "当前没有正在执行的任务": "目前沒有正在執行的任務", + "无效响应": "無效回應", + "字幕站点信息为空": "字幕站點資訊為空", + "字幕下载链接签名无效": "字幕下載連結簽名無效", + "字幕站点信息不存在": "字幕站點資訊不存在", + "附件不存在或已过期": "附件不存在或已過期", + "附件超过 32MB,无法发送给智能助手": "附件超過 32MB,無法傳送給智慧助手", + "附件保存失败": "附件儲存失敗", + "认证票据无效或已过期": "認證票據無效或已過期", + "用户不存在或已禁用": "使用者不存在或已停用", + "用户权限不足": "使用者權限不足", + "用户名或密码错误": "使用者名稱或密碼錯誤", + "需要双重验证,请提供验证码或使用通行密钥": "需要雙重驗證,請提供驗證碼或使用通行密鑰", + "图片读取出错": "圖片讀取出錯", + "授权失败": "授權失敗", + "报文内容为空": "報文內容為空", + "配置项不存在": "設定項不存在", + "智能助手未启用,请先在系统设置中开启。": "智慧助手未啟用,請先在系統設定中開啟。", + "语音识别失败,请稍后重试。": "語音識別失敗,請稍後重試。", + "请输入要发送给智能助手的内容或选择附件。": "請輸入要傳送給智慧助手的內容或選擇附件。", + "微信 ClawBot 通知未启用或配置尚未保存,请先保存并启用当前渠道": "微信 ClawBot 通知未啟用或設定尚未儲存,請先儲存並啟用目前渠道", + "请输入至少一个有效的站点 ID": "請輸入至少一個有效的站點 ID", + "所有订阅搜索完成": "所有訂閱搜尋完成", + "请输入订阅 ID,多个 ID 用空格分隔,或输入 all": "請輸入訂閱 ID,多個 ID 以空格分隔,或輸入 all", + "请输入至少一个有效的订阅 ID": "請輸入至少一個有效的訂閱 ID", + "格式错误,请输入:cookie [2fa_code/secret]": "格式錯誤,請輸入:cookie [2fa_code/secret]", + "认证凭证无效": "認證憑證無效", + "不支持的认证类型": "不支援的認證類型", + "CookieCloud参数不正确": "CookieCloud 參數不正確", + "未获取到cookie密文": "未取得 cookie 密文", + "cookie解密为空": "cookie 解密為空", + "未从本地CookieCloud服务加载到cookie数据,请检查服务器设置、用户KEY及加密密码是否正确": "未從本機 CookieCloud 服務載入 cookie 資料,請檢查伺服器設定、使用者 KEY 及加密密碼是否正確", + "CookieCloud请求失败,请检查服务器地址、用户KEY及加密密码是否正确": "CookieCloud 請求失敗,請檢查伺服器位址、使用者 KEY 及加密密碼是否正確", + "目录中没有可用于识别的媒体文件": "目錄中沒有可用於識別的媒體檔案", + "无匹配自定义定位规则,智能生成失败": "無匹配自訂定位規則,智慧產生失敗", + "样本命名与原生识别结果冲突,建议补充集数定位规则": "樣本命名與原生識別結果衝突,建議補充集數定位規則", + "有效正片样本覆盖率不足,建议补充集数定位规则": "有效正片樣本覆蓋率不足,建議補充集數定位規則", + "样本命名差异过大,建议补充集数定位规则": "樣本命名差異過大,建議補充集數定位規則", + "样本不足,仅基于单文件智能生成(仅供参考)": "樣本不足,僅基於單檔案智慧產生(僅供參考)", + "已放宽体积限制智能生成模板(仅供参考)": "已放寬體積限制智慧產生模板(僅供參考)", + "已结合原生集数识别智能生成模板(仅供参考)": "已結合原生集數識別智慧產生模板(僅供參考)", + "已根据多数派样本智能生成模板(仅供参考)": "已根據多數派樣本智慧產生模板(僅供參考)", + "无匹配自定义定位规则,已智能生成(仅供参考)": "無匹配自訂定位規則,已智慧產生(僅供參考)", + "样本未识别到有效集数,智能生成失败": "樣本未識別到有效集數,智慧產生失敗", + "不存在依赖": "不存在依賴", + "主运行环境已恢复": "主執行環境已恢復", + "[PIP] 所有策略均安装依赖失败,请检查网络连接、PIP 配置或插件依赖约束": "[PIP] 所有策略均安裝依賴失敗,請檢查網路連線、PIP 設定或插件依賴約束", + "可执行文件模式下,只能安装本地插件": "可執行檔模式下,只能安裝本機插件", + "不支持的插件仓库地址格式": "不支援的插件倉庫位址格式", + "本地插件来源与插件ID不匹配": "本機插件來源與插件 ID 不匹配", + "本地插件来源不能与运行目录相同": "本機插件來源不能與執行目錄相同", + "本地插件来源路径无效": "本機插件來源路徑無效", + "连接仓库失败": "連線倉庫失敗", + "文件列表为空": "檔案清單為空", + "requirements.txt 文件下载失败": "requirements.txt 檔案下載失敗", + "插件在仓库中不存在或返回数据格式不正确": "插件在倉庫中不存在或返回資料格式不正確", + "插件数据解析失败": "插件資料解析失敗", + "没有传入需要安装的依赖项": "未傳入需要安裝的依賴項", + "资产缺少ID信息": "資產缺少 ID 資訊", + "压缩包内容为空": "壓縮包內容為空", + "压缩包中无可写入文件": "壓縮包中無可寫入檔案", + "连接MoviePilot服务器失败": "連線 MoviePilot 伺服器失敗", + "当前没有开启订阅数据共享功能": "目前未開啟訂閱資料分享功能", + "当前没有开启工作流数据共享功能": "目前未開啟工作流資料分享功能", + "请分享有动作和流程的工作流": "請分享有動作和流程的工作流", + "升级模式仅支持 release 或 dev": "升級模式僅支援 release 或 dev", + "当前实例不是由 moviepilot CLI 启动,无法执行内建重启!": "目前實例不是由 moviepilot CLI 啟動,無法執行內建重啟!", + "已检测到自动升级模式 dev,正在重启并执行升级": "已偵測到自動升級模式 dev,正在重啟並執行升級", + "已检测到自动升级已开启,正在重启并执行升级": "已偵測到自動升級已開啟,正在重啟並執行升級", + "获取容器ID失败!": "取得容器 ID 失敗!", + "未登录,缺少 bot token": "未登入,缺少 bot token", + "连接正常": "連線正常", + "未登录,请先扫码完成绑定": "未登入,請先掃碼完成綁定", + "旧名称或新名称不能为空": "舊名稱或新名稱不能為空", + "通知名称未变化,无需迁移登录缓存": "通知名稱未變更,無需遷移登入快取", + "未找到可迁移的微信 ClawBot 登录缓存": "未找到可遷移的微信 ClawBot 登入快取", + "新名称下已存在登录缓存,跳过迁移": "新名稱下已存在登入快取,跳過遷移", + "[系统]": "[系統]", + "工作流": "工作流", + "正在运行": "正在執行", + "等待": "等待", + "同步CookieCloud站点": "同步 CookieCloud 站點", + "同步媒体服务器": "同步媒體伺服器", + "订阅元数据更新": "訂閱元資料更新", + "订阅搜索补全": "訂閱搜尋補全", + "新增订阅搜索": "新增訂閱搜尋", + "订阅刷新": "訂閱重新整理", + "关注的订阅分享": "關注的訂閱分享", + "下载文件整理": "下載檔案整理", + "缓存清理": "快取清理", + "数据表清理": "資料表清理", + "用户认证检查": "使用者認證檢查", + "公共定时服务": "公共定時服務", + "壁纸缓存": "桌布快取", + "站点数据刷新": "站點資料重新整理", + "推荐缓存": "推薦快取", + "插件市场缓存": "插件市場快取", + "订阅日历缓存": "訂閱日曆快取", + "主动内存回收": "主動記憶體回收", + "智能体定时任务": "智慧體定時任務", + "安装版本统计上报": "安裝版本統計上報", + "开始清理数据表 ...": "開始清理資料表 ...", + "站点资源刷新完成": "站點資源重新整理完成", + "未配置媒体服务器,跳过同步": "未設定媒體伺服器,跳過同步", + "没有已启用的媒体服务器": "沒有已啟用的媒體伺服器", + "媒体服务器同步完成": "媒體伺服器同步完成", + "站点数据刷新完成": "站點資料重新整理完成", + "开始下载 CookieCloud 数据 ...": "開始下載 CookieCloud 資料 ...", + "推荐数据刷新完成,正在缓存海报 ...": "推薦資料重新整理完成,正在快取海報 ...", + "推荐缓存刷新完成": "推薦快取重新整理完成", + "未配置下载器监控目录,跳过整理": "未設定下載器監控目錄,跳過整理", + "正在查询已完成下载任务 ...": "正在查詢已完成下載任務 ...", + "没有已完成下载但未整理的任务": "沒有已完成下載但未整理的任務", + "未开启任何有效站点,无法搜索资源": "未啟用任何有效站點,無法搜尋資源", + "未开启任何支持字幕搜索的有效站点,无法搜索字幕": "未啟用任何支援字幕搜尋的有效站點,無法搜尋字幕", + "订阅搜索完成": "訂閱搜尋完成", + "没有订阅需要刷新": "沒有訂閱需要重新整理", + "订阅刷新完成": "訂閱重新整理完成", + "没有缓存资源,跳过订阅匹配": "沒有快取資源,跳過訂閱匹配", + "正在预处理订阅资源 ...": "正在預處理訂閱資源 ...", + "订阅资源匹配完成": "訂閱資源匹配完成", + "订阅元数据更新完成": "訂閱元資料更新完成", + "未配置 Follow 订阅用户,跳过刷新": "未設定 Follow 訂閱使用者,跳過重新整理", + "订阅日历预缓存完成": "訂閱日曆預快取完成" + }, + "message_patterns": [ + { + "source": "工具 '{tool}' 未找到", + "target": "工具「{tool}」未找到" + }, + { + "source": "获取工具列表失败: {reason}", + "target": "取得工具列表失敗: {reason}" + }, + { + "source": "获取工具信息失败: {reason}", + "target": "取得工具資訊失敗: {reason}" + }, + { + "source": "获取工具Schema失败: {reason}", + "target": "取得工具 Schema 失敗: {reason}" + }, + { + "source": "插件 {plugin} 不存在或未安装", + "target": "插件 {plugin} 不存在或未安裝" + }, + { + "source": "插件 {plugin} 不存在或未加载", + "target": "插件 {plugin} 不存在或未載入" + }, + { + "source": "站点 {site} 不存在!", + "target": "站點 {site} 不存在!" + }, + { + "source": "站点 {site} 不存在", + "target": "站點 {site} 不存在" + }, + { + "source": "站点 {site} 不支持", + "target": "站點 {site} 不支援" + }, + { + "source": "{path} 不是文件", + "target": "{path} 不是檔案" + }, + { + "source": "智能助手执行失败: {reason}", + "target": "智慧助手執行失敗: {reason}" + }, + { + "source": "无法连接Qbittorrent下载器:{name}", + "target": "無法連線 qBittorrent 下載器:{name}" + }, + { + "source": "无法连接Transmission下载器:{name}", + "target": "無法連線 Transmission 下載器:{name}" + }, + { + "source": "无法连接rTorrent下载器:{name}", + "target": "無法連線 rTorrent 下載器:{name}" + }, + { + "source": "无法连接Emby服务器:{name}", + "target": "無法連線 Emby 伺服器:{name}" + }, + { + "source": "无法连接Jellyfin服务器:{name}", + "target": "無法連線 Jellyfin 伺服器:{name}" + }, + { + "source": "无法连接Plex服务器:{name}", + "target": "無法連線 Plex 伺服器:{name}" + }, + { + "source": "飞牛影视配置不完整:{name}", + "target": "飛牛影視設定不完整:{name}" + }, + { + "source": "无法连接飞牛影视:{name}", + "target": "無法連線飛牛影視:{name}" + }, + { + "source": "绿联影视配置不完整:{name}", + "target": "綠聯影視設定不完整:{name}" + }, + { + "source": "无法连接绿联影视:{name}", + "target": "無法連線綠聯影視:{name}" + }, + { + "source": "无法连接极影视服务器:{name}", + "target": "無法連線極影視伺服器:{name}" + }, + { + "source": "Telegram {name} 未就绪", + "target": "Telegram {name} 尚未就緒" + }, + { + "source": "飞书 {name} 未就绪", + "target": "飛書 {name} 尚未就緒" + }, + { + "source": "Discord {name} Bot 未就绪", + "target": "Discord {name} Bot 尚未就緒" + }, + { + "source": "Slack {name} 未就绪", + "target": "Slack {name} 尚未就緒" + }, + { + "source": "QQ Bot {name} 未就绪", + "target": "QQ Bot {name} 尚未就緒" + }, + { + "source": "VoceChat {name} 未就绪", + "target": "VoceChat {name} 尚未就緒" + }, + { + "source": "企业微信 {name} 未就绪", + "target": "企業微信 {name} 尚未就緒" + }, + { + "source": "微信 ClawBot {name} 未就绪:{reason}", + "target": "微信 ClawBot {name} 尚未就緒:{reason}" + }, + { + "source": "Synology Chat {name} 未就绪", + "target": "Synology Chat {name} 尚未就緒" + }, + { + "source": "无法连接Bangumi,错误码:{code}", + "target": "無法連線 Bangumi,錯誤碼:{code}" + }, + { + "source": "无法连接fanart,错误码:{code}", + "target": "無法連線 fanart,錯誤碼:{code}" + }, + { + "source": "无法连接 {domain},错误码:{code}", + "target": "無法連線 {domain},錯誤碼:{code}" + }, + { + "source": "{domain} 网络连接失败", + "target": "{domain} 網路連線失敗" + }, + { + "source": "{item} 删除失败", + "target": "{item} 刪除失敗" + }, + { + "source": "错误:{code} {reason}", + "target": "錯誤:{code} {reason}" + }, + { + "source": "错误:{code} {reason}!", + "target": "錯誤:{code} {reason}!" + }, + { + "source": "站点【{url}】不存在", + "target": "站點【{url}】不存在" + }, + { + "source": "站点编号 {site_id} 不存在", + "target": "站點編號 {site_id} 不存在" + }, + { + "source": "未找到站点:{names}", + "target": "未找到站點:{names}" + }, + { + "source": "【{name}】Cookie&UA 更新失败:{reason}", + "target": "【{name}】Cookie&UA 更新失敗:{reason}" + }, + { + "source": "【{name}】Cookie&UA 更新成功", + "target": "【{name}】Cookie&UA 更新成功" + }, + { + "source": "下载目录获取失败,无法保存字幕:{path} - {reason}", + "target": "下載目錄取得失敗,無法儲存字幕:{path} - {reason}" + }, + { + "source": "下载目录不存在,无法保存字幕:{path}", + "target": "下載目錄不存在,無法儲存字幕:{path}" + }, + { + "source": "保存字幕文件失败:{path}", + "target": "儲存字幕檔案失敗:{path}" + }, + { + "source": "下载字幕文件失败,状态码:{code}", + "target": "下載字幕檔案失敗,狀態碼:{code}" + }, + { + "source": "下载字幕文件失败,状态码:{code} {reason}", + "target": "下載字幕檔案失敗,狀態碼:{code} {reason}" + }, + { + "source": "下载字幕文件失败,状态码:{code} {reason}:{detail}", + "target": "下載字幕檔案失敗,狀態碼:{code} {reason}:{detail}" + }, + { + "source": "下载链接不是支持的字幕文件:{name}", + "target": "下載連結不是支援的字幕檔案:{name}" + }, + { + "source": "字幕压缩包解压失败:{reason}", + "target": "字幕壓縮包解壓失敗:{reason}" + }, + { + "source": "未获取到第 {season} 季的总集数", + "target": "未取得第 {season} 季的總集數" + }, + { + "source": "所有订阅搜索完成", + "target": "所有訂閱搜尋完成" + }, + { + "source": "请输入订阅 ID,多个 ID 用空格分隔,或输入 all", + "target": "請輸入訂閱 ID,多個 ID 以空格分隔,或輸入 all" + }, + { + "source": "未找到订阅:{ids}", + "target": "未找到訂閱:{ids}" + }, + { + "source": "已完成 {count} 个订阅搜索", + "target": "已完成 {count} 個訂閱搜尋" + }, + { + "source": "请输入至少一个有效的订阅 ID", + "target": "請輸入至少一個有效的訂閱 ID" + }, + { + "source": "已删除 {count} 个订阅", + "target": "已刪除 {count} 個訂閱" + }, + { + "source": "{name} 已在整理队列中", + "target": "{name} 已在整理佇列中" + }, + { + "source": "{name} 已取消", + "target": "{name} 已取消" + }, + { + "source": "{name} 没有找到可整理的媒体文件", + "target": "{name} 沒有找到可整理的媒體檔案" + }, + { + "source": "{name} 已整理过", + "target": "{name} 已整理過" + }, + { + "source": "{name} 无法识别有效信息", + "target": "{name} 無法識別有效資訊" + }, + { + "source": "源目录不存在:{path}", + "target": "來源目錄不存在:{path}" + }, + { + "source": "未识别到媒体信息,类型:{type},id:{id}", + "target": "未識別到媒體資訊,類型:{type},id:{id}" + }, + { + "source": "媒体信息识别失败,tmdbid:{tmdbid},doubanid:{doubanid},type: {type}", + "target": "媒體資訊識別失敗,tmdbid:{tmdbid},doubanid:{doubanid},type: {type}" + }, + { + "source": "{name} 的下载目录未设置", + "target": "{name} 的下載目錄未設定" + }, + { + "source": "{name} 的下载目录 {path} 不存在", + "target": "{name} 的下載目錄 {path} 不存在" + }, + { + "source": "{name} 的媒体库目录未设置", + "target": "{name} 的媒體庫目錄未設定" + }, + { + "source": "{name} 的媒体库目录 {path} 不存在", + "target": "{name} 的媒體庫目錄 {path} 不存在" + }, + { + "source": "{name} 的下载目录 {download_path} 与媒体库目录 {library_path} 不在同一磁盘,无法硬链接", + "target": "{name} 的下載目錄 {download_path} 與媒體庫目錄 {library_path} 不在同一磁碟,無法硬連結" + }, + { + "source": "{name} 的存储测试不通过", + "target": "{name} 的儲存測試不通過" + }, + { + "source": "{name} 的存储不支持 {transfer_type} 整理方式", + "target": "{name} 的儲存不支援 {transfer_type} 整理方式" + }, + { + "source": "不支持 {storage} 的 OAuth2 授权", + "target": "不支援 {storage} 的 OAuth2 授權" + }, + { + "source": "不支持 {source_storage} 到 {target_storage} 的文件整理", + "target": "不支援 {source_storage} 到 {target_storage} 的檔案整理" + }, + { + "source": "不支持的整理方式:{transfer_type}", + "target": "不支援的整理方式:{transfer_type}" + }, + { + "source": "{path} {transfer_type} 失败", + "target": "{path} {transfer_type} 失敗" + }, + { + "source": "文件 {path} 不存在", + "target": "檔案 {path} 不存在" + }, + { + "source": "{path} 上传 {storage} 失败", + "target": "{path} 上傳 {storage} 失敗" + }, + { + "source": "{path} {storage} 下载失败", + "target": "{path} {storage} 下載失敗" + }, + { + "source": "存储 {storage} 不支持 {transfer_type} 整理方式", + "target": "儲存 {storage} 不支援 {transfer_type} 整理方式" + }, + { + "source": "【{storage}】{path} 复制文件失败", + "target": "【{storage}】{path} 複製檔案失敗" + }, + { + "source": "【{storage}】{path} 移动文件失败", + "target": "【{storage}】{path} 移動檔案失敗" + }, + { + "source": "【{storage}】{path} 创建硬链接失败", + "target": "【{storage}】{path} 建立硬連結失敗" + }, + { + "source": "【{storage}】{path} 目录获取失败", + "target": "【{storage}】{path} 目錄取得失敗" + }, + { + "source": "获取目标目录失败:{path}", + "target": "取得目標目錄失敗:{path}" + }, + { + "source": "{path} 已存在", + "target": "{path} 已存在" + }, + { + "source": "【{storage}】{path} 已存在", + "target": "【{storage}】{path} 已存在" + }, + { + "source": "添加种子任务失败:{reason}", + "target": "新增種子任務失敗:{reason}" + }, + { + "source": "下载任务添加成功,但获取Qbittorrent任务信息失败:{reason}", + "target": "下載任務新增成功,但取得 qBittorrent 任務資訊失敗:{reason}" + }, + { + "source": "添加下载成功,已选择集数:{episodes}", + "target": "新增下載成功,已選擇集數:{episodes}" + }, + { + "source": "获取授权 URL 失败: {reason}", + "target": "取得授權 URL 失敗: {reason}" + }, + { + "source": "检查授权状态失败: {reason}", + "target": "檢查授權狀態失敗: {reason}" + }, + { + "source": "{path} 刮削完成", + "target": "{path} 刮削完成" + }, + { + "source": "生成注册选项失败: {reason}", + "target": "產生註冊選項失敗: {reason}" + }, + { + "source": "注册失败: {reason}", + "target": "註冊失敗: {reason}" + }, + { + "source": "获取列表失败: {reason}", + "target": "取得清單失敗: {reason}" + }, + { + "source": "删除失败: {reason}", + "target": "刪除失敗: {reason}" + }, + { + "source": "文件夹 '{folder}' 创建成功", + "target": "資料夾 '{folder}' 建立成功" + }, + { + "source": "文件夹 '{folder}' 已存在", + "target": "資料夾 '{folder}' 已存在" + }, + { + "source": "文件夹 '{folder}' 删除成功", + "target": "資料夾 '{folder}' 刪除成功" + }, + { + "source": "文件夹 '{folder}' 不存在", + "target": "資料夾 '{folder}' 不存在" + }, + { + "source": "文件夹 '{folder}' 中的插件已更新", + "target": "資料夾 '{folder}' 中的插件已更新" + }, + { + "source": "创建插件分身失败:{reason}", + "target": "建立插件分身失敗:{reason}" + }, + { + "source": "{domain} 站点己存在", + "target": "{domain} 站點已存在" + }, + { + "source": "获取映射失败:{reason}", + "target": "取得映射失敗:{reason}" + }, + { + "source": "{name} 未识别到媒体信息", + "target": "{name} 未識別到媒體資訊" + }, + { + "source": "{name} 未识别到新名称", + "target": "{name} 未識別到新名稱" + }, + { + "source": "{name} 重命名失败!", + "target": "{name} 重新命名失敗!" + }, + { + "source": "访问 Wiki 插件仓库清单失败,状态码:{code}", + "target": "存取 Wiki 插件倉庫清單失敗,狀態碼:{code}" + }, + { + "source": "配置项 '{key}' 不存在", + "target": "設定項 '{key}' 不存在" + }, + { + "source": "过滤规则组 {name} 不存在!", + "target": "過濾規則組 {name} 不存在!" + }, + { + "source": "{target}无法连接", + "target": "{target}無法連線" + }, + { + "source": "站点 {domain} 缓存不存在", + "target": "站點 {domain} 快取不存在" + }, + { + "source": "删除失败:{reason}", + "target": "刪除失敗:{reason}" + }, + { + "source": "清理失败:{reason}", + "target": "清理失敗:{reason}" + }, + { + "source": "缓存刷新完成,共刷新 {site_count} 个站点,{torrent_count} 个种子", + "target": "快取重新整理完成,共重新整理 {site_count} 個站點,{torrent_count} 個種子" + }, + { + "source": "刷新失败:{reason}", + "target": "重新整理失敗:{reason}" + }, + { + "source": "重新识别失败:{reason}", + "target": "重新識別失敗:{reason}" + }, + { + "source": "整理记录不存在,ID:{id}", + "target": "整理記錄不存在,ID:{id}" + }, + { + "source": "不支持的媒体类型:{type}", + "target": "不支援的媒體類型:{type}" + }, + { + "source": "整理记录不存在: {ids}", + "target": "整理記錄不存在: {ids}" + }, + { + "source": "未找到名为 {name} 的微信 ClawBot 通知配置", + "target": "未找到名為 {name} 的微信 ClawBot 通知設定" + }, + { + "source": "下载字幕文件失败:{reason}", + "target": "下載字幕檔案失敗:{reason}" + }, + { + "source": "更新了{updated}个站点,新增了{created}个站点", + "target": "更新了 {updated} 個站點,新增了 {created} 個站點" + }, + { + "source": "已{action} {count} 个站点", + "target": "已{action} {count} 個站點" + }, + { + "source": "认证类型:{type},辅助认证事件失败或无效", + "target": "認證類型:{type},輔助認證事件失敗或無效" + }, + { + "source": "远程同步CookieCloud失败,错误码:{code}", + "target": "遠端同步 CookieCloud 失敗,錯誤碼:{code}" + }, + { + "source": "未从{server}下载到cookie数据", + "target": "未從 {server} 下載到 cookie 資料" + }, + { + "source": "从{server}下载cookie数据错误:{reason}", + "target": "從 {server} 下載 cookie 資料錯誤:{reason}" + }, + { + "source": "cookie解密失败:{reason}", + "target": "cookie 解密失敗:{reason}" + }, + { + "source": "插件限定的系统版本范围 {range} 必须是字符串,请使用 pip 依赖版本格式,例如 >=2.12.0,<3", + "target": "插件限定的系統版本範圍 {range} 必須是字串,請使用 pip 依賴版本格式,例如 >=2.12.0,<3" + }, + { + "source": "插件限定的系统版本范围格式不正确:{range},请使用 pip 依赖版本格式,例如 >=2.12.0,<3", + "target": "插件限定的系統版本範圍格式不正確:{range},請使用 pip 依賴版本格式,例如 >=2.12.0,<3" + }, + { + "source": "当前 MoviePilot 版本 {version} 无法解析,已拒绝安装带版本限制的插件", + "target": "目前 MoviePilot 版本 {version} 無法解析,已拒絕安裝帶版本限制的插件" + }, + { + "source": "插件要求 MoviePilot 版本 {range},当前版本 {version} 不满足,已拒绝安装", + "target": "插件要求 MoviePilot 版本 {range},目前版本 {version} 不滿足,已拒絕安裝" + }, + { + "source": "插件依赖与当前运行环境的{dependency}冲突:{reason}。为避免共享运行环境被污染,已拒绝安装。", + "target": "插件依賴與目前執行環境的 {dependency} 衝突:{reason}。為避免共享執行環境被污染,已拒絕安裝。" + }, + { + "source": "插件依赖安装失败后主运行环境异常,且恢复失败:{health}; {repair}", + "target": "插件依賴安裝失敗後主執行環境異常,且恢復失敗:{health}; {repair}" + }, + { + "source": "插件依赖安装失败后主运行环境异常,恢复后仍异常:{reason}", + "target": "插件依賴安裝失敗後主執行環境異常,恢復後仍異常:{reason}" + }, + { + "source": "{check_name}失败:{message}", + "target": "{check_name}失敗:{message}" + }, + { + "source": "恢复依赖文件不存在:{path}", + "target": "恢復依賴檔案不存在:{path}" + }, + { + "source": "恢复{desc}失败", + "target": "恢復{desc}失敗" + }, + { + "source": "[PIP] 所有策略均安装依赖失败:{reason}", + "target": "[PIP] 所有策略均安裝依賴失敗:{reason}" + }, + { + "source": "{pid} 未声明 Release 安装,无法安装指定版本", + "target": "{pid} 未宣告 Release 安裝,無法安裝指定版本" + }, + { + "source": "{pid} 未找到可安装的 Release 版本:{version}", + "target": "{pid} 未找到可安裝的 Release 版本:{version}" + }, + { + "source": "未在插件清单中找到 {pid} 的版本号,无法进行 Release 安装", + "target": "未在插件清單中找到 {pid} 的版本號,無法進行 Release 安裝" + }, + { + "source": "未找到本地插件:{pid}", + "target": "未找到本機插件:{pid}" + }, + { + "source": "复制本地插件失败:{reason}", + "target": "複製本機插件失敗:{reason}" + }, + { + "source": "连接仓库失败:{code} - {reason}", + "target": "連線倉庫失敗:{code} - {reason}" + }, + { + "source": "文件 {path} 下载失败!", + "target": "檔案 {path} 下載失敗!" + }, + { + "source": "下载文件 {path} 失败:{code}", + "target": "下載檔案 {path} 失敗:{code}" + }, + { + "source": "下载 requirements.txt 文件失败:{code}", + "target": "下載 requirements.txt 檔案失敗:{code}" + }, + { + "source": "插件依赖预检失败:{reason}", + "target": "插件依賴預檢失敗:{reason}" + }, + { + "source": "获取 Release 信息失败:{reason}", + "target": "取得 Release 資訊失敗:{reason}" + }, + { + "source": "未找到资产文件:{asset}", + "target": "未找到資產檔案:{asset}" + }, + { + "source": "解析 Release 信息失败:{reason}", + "target": "解析 Release 資訊失敗:{reason}" + }, + { + "source": "下载资产失败:{reason}", + "target": "下載資產失敗:{reason}" + }, + { + "source": "解压 Release 压缩包失败:{reason}", + "target": "解壓 Release 壓縮包失敗:{reason}" + }, + { + "source": "安装依赖项时发生错误:{reason}", + "target": "安裝依賴項時發生錯誤:{reason}" + }, + { + "source": "创建运行环境约束文件失败:{reason}", + "target": "建立執行環境約束檔案失敗:{reason}" + }, + { + "source": "策略 {strategy} 安装依赖失败:{reason};{detail}", + "target": "策略 {strategy} 安裝依賴失敗:{reason};{detail}" + }, + { + "source": "依赖安装后运行环境自检失败,已自动恢复主程序依赖:{reason}", + "target": "依賴安裝後執行環境自檢失敗,已自動恢復主程式依賴:{reason}" + }, + { + "source": "依赖安装后运行环境自检失败,恢复主程序依赖后仍异常:{reason}", + "target": "依賴安裝後執行環境自檢失敗,恢復主程式依賴後仍異常:{reason}" + }, + { + "source": "依赖安装后运行环境自检失败,且自动恢复主程序依赖失败:{reason}", + "target": "依賴安裝後執行環境自檢失敗,且自動恢復主程式依賴失敗:{reason}" + }, + { + "source": "响应解析失败: {detail}...", + "target": "回應解析失敗: {detail}..." + }, + { + "source": "写入一次性升级标记失败:{reason}", + "target": "寫入一次性升級標記失敗:{reason}" + }, + { + "source": "本地 CLI 重启失败:{reason}", + "target": "本機 CLI 重啟失敗:{reason}" + }, + { + "source": "重启时发生错误:{reason}", + "target": "重啟時發生錯誤:{reason}" + }, + { + "source": "已安排一次性 {mode} 升级并重启", + "target": "已安排一次性 {mode} 升級並重啟" + }, + { + "source": "错误码:{code}", + "target": "錯誤碼:{code}" + }, + { + "source": "PostgreSQL连接失败:{reason}", + "target": "PostgreSQL 連線失敗:{reason}" + }, + { + "source": "整理完成,{count} 个文件转移失败!", + "target": "整理完成,{count} 個檔案轉移失敗!" + }, + { + "source": "下载任务添加成功,但获取rTorrent任务信息失败:{reason}", + "target": "下載任務新增成功,但取得 rTorrent 任務資訊失敗:{reason}" + }, + { + "source": "已将微信 ClawBot 登录缓存从 {old_name} 迁移到 {new_name}", + "target": "已將微信 ClawBot 登入快取從 {old_name} 遷移到 {new_name}" + }, + { + "source": "数据表 {name} 跳过清理", + "target": "資料表 {name} 跳過清理" + }, + { + "source": "正在清理数据表 {name} ...", + "target": "正在清理資料表 {name} ..." + }, + { + "source": "数据表 {name} 清理处理完成", + "target": "資料表 {name} 清理處理完成" + }, + { + "source": "开始刷新站点资源,共 {count} 个站点 ...", + "target": "開始重新整理站點資源,共 {count} 個站點 ..." + }, + { + "source": "开始同步媒体服务器,共 {count} 个 ...", + "target": "開始同步媒體伺服器,共 {count} 個 ..." + }, + { + "source": "媒体服务器 {name} 无可同步媒体库", + "target": "媒體伺服器 {name} 無可同步媒體庫" + }, + { + "source": "开始刷新站点数据,共 {count} 个站点 ...", + "target": "開始重新整理站點資料,共 {count} 個站點 ..." + }, + { + "source": "正在刷新站点数据({index}/{total}){name} ...", + "target": "正在重新整理站點資料({index}/{total}){name} ..." + }, + { + "source": "站点数据({index}/{total})刷新完成", + "target": "站點資料({index}/{total})重新整理完成" + }, + { + "source": "CookieCloud同步失败:{reason}", + "target": "CookieCloud 同步失敗:{reason}" + }, + { + "source": "正在同步 CookieCloud 站点({index}/{total}){domain} ...", + "target": "正在同步 CookieCloud 站點({index}/{total}){domain} ..." + }, + { + "source": "CookieCloud 站点({index}/{total})同步完成", + "target": "CookieCloud 站點({index}/{total})同步完成" + }, + { + "source": "CookieCloud同步成功:{message}", + "target": "CookieCloud 同步成功:{message}" + }, + { + "source": "开始刷新推荐缓存,共 {count} 个数据分页 ...", + "target": "開始重新整理推薦快取,共 {count} 個資料分頁 ..." + }, + { + "source": "正在缓存推荐海报({index}/{total})...", + "target": "正在快取推薦海報({index}/{total})..." + }, + { + "source": "获取到 {count} 个已完成下载任务", + "target": "取得 {count} 個已完成下載任務" + }, + { + "source": "正在整理下载任务({index}/{total}){name} ...", + "target": "正在整理下載任務({index}/{total}){name} ..." + }, + { + "source": "下载任务({index}/{total})整理处理完成", + "target": "下載任務({index}/{total})整理處理完成" + }, + { + "source": "搜索完成,共 {count} 个资源", + "target": "搜尋完成,共 {count} 個資源" + }, + { + "source": "搜索完成,共 {count} 个字幕", + "target": "搜尋完成,共 {count} 個字幕" + }, + { + "source": "正在过滤匹配 {count} 个候选资源 ...", + "target": "正在過濾匹配 {count} 個候選資源 ..." + }, + { + "source": "过滤匹配完成,共 {count} 个资源", + "target": "過濾匹配完成,共 {count} 個資源" + }, + { + "source": "正在识别匹配 {count} 个候选字幕 ...", + "target": "正在識別匹配 {count} 個候選字幕 ..." + }, + { + "source": "识别匹配完成,共 {count} 个字幕", + "target": "識別匹配完成,共 {count} 個字幕" + }, + { + "source": "开始搜索字幕,共 {site_count} 个站点,{page_count} 页 ...", + "target": "開始搜尋字幕,共 {site_count} 個站點,{page_count} 頁 ..." + }, + { + "source": "正在搜索字幕{keyword},已完成 {finished} / {total} 个请求 ...", + "target": "正在搜尋字幕{keyword},已完成 {finished} / {total} 個請求 ..." + }, + { + "source": "正在搜索字幕,已完成 {finished} / {total} 个请求 ...", + "target": "正在搜尋字幕,已完成 {finished} / {total} 個請求 ..." + }, + { + "source": "站点字幕搜索完成,有效字幕数:{count},总耗时 {seconds} 秒", + "target": "站點字幕搜尋完成,有效字幕數:{count},總耗時 {seconds} 秒" + }, + { + "source": "开始搜索,共 {site_count} 个站点,{page_count} 页 ...", + "target": "開始搜尋,共 {site_count} 個站點,{page_count} 頁 ..." + }, + { + "source": "正在搜索{keyword},已完成 {finished} / {total} 个请求 ...", + "target": "正在搜尋{keyword},已完成 {finished} / {total} 個請求 ..." + }, + { + "source": "正在搜索,已完成 {finished} / {total} 个请求 ...", + "target": "正在搜尋,已完成 {finished} / {total} 個請求 ..." + }, + { + "source": "站点搜索完成,有效资源数:{count},总耗时 {seconds} 秒", + "target": "站點搜尋完成,有效資源數:{count},總耗時 {seconds} 秒" + }, + { + "source": "开始订阅搜索,共 {count} 个订阅 ...", + "target": "開始訂閱搜尋,共 {count} 個訂閱 ..." + }, + { + "source": "正在搜索订阅({index}/{total}){name} ...", + "target": "正在搜尋訂閱({index}/{total}){name} ..." + }, + { + "source": "订阅搜索随机休眠 {seconds} 秒后继续 ...", + "target": "訂閱搜尋隨機休眠 {seconds} 秒後繼續 ..." + }, + { + "source": "订阅搜索({index}/{total})处理完成", + "target": "訂閱搜尋({index}/{total})處理完成" + }, + { + "source": "开始刷新订阅,共 {count} 个订阅 ...", + "target": "開始重新整理訂閱,共 {count} 個訂閱 ..." + }, + { + "source": "资源预处理完成,开始匹配 {count} 个订阅 ...", + "target": "資源預處理完成,開始匹配 {count} 個訂閱 ..." + }, + { + "source": "开始更新订阅元数据,共 {count} 个订阅 ...", + "target": "開始更新訂閱元資料,共 {count} 個訂閱 ..." + }, + { + "source": "正在更新订阅元数据({index}/{total}){name} ...", + "target": "正在更新訂閱元資料({index}/{total}){name} ..." + }, + { + "source": "订阅元数据({index}/{total})更新完成", + "target": "訂閱元資料({index}/{total})更新完成" + }, + { + "source": "开始刷新 Follow 订阅分享,共 {count} 条 ...", + "target": "開始重新整理 Follow 訂閱分享,共 {count} 條 ..." + }, + { + "source": "正在处理 Follow 订阅分享({index}/{total})...", + "target": "正在處理 Follow 訂閱分享({index}/{total})..." + }, + { + "source": "Follow 订阅分享刷新完成,新增 {count} 个订阅", + "target": "Follow 訂閱分享重新整理完成,新增 {count} 個訂閱" + }, + { + "source": "开始预缓存订阅日历,共 {count} 个订阅 ...", + "target": "開始預快取訂閱日曆,共 {count} 個訂閱 ..." + }, + { + "source": "正在预缓存订阅日历({index}/{total}){name} ...", + "target": "正在預快取訂閱日曆({index}/{total}){name} ..." + }, + { + "source": "订阅日历({index}/{total})预缓存完成", + "target": "訂閱日曆({index}/{total})預快取完成" + }, + { + "source": "{path} 不存在", + "target": "{path} 不存在" + }, + { + "source": "{days}天{hours}小时{minutes}分钟", + "target": "{days}天{hours}小時{minutes}分鐘" + }, + { + "source": "{days}天{hours}小时", + "target": "{days}天{hours}小時" + }, + { + "source": "{days}天{minutes}分钟", + "target": "{days}天{minutes}分鐘" + }, + { + "source": "{hours}小时{minutes}分钟", + "target": "{hours}小時{minutes}分鐘" + }, + { + "source": "{days}天", + "target": "{days}天" + }, + { + "source": "{hours}小时", + "target": "{hours}小時" + }, + { + "source": "{minutes}分钟", + "target": "{minutes}分鐘" + }, + { + "source": "{seconds}秒", + "target": "{seconds}秒" + } + ] +} diff --git a/app/schemas/dashboard.py b/app/schemas/dashboard.py index fd826df5c..2f9a01959 100644 --- a/app/schemas/dashboard.py +++ b/app/schemas/dashboard.py @@ -1,6 +1,8 @@ from typing import Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator + +from app.helper.locale import LocaleHelper class Statistic(BaseModel): @@ -87,14 +89,20 @@ class ScheduleProgress(BaseModel): id: Optional[str] = None # 名称 name: Optional[str] = None + # 多语言名称 + name_i18n: Optional[str] = None # 提供者 provider: Optional[str] = None + # 多语言提供者 + provider_i18n: Optional[str] = None # 是否正在执行 enable: Optional[bool] = False # 当前完成百分比 value: Optional[float] = 0.0 # 当前进度文本 text: Optional[str] = None + # 多语言进度文本 + text_i18n: Optional[str] = None # 执行状态 waiting/running/success/failed status: Optional[str] = None # 最近一次执行是否成功 @@ -105,9 +113,27 @@ class ScheduleProgress(BaseModel): finished_at: Optional[str] = None # 最近一次错误信息 error: Optional[str] = None + # 多语言错误信息 + error_i18n: Optional[str] = None # 扩展数据 data: Optional[dict] = Field(default_factory=dict) + @model_validator(mode="after") + def fill_i18n_fields(self) -> "ScheduleProgress": + """ + 自动补充后台服务进度的多语言展示字段。 + """ + locale = LocaleHelper.get_current_locale() + if self.name and self.name_i18n is None: + self.name_i18n = LocaleHelper.translate_text(self.name, locale=locale) + if self.provider and self.provider_i18n is None: + self.provider_i18n = LocaleHelper.translate_text(self.provider, locale=locale) + if self.text and self.text_i18n is None: + self.text_i18n = LocaleHelper.translate_text(self.text, locale=locale) + if self.error and self.error_i18n is None: + self.error_i18n = LocaleHelper.translate_text(self.error, locale=locale) + return self + class ScheduleInfo(BaseModel): """仪表板后台服务信息。""" @@ -116,21 +142,49 @@ class ScheduleInfo(BaseModel): id: Optional[str] = None # 名称 name: Optional[str] = None + # 多语言名称 + name_i18n: Optional[str] = None # 提供者 provider: Optional[str] = None + # 多语言提供者 + provider_i18n: Optional[str] = None # 状态 status: Optional[str] = None + # 多语言状态 + status_i18n: Optional[str] = None # 下次执行时间 next_run: Optional[str] = None + # 多语言下次执行时间 + next_run_i18n: Optional[str] = None # 当前完成百分比 progress: Optional[float] = 0.0 # 进度文本 progress_text: Optional[str] = None + # 多语言进度文本 + progress_text_i18n: Optional[str] = None # 是否正在更新进度 progress_enable: Optional[bool] = False # 进度详情 progress_detail: Optional[ScheduleProgress] = None + @model_validator(mode="after") + def fill_i18n_fields(self) -> "ScheduleInfo": + """ + 自动补充后台服务列表的多语言展示字段。 + """ + locale = LocaleHelper.get_current_locale() + if self.name and self.name_i18n is None: + self.name_i18n = LocaleHelper.translate_text(self.name, locale=locale) + if self.provider and self.provider_i18n is None: + self.provider_i18n = LocaleHelper.translate_text(self.provider, locale=locale) + if self.status and self.status_i18n is None: + self.status_i18n = LocaleHelper.translate_text(self.status, locale=locale) + if self.next_run and self.next_run_i18n is None: + self.next_run_i18n = LocaleHelper.translate_text(self.next_run, locale=locale) + if self.progress_text and self.progress_text_i18n is None: + self.progress_text_i18n = LocaleHelper.translate_text(self.progress_text, locale=locale) + return self + class DashboardSystemInfo(BaseModel): """仪表板系统摘要信息。""" diff --git a/app/schemas/response.py b/app/schemas/response.py index 8e71d67ee..89df31652 100644 --- a/app/schemas/response.py +++ b/app/schemas/response.py @@ -1,12 +1,29 @@ from typing import Optional, Union -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator + +from app.helper.locale import LocaleHelper class Response(BaseModel): + """通用接口响应结构""" + # 状态 success: bool # 消息文本 message: Optional[str] = None + # 多语言消息文本 + message_i18n: Optional[str] = None # 数据 data: Optional[Union[dict, list]] = Field(default_factory=dict) + + @model_validator(mode="after") + def fill_message_i18n(self) -> "Response": + """ + 自动补充响应消息的多语言文本。 + """ + if self.message and self.message_i18n is None: + self.message_i18n = LocaleHelper.translate_text( + self.message, locale=LocaleHelper.get_current_locale() + ) + return self diff --git a/docs/mcp-api.md b/docs/mcp-api.md index f8881dcf8..ec04893b7 100644 --- a/docs/mcp-api.md +++ b/docs/mcp-api.md @@ -86,6 +86,10 @@ MCP 使用系统配置中的 `API_TOKEN` 作为认证密钥,文档中的 API K MoviePilot 也提供普通 REST API 给前端和自动化客户端使用。所有接口同样需要 API KEY 认证,在请求头中添加 `X-API-KEY: ` 或在查询参数中添加 `apikey=`。 +标准 REST 响应包含 `success`、`message`、`message_i18n`、`data` 字段。为兼容 App 和第三方客户端,`message` 继续保留原中文或原始后端文本;新版前端可发送 `X-MoviePilot-Locale: zh-CN|zh-TW|en-US` 或 `Accept-Language`,并优先展示 `message_i18n`。未提供语言头或翻译缺失时,`message_i18n` 会回退为原文本。 + +FastAPI 异常响应保留 `detail` 字段,并在错误详情为文本时返回 `detail_i18n`;新版前端优先展示 `detail_i18n`,缺失时回退 `detail`。 + #### 搜索 / 种子 / 字幕 | 方法 | 路径 | 说明 | @@ -127,6 +131,8 @@ MoviePilot 也提供普通 REST API 给前端和自动化客户端使用。所 | GET | `/api/v1/dashboard/schedule2/{job_id}/progress` | 使用 API_TOKEN 查询指定后台定时服务的实时进度详情 | | GET | `/api/v1/system/setting/public/{key}` | 登录用户读取白名单内非敏感系统设置,仅支持目录、存储、站点范围、默认订阅规则、Follow 订阅者和插件市场地址等前端必需配置 | | POST | `/api/v1/system/setting/PLUGIN_MARKET/sync-wiki` | 管理员从 MoviePilot Wiki 的插件文档同步公开插件仓库清单,和本地 `PLUGIN_MARKET` 合并去重后写入配置 | +| GET | `/api/v1/system/modulelist` | 查询已加载模块,保留 `name` 原始中文字段,并提供 `name_i18n` 和 `name_key` 给多语言前端展示 | +| GET | `/api/v1/system/moduletest/{moduleid}` | 测试指定模块可用性,保留原 `message`,并在标准响应顶层返回 `message_i18n` | ### 插件补充接口 diff --git a/tests/test_locale_helper.py b/tests/test_locale_helper.py new file mode 100644 index 000000000..b12402042 --- /dev/null +++ b/tests/test_locale_helper.py @@ -0,0 +1,366 @@ +import ast +import asyncio +import json +from pathlib import Path +from types import SimpleNamespace + +from fastapi import HTTPException + +from app.factory import localized_http_exception_handler +from app.helper.locale import LocaleHelper +from app.helper.progress import ProgressHelper +from app.schemas.dashboard import ScheduleInfo, ScheduleProgress +from app.schemas.response import Response + + +def _has_chinese(text: str) -> bool: + """判断文本是否包含中文字符。""" + return any("\u4e00" <= char <= "\u9fff" for char in text) + + +def _sample_message_expression(value: ast.AST) -> list[str]: + """从接口 message 表达式中生成用于翻译校验的样本文本。""" + if isinstance(value, ast.Constant) and isinstance(value.value, str): + return [value.value] + if isinstance(value, ast.JoinedStr): + parts = [] + has_chinese = False + index = 0 + for item in value.values: + if isinstance(item, ast.Constant) and isinstance(item.value, str): + parts.append(item.value) + has_chinese = has_chinese or _has_chinese(item.value) + else: + index += 1 + parts.append(f"样例{index}") + return ["".join(parts)] if has_chinese else [] + if isinstance(value, ast.IfExp): + return _sample_message_expression(value.body) + _sample_message_expression(value.orelse) + if isinstance(value, ast.BoolOp): + samples = [] + for item in value.values: + samples.extend(_sample_message_expression(item)) + return samples + if isinstance(value, ast.BinOp) and isinstance(value.op, ast.Add): + left_samples = _sample_message_expression(value.left) + right_samples = _sample_message_expression(value.right) + if left_samples and right_samples: + return [left + right for left in left_samples for right in right_samples] + if left_samples: + return [f"{left}样例" for left in left_samples] + if right_samples: + return [f"样例{right}" for right in right_samples] + return [] + + +def test_locale_helper_normalizes_supported_aliases(): + """语言别名应规范化为项目支持的语言标识。""" + assert LocaleHelper.normalize_locale("en") == "en-US" + assert LocaleHelper.normalize_locale("zh_Hant") == "zh-TW" + assert LocaleHelper.normalize_locale("unknown") == "zh-CN" + + +def test_locale_helper_reads_request_headers_by_priority(): + """请求语言应优先使用显式前端语言头,再回退 Accept-Language。""" + request = SimpleNamespace( + headers={ + "x-moviepilot-locale": "en-US", + "accept-language": "zh-TW,zh;q=0.9", + } + ) + + assert LocaleHelper.get_locale_from_request(request) == "en-US" + + +def test_locale_helper_reads_query_locale_before_headers(): + """SSE 请求可通过查询参数显式指定前端语言。""" + request = SimpleNamespace( + query_params={"locale": "zh-TW"}, + headers={ + "x-moviepilot-locale": "en-US", + "accept-language": "en-US", + }, + ) + + assert LocaleHelper.get_locale_from_request(request) == "zh-TW" + + +def test_locale_helper_parses_accept_language_quality(): + """Accept-Language 应按 q 权重选择支持的语言。""" + request = SimpleNamespace( + headers={ + "accept-language": "fr-FR, en-US;q=0.8, zh-TW;q=0.9", + } + ) + + assert LocaleHelper.get_locale_from_request(request) == "zh-TW" + + +def test_locale_helper_translates_module_name_and_keeps_missing_text(): + """翻译存在时返回目标语言,缺失时返回默认文本。""" + assert LocaleHelper.translate( + "system.modules.DoubanModule.name", + locale="en-US", + default="豆瓣", + ) == "Douban" + assert LocaleHelper.translate_text("模块不支持测试", locale="en-US") == "Module does not support testing" + assert LocaleHelper.translate_text( + "requirements.txt 文件下载失败", locale="en-US" + ) == "Failed to download requirements.txt" + assert LocaleHelper.translate_text("未收录的中文错误", locale="en-US") == "未收录的中文错误" + + +def test_locale_helper_translates_dynamic_message_patterns(): + """动态消息应保留变量并翻译模板文本。""" + assert LocaleHelper.translate_text( + "无法连接Qbittorrent下载器:默认", + locale="en-US", + ) == "Unable to connect to qBittorrent downloader: 默认" + assert LocaleHelper.translate_text( + "无法连接 api.themoviedb.org,错误码:403", + locale="en-US", + ) == "Unable to connect to api.themoviedb.org, error code: 403" + assert LocaleHelper.translate_text( + "飞书 工作 未就绪", + locale="zh-TW", + ) == "飛書 工作 尚未就緒" + + +def test_locale_helper_translates_common_backend_response_messages(): + """常见后端链路消息应能生成英文展示文本。""" + samples = { + "QQ Bot 默认 未就绪": "QQ Bot 默认 is not ready", + "微信 ClawBot 工作 未就绪:未登录": "WeChat ClawBot 工作 is not ready: 未登录", + "无法打开网站!": "Unable to open the site!", + "错误:403 Forbidden": "Error: 403 Forbidden", + "站点【https://example.com】不存在": "Site [https://example.com] does not exist", + "下载字幕文件失败,状态码:404 Not Found": "Failed to download subtitle file, status code: 404 Not Found", + "未获取到第 2 季的总集数": "Unable to get the total episode count for season 2", + "电影.mkv 已在整理队列中": "电影.mkv is already in the organization queue", + "未识别到媒体信息,类型:电视剧,id:123": "Unable to recognize media information, type: 电视剧, id: 123", + "默认 的下载目录 /downloads 不存在": "Download directory for 默认 does not exist: /downloads", + "不支持 Local 到 Alist 的文件整理": "File organization from Local to Alist is not supported", + "文件 /tmp/a.mkv 不存在": "File /tmp/a.mkv does not exist", + "添加种子任务失败:种子无效": "Failed to add torrent task: 种子无效", + "检查授权状态失败: timeout": "Failed to check authorization status: timeout", + "未找到名为 工作 的微信 ClawBot 通知配置": ( + "No WeChat ClawBot notification configuration named 工作 was found" + ), + "整理记录不存在: 1, 2": "Organization record does not exist: 1, 2", + "插件要求 MoviePilot 版本 >=2.14.0,当前版本 2.13.0 不满足,已拒绝安装": ( + "The plugin requires MoviePilot version >=2.14.0, but current version 2.13.0 " + "does not satisfy it. Installation was rejected" + ), + "已安排一次性 release 升级并重启": "Scheduled one-shot release upgrade and restart", + "已将微信 ClawBot 登录缓存从 old 迁移到 new": ( + "Migrated WeChat ClawBot login cache from old to new" + ), + "搜索完成,共 3 个资源": "Search completed, 3 resources", + "正在搜索关键字,已完成 1 / 6 个请求 ...": ( + "Searching 关键字, completed 1/6 requests ..." + ), + "正在搜索字幕关键字,已完成 1 / 6 个请求 ...": ( + "Searching subtitles 关键字, completed 1/6 requests ..." + ), + "未开启任何支持字幕搜索的有效站点,无法搜索字幕": ( + "No valid subtitle-search site is enabled, unable to search subtitles" + ), + "用户名或密码错误": "Incorrect username or password", + "工具 'query_schedulers' 未找到": "Tool 'query_schedulers' was not found", + "插件 test 不存在或未加载": "Plugin test does not exist or is not loaded", + "站点 1 不存在": "Site 1 does not exist", + "智能助手未启用,请先在系统设置中开启。": ( + "The assistant is not enabled. Enable it in system settings first." + ), + "智能助手执行失败: timeout": "Assistant execution failed: timeout", + } + + for message, expected in samples.items(): + assert LocaleHelper.translate_text(message, locale="en-US") == expected + + +def test_response_auto_fills_message_i18n_from_locale_context(): + """通用 Response 应根据请求语言上下文自动补充多语言消息。""" + token = LocaleHelper.set_current_locale("en-US") + try: + response = Response(success=False, message="模块不支持测试") + finally: + LocaleHelper.reset_current_locale(token) + + assert response.message == "模块不支持测试" + assert response.message_i18n == "Module does not support testing" + + +def test_http_exception_handler_adds_detail_i18n_from_locale_context(): + """HTTPException 响应应补充多语言 detail 字段。""" + token = LocaleHelper.set_current_locale("en-US") + try: + response = asyncio.run( + localized_http_exception_handler( + None, + HTTPException(status_code=401, detail="用户名或密码错误"), + ) + ) + finally: + LocaleHelper.reset_current_locale(token) + + payload = json.loads(response.body) + assert payload["detail"] == "用户名或密码错误" + assert payload["detail_i18n"] == "Incorrect username or password" + + +def test_progress_helper_get_adds_i18n_fields_without_mutating_cache(): + """通用进度字典应返回展示字段翻译,同时保留缓存中的原始中文。""" + progress = ProgressHelper("__test_i18n_progress") + progress.start() + progress.update( + text="开始同步媒体服务器,共 2 个 ...", + data={"error": "后台服务不存在"}, + ) + + detail = progress.get(locale="en-US") + assert detail is not None + assert detail["text"] == "开始同步媒体服务器,共 2 个 ..." + assert detail["text_i18n"] == "Starting media server sync, 2 servers ..." + assert detail["data"]["error"] == "后台服务不存在" + assert detail["data"]["error_i18n"] == "Background service does not exist" + + detail["data"]["error_i18n"] = "mutated" + second_detail = progress.get(locale="en-US") + assert second_detail is not None + assert second_detail["data"]["error_i18n"] == "Background service does not exist" + + +def test_schedule_info_auto_fills_i18n_display_fields(): + """后台服务数据应补充前端展示所需的多语言字段。""" + token = LocaleHelper.set_current_locale("en-US") + try: + schedule = ScheduleInfo( + id="mediaserver_sync", + name="同步媒体服务器", + provider="[系统]", + status="等待", + next_run="2小时3分钟", + progress_text="开始同步媒体服务器,共 2 个 ...", + progress_detail=ScheduleProgress( + id="mediaserver_sync", + name="同步媒体服务器", + provider="[系统]", + text="媒体服务器 Emby 无可同步媒体库", + error="后台服务不存在", + ), + ) + finally: + LocaleHelper.reset_current_locale(token) + + assert schedule.name_i18n == "Sync Media Servers" + assert schedule.provider_i18n == "[System]" + assert schedule.status_i18n == "Waiting" + assert schedule.next_run_i18n == "2h 3m" + assert schedule.progress_text_i18n == "Starting media server sync, 2 servers ..." + assert schedule.progress_detail.text_i18n == "Media server Emby has no libraries to sync" + assert schedule.progress_detail.error_i18n == "Background service does not exist" + + +def test_api_endpoint_literal_messages_have_english_translations(): + """接口直接返回的中文 message 应有英文翻译,避免前端切英文后回退中文。""" + untranslated = [] + for path in Path("app/api/endpoints").glob("*.py"): + tree = ast.parse(path.read_text()) + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + for keyword in node.keywords: + if keyword.arg != "message": + continue + value = keyword.value + if not isinstance(value, ast.Constant) or not isinstance(value.value, str): + continue + message = value.value + if not any("\u4e00" <= char <= "\u9fff" for char in message): + continue + translated = LocaleHelper.translate_text(message, locale="en-US") + if translated == message: + untranslated.append(f"{path}:{value.lineno}:{message}") + + assert untranslated == [] + + +def test_api_endpoint_dynamic_messages_have_english_translations(): + """接口动态拼接的中文 message 应由模板翻译覆盖。""" + untranslated = [] + for path in Path("app/api/endpoints").glob("*.py"): + tree = ast.parse(path.read_text()) + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + for keyword in node.keywords: + if keyword.arg != "message": + continue + value = keyword.value + if not isinstance(value, ast.JoinedStr): + continue + parts = [] + has_chinese = False + index = 0 + for item in value.values: + if isinstance(item, ast.Constant) and isinstance(item.value, str): + parts.append(item.value) + has_chinese = has_chinese or _has_chinese(item.value) + else: + index += 1 + parts.append(f"样例{index}") + if not has_chinese: + continue + message = "".join(parts) + translated = LocaleHelper.translate_text(message, locale="en-US") + if translated == message: + untranslated.append(f"{path}:{value.lineno}:{message}") + + assert untranslated == [] + + +def test_api_endpoint_message_expressions_have_english_translations(): + """接口 message 表达式中的中文分支应有英文翻译。""" + untranslated = [] + for path in Path("app/api/endpoints").glob("*.py"): + tree = ast.parse(path.read_text()) + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + if ast.unparse(node.func) not in {"schemas.Response", "Response"}: + continue + for keyword in node.keywords: + if keyword.arg != "message": + continue + for message in _sample_message_expression(keyword.value): + if not _has_chinese(message): + continue + translated = LocaleHelper.translate_text(message, locale="en-US") + if translated == message: + untranslated.append(f"{path}:{keyword.value.lineno}:{message}") + + assert untranslated == [] + + +def test_api_endpoint_http_exception_details_have_english_translations(): + """HTTPException 的中文 detail 应有英文翻译。""" + untranslated = [] + for path in Path("app/api/endpoints").glob("*.py"): + tree = ast.parse(path.read_text()) + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + if not ast.unparse(node.func).endswith("HTTPException"): + continue + for keyword in node.keywords: + if keyword.arg != "detail": + continue + for message in _sample_message_expression(keyword.value): + if not _has_chinese(message): + continue + translated = LocaleHelper.translate_text(message, locale="en-US") + if translated == message: + untranslated.append(f"{path}:{keyword.value.lineno}:{message}") + + assert untranslated == [] diff --git a/tests/test_system_i18n.py b/tests/test_system_i18n.py new file mode 100644 index 000000000..429833a63 --- /dev/null +++ b/tests/test_system_i18n.py @@ -0,0 +1,111 @@ +from types import ModuleType +from unittest.mock import patch + +from app.helper.locale import LocaleHelper +from app.testing import stub_modules + + +def _stub(name: str, **attrs) -> tuple: + """构造带指定属性的占位模块,返回给 stub_modules 使用。""" + module = ModuleType(name) + for key, value in attrs.items(): + setattr(module, key, value) + return name, module + + +class _Dummy: + """隔离 system endpoint 导入期重依赖的占位对象。""" + + def __init__(self, *args, **kwargs): + pass + + def __getattr__(self, _name): + return lambda *args, **kwargs: None + + +class _FakeDoubanModule: + """构造带中文名称的模块类,模拟真实 DoubanModule。""" + + @staticmethod + def get_name() -> str: + """获取模块中文名称""" + return "豆瓣" + + +class _FakeModuleManager: + """提供 system 模块接口测试所需的最小模块管理器。""" + + def get_modules(self) -> dict: + """返回模块字典""" + return {"DoubanModule": _FakeDoubanModule} + + def test(self, moduleid: str) -> tuple[bool, str]: + """返回模块测试结果""" + return False, "模块不支持测试" + + +_STUB_MODULES = dict([ + _stub("pillow_avif"), + _stub("aiofiles"), + _stub("psutil"), + _stub("app.helper.sites", SitesHelper=_Dummy), + _stub("app.chain.media", MediaChain=_Dummy), + _stub("app.chain.mediaserver", MediaServerChain=_Dummy), + _stub("app.chain.search", SearchChain=_Dummy), + _stub("app.chain.system", SystemChain=_Dummy), + _stub("app.core.event", eventmanager=_Dummy(), Event=_Dummy, EventManager=_Dummy), + _stub("app.core.metainfo", MetaInfo=_Dummy), + _stub("app.core.module", ModuleManager=_Dummy), + _stub("app.core.security", verify_apitoken=_Dummy, verify_resource_token=_Dummy, verify_token=_Dummy), + _stub("app.db.models", User=_Dummy), + _stub("app.db.systemconfig_oper", SystemConfigOper=_Dummy), + _stub("app.db.user_oper", get_current_active_superuser=_Dummy, + get_current_active_superuser_async=_Dummy, get_current_active_user_async=_Dummy), + _stub("app.helper.image", ImageHelper=_Dummy), + _stub("app.helper.mediaserver", MediaServerHelper=_Dummy), + _stub("app.helper.message", MessageHelper=_Dummy), + _stub("app.helper.progress", ProgressHelper=_Dummy), + _stub("app.helper.rule", RuleHelper=_Dummy), + _stub("app.helper.server", MoviePilotServerHelper=_Dummy), + _stub("app.helper.system", SystemHelper=_Dummy), + _stub("app.log", logger=_Dummy(), log_settings=_Dummy(), + LogConfigModel=type("LogConfigModel", (), {})), + _stub("app.scheduler", Scheduler=_Dummy), + _stub("app.utils.crypto", HashUtils=_Dummy), + _stub("app.utils.http", RequestUtils=_Dummy, AsyncRequestUtils=_Dummy), + _stub("version", APP_VERSION="test"), +]) + + +with stub_modules(_STUB_MODULES): + from app.api.endpoints import system as system_endpoint + + +def test_system_modulelist_keeps_chinese_name_and_adds_i18n_name(): + """模块列表接口应保留旧中文字段,并提供前端可用的多语言字段。""" + token = LocaleHelper.set_current_locale("en-US") + with patch.object(system_endpoint, "ModuleManager", return_value=_FakeModuleManager()): + try: + response = system_endpoint.modulelist(_="token") + finally: + LocaleHelper.reset_current_locale(token) + + module = response.data["modules"][0] + assert module["id"] == "DoubanModule" + assert module["name"] == "豆瓣" + assert module["name_i18n"] == "Douban" + assert module["name_key"] == "system.modules.DoubanModule.name" + + +def test_system_moduletest_keeps_chinese_message_and_adds_i18n_message(): + """模块测试接口应保留旧中文 message,并在顶层提供多语言 message_i18n。""" + token = LocaleHelper.set_current_locale("en-US") + with patch.object(system_endpoint, "ModuleManager", return_value=_FakeModuleManager()): + try: + response = system_endpoint.moduletest("DoubanModule", _="token") + finally: + LocaleHelper.reset_current_locale(token) + + assert response.success is False + assert response.message == "模块不支持测试" + assert response.message_i18n == "Module does not support testing"