-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathi18n.py
More file actions
228 lines (184 loc) · 8.25 KB
/
Copy pathi18n.py
File metadata and controls
228 lines (184 loc) · 8.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
"""
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