truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- truthound_dashboard/api/alerts.py +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/METADATA +147 -23
- truthound_dashboard-1.4.1.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
- truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
- truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"""Base classes for report localization.
|
|
2
|
+
|
|
3
|
+
This module provides the core localization infrastructure including:
|
|
4
|
+
- SupportedLocale enum for supported languages
|
|
5
|
+
- ReportLocalizer class for accessing localized strings
|
|
6
|
+
- LocalizationRegistry for managing custom catalogs
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Callable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SupportedLocale(str, Enum):
|
|
17
|
+
"""Supported languages for report generation.
|
|
18
|
+
|
|
19
|
+
Based on truthound documentation:
|
|
20
|
+
- 7 languages for validator error messages
|
|
21
|
+
- 15 languages for reports (extended set)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Core languages (validator error messages)
|
|
25
|
+
ENGLISH = "en"
|
|
26
|
+
KOREAN = "ko"
|
|
27
|
+
JAPANESE = "ja"
|
|
28
|
+
CHINESE = "zh"
|
|
29
|
+
GERMAN = "de"
|
|
30
|
+
FRENCH = "fr"
|
|
31
|
+
SPANISH = "es"
|
|
32
|
+
|
|
33
|
+
# Extended languages (reports only)
|
|
34
|
+
PORTUGUESE = "pt"
|
|
35
|
+
ITALIAN = "it"
|
|
36
|
+
RUSSIAN = "ru"
|
|
37
|
+
ARABIC = "ar"
|
|
38
|
+
THAI = "th"
|
|
39
|
+
VIETNAMESE = "vi"
|
|
40
|
+
INDONESIAN = "id"
|
|
41
|
+
TURKISH = "tr"
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_string(cls, value: str) -> SupportedLocale:
|
|
45
|
+
"""Parse locale from string.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
value: Locale code (case-insensitive, e.g., 'en', 'ko', 'EN-US')
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
SupportedLocale enum value.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If locale is not supported.
|
|
55
|
+
"""
|
|
56
|
+
# Normalize: take first part of locale code (e.g., 'en-US' -> 'en')
|
|
57
|
+
normalized = value.lower().split("-")[0].split("_")[0]
|
|
58
|
+
|
|
59
|
+
for locale in cls:
|
|
60
|
+
if locale.value == normalized:
|
|
61
|
+
return locale
|
|
62
|
+
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Unsupported locale: {value}. "
|
|
65
|
+
f"Supported locales: {[loc.value for loc in cls]}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def native_name(self) -> str:
|
|
70
|
+
"""Get the native name of this locale."""
|
|
71
|
+
native_names = {
|
|
72
|
+
SupportedLocale.ENGLISH: "English",
|
|
73
|
+
SupportedLocale.KOREAN: "한국어",
|
|
74
|
+
SupportedLocale.JAPANESE: "日本語",
|
|
75
|
+
SupportedLocale.CHINESE: "中文",
|
|
76
|
+
SupportedLocale.GERMAN: "Deutsch",
|
|
77
|
+
SupportedLocale.FRENCH: "Français",
|
|
78
|
+
SupportedLocale.SPANISH: "Español",
|
|
79
|
+
SupportedLocale.PORTUGUESE: "Português",
|
|
80
|
+
SupportedLocale.ITALIAN: "Italiano",
|
|
81
|
+
SupportedLocale.RUSSIAN: "Русский",
|
|
82
|
+
SupportedLocale.ARABIC: "العربية",
|
|
83
|
+
SupportedLocale.THAI: "ไทย",
|
|
84
|
+
SupportedLocale.VIETNAMESE: "Tiếng Việt",
|
|
85
|
+
SupportedLocale.INDONESIAN: "Bahasa Indonesia",
|
|
86
|
+
SupportedLocale.TURKISH: "Türkçe",
|
|
87
|
+
}
|
|
88
|
+
return native_names.get(self, self.value)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def english_name(self) -> str:
|
|
92
|
+
"""Get the English name of this locale."""
|
|
93
|
+
english_names = {
|
|
94
|
+
SupportedLocale.ENGLISH: "English",
|
|
95
|
+
SupportedLocale.KOREAN: "Korean",
|
|
96
|
+
SupportedLocale.JAPANESE: "Japanese",
|
|
97
|
+
SupportedLocale.CHINESE: "Chinese",
|
|
98
|
+
SupportedLocale.GERMAN: "German",
|
|
99
|
+
SupportedLocale.FRENCH: "French",
|
|
100
|
+
SupportedLocale.SPANISH: "Spanish",
|
|
101
|
+
SupportedLocale.PORTUGUESE: "Portuguese",
|
|
102
|
+
SupportedLocale.ITALIAN: "Italian",
|
|
103
|
+
SupportedLocale.RUSSIAN: "Russian",
|
|
104
|
+
SupportedLocale.ARABIC: "Arabic",
|
|
105
|
+
SupportedLocale.THAI: "Thai",
|
|
106
|
+
SupportedLocale.VIETNAMESE: "Vietnamese",
|
|
107
|
+
SupportedLocale.INDONESIAN: "Indonesian",
|
|
108
|
+
SupportedLocale.TURKISH: "Turkish",
|
|
109
|
+
}
|
|
110
|
+
return english_names.get(self, self.value)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def flag_emoji(self) -> str:
|
|
114
|
+
"""Get the flag emoji for this locale."""
|
|
115
|
+
flags = {
|
|
116
|
+
SupportedLocale.ENGLISH: "🇺🇸",
|
|
117
|
+
SupportedLocale.KOREAN: "🇰🇷",
|
|
118
|
+
SupportedLocale.JAPANESE: "🇯🇵",
|
|
119
|
+
SupportedLocale.CHINESE: "🇨🇳",
|
|
120
|
+
SupportedLocale.GERMAN: "🇩🇪",
|
|
121
|
+
SupportedLocale.FRENCH: "🇫🇷",
|
|
122
|
+
SupportedLocale.SPANISH: "🇪🇸",
|
|
123
|
+
SupportedLocale.PORTUGUESE: "🇧🇷",
|
|
124
|
+
SupportedLocale.ITALIAN: "🇮🇹",
|
|
125
|
+
SupportedLocale.RUSSIAN: "🇷🇺",
|
|
126
|
+
SupportedLocale.ARABIC: "🇸🇦",
|
|
127
|
+
SupportedLocale.THAI: "🇹🇭",
|
|
128
|
+
SupportedLocale.VIETNAMESE: "🇻🇳",
|
|
129
|
+
SupportedLocale.INDONESIAN: "🇮🇩",
|
|
130
|
+
SupportedLocale.TURKISH: "🇹🇷",
|
|
131
|
+
}
|
|
132
|
+
return flags.get(self, "🌐")
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def is_rtl(self) -> bool:
|
|
136
|
+
"""Check if this locale uses right-to-left text direction."""
|
|
137
|
+
return self == SupportedLocale.ARABIC
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class LocaleCatalog:
|
|
142
|
+
"""A collection of localized strings for a specific locale.
|
|
143
|
+
|
|
144
|
+
Attributes:
|
|
145
|
+
locale: The locale this catalog is for.
|
|
146
|
+
messages: Dictionary of message keys to localized strings.
|
|
147
|
+
plurals: Dictionary of plural rules (optional).
|
|
148
|
+
formatters: Dictionary of custom formatters (optional).
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
locale: SupportedLocale
|
|
152
|
+
messages: dict[str, str]
|
|
153
|
+
plurals: dict[str, Callable[[int], str]] = field(default_factory=dict)
|
|
154
|
+
formatters: dict[str, Callable[[Any], str]] = field(default_factory=dict)
|
|
155
|
+
|
|
156
|
+
def get(self, key: str, default: str | None = None) -> str:
|
|
157
|
+
"""Get a localized message by key.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
key: Message key (dot-notation supported, e.g., 'report.title')
|
|
161
|
+
default: Default value if key not found.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Localized string or default.
|
|
165
|
+
"""
|
|
166
|
+
return self.messages.get(key, default or key)
|
|
167
|
+
|
|
168
|
+
def format(self, key: str, **kwargs: Any) -> str:
|
|
169
|
+
"""Get and format a localized message.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
key: Message key.
|
|
173
|
+
**kwargs: Format arguments.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Formatted localized string.
|
|
177
|
+
"""
|
|
178
|
+
message = self.get(key)
|
|
179
|
+
try:
|
|
180
|
+
return message.format(**kwargs)
|
|
181
|
+
except (KeyError, IndexError):
|
|
182
|
+
return message
|
|
183
|
+
|
|
184
|
+
def plural(self, key: str, count: int, **kwargs: Any) -> str:
|
|
185
|
+
"""Get a pluralized message.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
key: Message key base (will look for key.zero, key.one, key.other)
|
|
189
|
+
count: Count for pluralization.
|
|
190
|
+
**kwargs: Additional format arguments.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Pluralized and formatted string.
|
|
194
|
+
"""
|
|
195
|
+
if key in self.plurals:
|
|
196
|
+
message = self.plurals[key](count)
|
|
197
|
+
else:
|
|
198
|
+
# Default plural rules
|
|
199
|
+
if count == 0 and f"{key}.zero" in self.messages:
|
|
200
|
+
message = self.messages[f"{key}.zero"]
|
|
201
|
+
elif count == 1 and f"{key}.one" in self.messages:
|
|
202
|
+
message = self.messages[f"{key}.one"]
|
|
203
|
+
else:
|
|
204
|
+
message = self.messages.get(f"{key}.other", self.get(key))
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
return message.format(count=count, **kwargs)
|
|
208
|
+
except (KeyError, IndexError):
|
|
209
|
+
return message
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class ReportLocalizer:
|
|
213
|
+
"""Main localization interface for report generation.
|
|
214
|
+
|
|
215
|
+
Provides access to localized strings with fallback to English.
|
|
216
|
+
|
|
217
|
+
Example:
|
|
218
|
+
localizer = ReportLocalizer(SupportedLocale.KOREAN)
|
|
219
|
+
title = localizer.t("report.title") # "검증 리포트"
|
|
220
|
+
issues = localizer.plural("report.issues", 5) # "5개 이슈"
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
def __init__(
|
|
224
|
+
self,
|
|
225
|
+
locale: SupportedLocale,
|
|
226
|
+
catalog: LocaleCatalog,
|
|
227
|
+
fallback_catalog: LocaleCatalog | None = None,
|
|
228
|
+
) -> None:
|
|
229
|
+
"""Initialize the localizer.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
locale: Target locale.
|
|
233
|
+
catalog: Primary catalog for this locale.
|
|
234
|
+
fallback_catalog: Fallback catalog (typically English).
|
|
235
|
+
"""
|
|
236
|
+
self.locale = locale
|
|
237
|
+
self._catalog = catalog
|
|
238
|
+
self._fallback = fallback_catalog
|
|
239
|
+
|
|
240
|
+
def t(self, key: str, default: str | None = None) -> str:
|
|
241
|
+
"""Translate a key to localized string.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
key: Message key.
|
|
245
|
+
default: Default value if not found.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Localized string.
|
|
249
|
+
"""
|
|
250
|
+
result = self._catalog.get(key)
|
|
251
|
+
if result == key and self._fallback:
|
|
252
|
+
result = self._fallback.get(key, default)
|
|
253
|
+
return result if result != key else (default or key)
|
|
254
|
+
|
|
255
|
+
def get(self, key: str, default: str | None = None) -> str:
|
|
256
|
+
"""Alias for t() method."""
|
|
257
|
+
return self.t(key, default)
|
|
258
|
+
|
|
259
|
+
def format(self, key: str, **kwargs: Any) -> str:
|
|
260
|
+
"""Get and format a localized message.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
key: Message key.
|
|
264
|
+
**kwargs: Format arguments.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Formatted localized string.
|
|
268
|
+
"""
|
|
269
|
+
message = self.t(key)
|
|
270
|
+
try:
|
|
271
|
+
return message.format(**kwargs)
|
|
272
|
+
except (KeyError, IndexError):
|
|
273
|
+
return message
|
|
274
|
+
|
|
275
|
+
def plural(self, key: str, count: int, **kwargs: Any) -> str:
|
|
276
|
+
"""Get a pluralized message.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
key: Message key base.
|
|
280
|
+
count: Count for pluralization.
|
|
281
|
+
**kwargs: Additional format arguments.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Pluralized and formatted string.
|
|
285
|
+
"""
|
|
286
|
+
return self._catalog.plural(key, count, **kwargs)
|
|
287
|
+
|
|
288
|
+
def format_number(self, value: int | float) -> str:
|
|
289
|
+
"""Format a number according to locale conventions.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
value: Number to format.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Formatted number string.
|
|
296
|
+
"""
|
|
297
|
+
# Use locale-specific formatting
|
|
298
|
+
if self.locale in (SupportedLocale.GERMAN, SupportedLocale.FRENCH,
|
|
299
|
+
SupportedLocale.ITALIAN, SupportedLocale.SPANISH,
|
|
300
|
+
SupportedLocale.PORTUGUESE, SupportedLocale.RUSSIAN,
|
|
301
|
+
SupportedLocale.TURKISH):
|
|
302
|
+
# European style: 1.234.567,89
|
|
303
|
+
if isinstance(value, float):
|
|
304
|
+
int_part = int(value)
|
|
305
|
+
dec_part = f"{value - int_part:.2f}"[2:]
|
|
306
|
+
return f"{int_part:,}".replace(",", ".") + "," + dec_part
|
|
307
|
+
return f"{value:,}".replace(",", ".")
|
|
308
|
+
else:
|
|
309
|
+
# Standard style: 1,234,567.89
|
|
310
|
+
if isinstance(value, float):
|
|
311
|
+
return f"{value:,.2f}"
|
|
312
|
+
return f"{value:,}"
|
|
313
|
+
|
|
314
|
+
def format_percentage(self, value: float) -> str:
|
|
315
|
+
"""Format a percentage according to locale conventions.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
value: Percentage value (0-100).
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Formatted percentage string.
|
|
322
|
+
"""
|
|
323
|
+
return f"{value:.1f}%"
|
|
324
|
+
|
|
325
|
+
def format_date(self, value: Any) -> str:
|
|
326
|
+
"""Format a date according to locale conventions.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
value: Date/datetime object.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Formatted date string.
|
|
333
|
+
"""
|
|
334
|
+
from datetime import datetime
|
|
335
|
+
|
|
336
|
+
if isinstance(value, datetime):
|
|
337
|
+
# Use locale-appropriate format
|
|
338
|
+
if self.locale in (SupportedLocale.ENGLISH,):
|
|
339
|
+
return value.strftime("%m/%d/%Y %H:%M:%S")
|
|
340
|
+
elif self.locale in (SupportedLocale.GERMAN, SupportedLocale.FRENCH,
|
|
341
|
+
SupportedLocale.ITALIAN, SupportedLocale.SPANISH,
|
|
342
|
+
SupportedLocale.PORTUGUESE, SupportedLocale.RUSSIAN):
|
|
343
|
+
return value.strftime("%d/%m/%Y %H:%M:%S")
|
|
344
|
+
elif self.locale in (SupportedLocale.JAPANESE, SupportedLocale.CHINESE,
|
|
345
|
+
SupportedLocale.KOREAN):
|
|
346
|
+
return value.strftime("%Y/%m/%d %H:%M:%S")
|
|
347
|
+
else:
|
|
348
|
+
return value.strftime("%Y-%m-%d %H:%M:%S")
|
|
349
|
+
return str(value)
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def text_direction(self) -> str:
|
|
353
|
+
"""Get CSS text direction for this locale."""
|
|
354
|
+
return "rtl" if self.locale.is_rtl else "ltr"
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class LocalizationRegistry:
|
|
358
|
+
"""Registry for managing locale catalogs.
|
|
359
|
+
|
|
360
|
+
Supports runtime registration of custom catalogs and
|
|
361
|
+
lazy loading of built-in catalogs.
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
_instance: LocalizationRegistry | None = None
|
|
365
|
+
_catalogs: dict[SupportedLocale, LocaleCatalog] = {}
|
|
366
|
+
|
|
367
|
+
def __new__(cls) -> LocalizationRegistry:
|
|
368
|
+
"""Singleton pattern."""
|
|
369
|
+
if cls._instance is None:
|
|
370
|
+
cls._instance = super().__new__(cls)
|
|
371
|
+
return cls._instance
|
|
372
|
+
|
|
373
|
+
def register(self, catalog: LocaleCatalog) -> None:
|
|
374
|
+
"""Register a catalog for a locale.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
catalog: LocaleCatalog to register.
|
|
378
|
+
"""
|
|
379
|
+
self._catalogs[catalog.locale] = catalog
|
|
380
|
+
|
|
381
|
+
def get_catalog(self, locale: SupportedLocale) -> LocaleCatalog | None:
|
|
382
|
+
"""Get the catalog for a locale.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
locale: Target locale.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
LocaleCatalog or None if not registered.
|
|
389
|
+
"""
|
|
390
|
+
return self._catalogs.get(locale)
|
|
391
|
+
|
|
392
|
+
def get_or_load(self, locale: SupportedLocale) -> LocaleCatalog:
|
|
393
|
+
"""Get or lazily load a catalog for a locale.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
locale: Target locale.
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
LocaleCatalog for the locale.
|
|
400
|
+
"""
|
|
401
|
+
if locale not in self._catalogs:
|
|
402
|
+
self._load_builtin_catalog(locale)
|
|
403
|
+
return self._catalogs.get(locale, self._get_english_fallback())
|
|
404
|
+
|
|
405
|
+
def _load_builtin_catalog(self, locale: SupportedLocale) -> None:
|
|
406
|
+
"""Load a built-in catalog."""
|
|
407
|
+
from . import catalogs
|
|
408
|
+
|
|
409
|
+
catalog_map = {
|
|
410
|
+
SupportedLocale.ENGLISH: catalogs.ENGLISH_CATALOG,
|
|
411
|
+
SupportedLocale.KOREAN: catalogs.KOREAN_CATALOG,
|
|
412
|
+
SupportedLocale.JAPANESE: catalogs.JAPANESE_CATALOG,
|
|
413
|
+
SupportedLocale.CHINESE: catalogs.CHINESE_CATALOG,
|
|
414
|
+
SupportedLocale.GERMAN: catalogs.GERMAN_CATALOG,
|
|
415
|
+
SupportedLocale.FRENCH: catalogs.FRENCH_CATALOG,
|
|
416
|
+
SupportedLocale.SPANISH: catalogs.SPANISH_CATALOG,
|
|
417
|
+
SupportedLocale.PORTUGUESE: catalogs.PORTUGUESE_CATALOG,
|
|
418
|
+
SupportedLocale.ITALIAN: catalogs.ITALIAN_CATALOG,
|
|
419
|
+
SupportedLocale.RUSSIAN: catalogs.RUSSIAN_CATALOG,
|
|
420
|
+
SupportedLocale.ARABIC: catalogs.ARABIC_CATALOG,
|
|
421
|
+
SupportedLocale.THAI: catalogs.THAI_CATALOG,
|
|
422
|
+
SupportedLocale.VIETNAMESE: catalogs.VIETNAMESE_CATALOG,
|
|
423
|
+
SupportedLocale.INDONESIAN: catalogs.INDONESIAN_CATALOG,
|
|
424
|
+
SupportedLocale.TURKISH: catalogs.TURKISH_CATALOG,
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if locale in catalog_map:
|
|
428
|
+
self._catalogs[locale] = catalog_map[locale]
|
|
429
|
+
|
|
430
|
+
def _get_english_fallback(self) -> LocaleCatalog:
|
|
431
|
+
"""Get English catalog as fallback."""
|
|
432
|
+
if SupportedLocale.ENGLISH not in self._catalogs:
|
|
433
|
+
from . import catalogs
|
|
434
|
+
self._catalogs[SupportedLocale.ENGLISH] = catalogs.ENGLISH_CATALOG
|
|
435
|
+
return self._catalogs[SupportedLocale.ENGLISH]
|
|
436
|
+
|
|
437
|
+
def list_locales(self) -> list[dict[str, str]]:
|
|
438
|
+
"""List all available locales with metadata.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
List of locale information dictionaries.
|
|
442
|
+
"""
|
|
443
|
+
return [
|
|
444
|
+
{
|
|
445
|
+
"code": locale.value,
|
|
446
|
+
"english_name": locale.english_name,
|
|
447
|
+
"native_name": locale.native_name,
|
|
448
|
+
"flag": locale.flag_emoji,
|
|
449
|
+
"rtl": locale.is_rtl,
|
|
450
|
+
}
|
|
451
|
+
for locale in SupportedLocale
|
|
452
|
+
]
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# Global registry instance
|
|
456
|
+
_registry = LocalizationRegistry()
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def get_localizer(
|
|
460
|
+
locale: SupportedLocale | str,
|
|
461
|
+
fallback_to_english: bool = True,
|
|
462
|
+
) -> ReportLocalizer:
|
|
463
|
+
"""Get a localizer for the specified locale.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
locale: Target locale (SupportedLocale or string code).
|
|
467
|
+
fallback_to_english: Whether to fall back to English for missing keys.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
ReportLocalizer instance.
|
|
471
|
+
|
|
472
|
+
Raises:
|
|
473
|
+
ValueError: If locale is not supported.
|
|
474
|
+
"""
|
|
475
|
+
if isinstance(locale, str):
|
|
476
|
+
locale = SupportedLocale.from_string(locale)
|
|
477
|
+
|
|
478
|
+
catalog = _registry.get_or_load(locale)
|
|
479
|
+
fallback = (
|
|
480
|
+
_registry.get_or_load(SupportedLocale.ENGLISH)
|
|
481
|
+
if fallback_to_english and locale != SupportedLocale.ENGLISH
|
|
482
|
+
else None
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
return ReportLocalizer(locale, catalog, fallback)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def get_supported_locales() -> list[dict[str, Any]]:
|
|
489
|
+
"""Get list of all supported locales.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
List of locale information dictionaries.
|
|
493
|
+
"""
|
|
494
|
+
return _registry.list_locales()
|