Files

229 lines
8.2 KiB
Python

"""
Internationalization (i18n) loader for ComfyUI Runtime Diagnostics.
Translation data lives in i18n_data/<language>.json so translators can edit
localized UI text and suggestions without touching Python source.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Dict, Optional
# ============================================================================
# CRITICAL: Default Language Configuration
# ============================================================================
# WARNING: DO NOT change this default to any language other than "en".
# This is the fallback language used before user settings are applied and it
# matches frontend DEFAULTS.LANGUAGE in web/doctor.js.
_current_language = "en"
SUPPORTED_LANGUAGES = ['en', 'zh_TW', 'zh_CN', 'ja', 'de', 'fr', 'it', 'es', 'ko']
ERROR_KEYS = {'TYPE_MISMATCH': 'type_mismatch',
'DIMENSION_MISMATCH': 'dimension_mismatch',
'OOM': 'oom',
'MATRIX_MULT': 'matrix_mult',
'DEVICE_TYPE': 'device_type',
'MISSING_MODULE': 'missing_module',
'ASSERTION': 'assertion',
'KEY_ERROR': 'key_error',
'ATTRIBUTE_ERROR': 'attribute_error',
'SHAPE_MISMATCH': 'shape_mismatch',
'FILE_NOT_FOUND': 'file_not_found',
'TORCH_OOM': 'torch_oom',
'AUTOGRAD': 'autograd',
'SAFETENSORS_ERROR': 'safetensors_error',
'CUDNN_ERROR': 'cudnn_error',
'MISSING_INSIGHTFACE': 'missing_insightface',
'MODEL_VAE_MISMATCH': 'model_vae_mismatch',
'MPS_OOM': 'mps_oom',
'INVALID_PROMPT': 'invalid_prompt',
'VALIDATION_ERROR': 'validation_error',
'TENSOR_NAN_INF': 'tensor_nan_inf',
'META_TENSOR': 'meta_tensor',
'MISSING_INPUT': 'missing_input',
'CONTROLNET_MODEL_NOT_FOUND': 'controlnet_model_not_found',
'CONTROLNET_PREPROCESSOR_FAILED': 'controlnet_preprocessor_failed',
'CONTROLNET_SIZE_MISMATCH': 'controlnet_size_mismatch',
'CONTROLNET_UNSUPPORTED_MODEL': 'controlnet_unsupported_model',
'CONTROLNET_INVALID_STRENGTH': 'controlnet_invalid_strength',
'CONTROLNET_MISSING_PREPROCESSOR': 'controlnet_missing_preprocessor',
'CONTROLNET_CHANNEL_MISMATCH': 'controlnet_channel_mismatch',
'CONTROLNET_DEVICE_MISMATCH': 'controlnet_device_mismatch',
'LORA_NOT_FOUND': 'lora_not_found',
'LORA_INCOMPATIBLE': 'lora_incompatible',
'LORA_CORRUPTED': 'lora_corrupted',
'LORA_STRENGTH_INVALID': 'lora_strength_invalid',
'LORA_OOM': 'lora_oom',
'LORA_KEY_MISMATCH': 'lora_key_mismatch',
'VAE_DECODE_FAILED': 'vae_decode_failed',
'VAE_ENCODE_FAILED': 'vae_encode_failed',
'VAE_TILING_ERROR': 'vae_tiling_error',
'VAE_FP16_ISSUE': 'vae_fp16_issue',
'VAE_BATCH_SIZE_ERROR': 'vae_batch_size_error',
'ANIMATEDIFF_MODEL_NOT_FOUND': 'animatediff_model_not_found',
'ANIMATEDIFF_FRAME_MISMATCH': 'animatediff_frame_mismatch',
'ANIMATEDIFF_CONTEXT_ERROR': 'animatediff_context_error',
'ANIMATEDIFF_OOM': 'animatediff_oom',
'IPADAPTER_MODEL_NOT_FOUND': 'ipadapter_model_not_found',
'IPADAPTER_IMAGE_ENCODING_FAILED': 'ipadapter_image_encoding_failed',
'IPADAPTER_INCOMPATIBLE': 'ipadapter_incompatible',
'IPADAPTER_WEIGHT_ERROR': 'ipadapter_weight_error',
'FACERESTORE_MODEL_NOT_FOUND': 'facerestore_model_not_found',
'FACERESTORE_DETECTION_FAILED': 'facerestore_detection_failed',
'FACERESTORE_OOM': 'facerestore_oom',
'CHECKPOINT_CORRUPTED': 'checkpoint_corrupted',
'IMAGE_FORMAT_UNSUPPORTED': 'image_format_unsupported',
'SAMPLER_NOT_FOUND': 'sampler_not_found',
'SCHEDULER_ERROR': 'scheduler_error',
'CLIP_ENCODING_ERROR': 'clip_encoding_error',
'VALUE_NOT_IN_LIST': 'value_not_in_list'}
I18N_DATA_DIR = Path(__file__).resolve().with_name("i18n_data")
SUGGESTION_PREFIX = "\U0001f4a1 SUGGESTION: "
class I18nDataError(RuntimeError):
"""Raised when translation data is missing or malformed."""
def _require_string_mapping(value: Any, *, field: str, language: str) -> Dict[str, str]:
if not isinstance(value, dict):
raise I18nDataError(f"{language}: {field} must be an object")
result: Dict[str, str] = {}
for key, text in value.items():
if not isinstance(key, str):
raise I18nDataError(f"{language}: {field} contains a non-string key")
if not isinstance(text, str):
raise I18nDataError(f"{language}: {field}.{key} must be a string")
result[key] = text
return result
def _load_language_file(path: Path, expected_language: str) -> Dict[str, Dict[str, str]]:
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise I18nDataError(f"{expected_language}: invalid JSON in {path.name}: {exc}") from exc
except OSError as exc:
raise I18nDataError(f"{expected_language}: cannot read {path.name}: {exc}") from exc
if not isinstance(raw, dict):
raise I18nDataError(f"{expected_language}: language file root must be an object")
if raw.get("language") != expected_language:
raise I18nDataError(
f"{expected_language}: language marker mismatch in {path.name} "
f"(got {raw.get('language')!r})"
)
return {
"ui_text": _require_string_mapping(raw.get("ui_text"), field="ui_text", language=expected_language),
"suggestions": _require_string_mapping(raw.get("suggestions"), field="suggestions", language=expected_language),
}
def _validate_loaded_data(ui_text: Dict[str, Dict[str, str]], suggestions: Dict[str, Dict[str, str]]) -> None:
missing_ui_languages = [lang for lang in SUPPORTED_LANGUAGES if lang not in ui_text]
missing_suggestion_languages = [lang for lang in SUPPORTED_LANGUAGES if lang not in suggestions]
if missing_ui_languages or missing_suggestion_languages:
raise I18nDataError(
"Missing i18n languages: "
f"ui_text={missing_ui_languages}, suggestions={missing_suggestion_languages}"
)
required_suggestion_keys = set(ERROR_KEYS.values())
for lang in SUPPORTED_LANGUAGES:
missing = sorted(required_suggestion_keys - set(suggestions[lang]))
if missing:
raise I18nDataError(f"{lang}: missing suggestion translations: {', '.join(missing)}")
def _load_translation_data(data_dir: Path = I18N_DATA_DIR) -> tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, str]]]:
ui_text: Dict[str, Dict[str, str]] = {}
suggestions: Dict[str, Dict[str, str]] = {}
for lang in SUPPORTED_LANGUAGES:
payload = _load_language_file(data_dir / f"{lang}.json", lang)
ui_text[lang] = payload["ui_text"]
suggestions[lang] = payload["suggestions"]
_validate_loaded_data(ui_text, suggestions)
return ui_text, suggestions
UI_TEXT, SUGGESTIONS = _load_translation_data()
def set_language(lang: str) -> bool:
"""
Set the current language for suggestions.
Args:
lang: Language code (e.g., 'en', 'zh_TW', 'zh_CN', 'ja')
Returns:
True if language was set successfully, False otherwise.
"""
global _current_language
if lang in SUPPORTED_LANGUAGES:
_current_language = lang
return True
return False
def get_language() -> str:
"""Get the current language setting."""
return _current_language
def get_suggestion(key: str, *args) -> Optional[str]:
"""
Get a localized suggestion by key.
Args:
key: The suggestion key (from ERROR_KEYS values)
*args: Format arguments for the suggestion template
Returns:
Formatted localized suggestion, or None if key not found.
"""
lang_dict = SUGGESTIONS.get(_current_language, SUGGESTIONS["en"])
template = lang_dict.get(key)
if template is None:
template = SUGGESTIONS["en"].get(key)
if template is None:
return None
try:
if args:
return SUGGESTION_PREFIX + template.format(*args)
return SUGGESTION_PREFIX + template
except (IndexError, KeyError):
return SUGGESTION_PREFIX + template
def get_ui_text(key: str, lang: Optional[str] = None) -> str:
"""
Get localized UI text by key.
Args:
key: The UI text key (from UI_TEXT values)
lang: Optional language override (defaults to current language)
Returns:
Localized UI text, or English fallback if key not found.
"""
target_lang = lang if lang else _current_language
lang_dict = UI_TEXT.get(target_lang, UI_TEXT["en"])
text = lang_dict.get(key)
if text is None:
text = UI_TEXT["en"].get(key, f"[Missing: {key}]")
return text