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.
Files changed (169) hide show
  1. truthound_dashboard/api/alerts.py +258 -0
  2. truthound_dashboard/api/anomaly.py +1302 -0
  3. truthound_dashboard/api/cross_alerts.py +352 -0
  4. truthound_dashboard/api/deps.py +143 -0
  5. truthound_dashboard/api/drift_monitor.py +540 -0
  6. truthound_dashboard/api/lineage.py +1151 -0
  7. truthound_dashboard/api/maintenance.py +363 -0
  8. truthound_dashboard/api/middleware.py +373 -1
  9. truthound_dashboard/api/model_monitoring.py +805 -0
  10. truthound_dashboard/api/notifications_advanced.py +2452 -0
  11. truthound_dashboard/api/plugins.py +2096 -0
  12. truthound_dashboard/api/profile.py +211 -14
  13. truthound_dashboard/api/reports.py +853 -0
  14. truthound_dashboard/api/router.py +147 -0
  15. truthound_dashboard/api/rule_suggestions.py +310 -0
  16. truthound_dashboard/api/schema_evolution.py +231 -0
  17. truthound_dashboard/api/sources.py +47 -3
  18. truthound_dashboard/api/triggers.py +190 -0
  19. truthound_dashboard/api/validations.py +13 -0
  20. truthound_dashboard/api/validators.py +333 -4
  21. truthound_dashboard/api/versioning.py +309 -0
  22. truthound_dashboard/api/websocket.py +301 -0
  23. truthound_dashboard/core/__init__.py +27 -0
  24. truthound_dashboard/core/anomaly.py +1395 -0
  25. truthound_dashboard/core/anomaly_explainer.py +633 -0
  26. truthound_dashboard/core/cache.py +206 -0
  27. truthound_dashboard/core/cached_services.py +422 -0
  28. truthound_dashboard/core/charts.py +352 -0
  29. truthound_dashboard/core/connections.py +1069 -42
  30. truthound_dashboard/core/cross_alerts.py +837 -0
  31. truthound_dashboard/core/drift_monitor.py +1477 -0
  32. truthound_dashboard/core/drift_sampling.py +669 -0
  33. truthound_dashboard/core/i18n/__init__.py +42 -0
  34. truthound_dashboard/core/i18n/detector.py +173 -0
  35. truthound_dashboard/core/i18n/messages.py +564 -0
  36. truthound_dashboard/core/lineage.py +971 -0
  37. truthound_dashboard/core/maintenance.py +443 -5
  38. truthound_dashboard/core/model_monitoring.py +1043 -0
  39. truthound_dashboard/core/notifications/channels.py +1020 -1
  40. truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
  41. truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
  42. truthound_dashboard/core/notifications/deduplication/service.py +400 -0
  43. truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
  44. truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
  45. truthound_dashboard/core/notifications/dispatcher.py +43 -0
  46. truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
  47. truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
  48. truthound_dashboard/core/notifications/escalation/engine.py +429 -0
  49. truthound_dashboard/core/notifications/escalation/models.py +336 -0
  50. truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
  51. truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
  52. truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
  53. truthound_dashboard/core/notifications/events.py +49 -0
  54. truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
  55. truthound_dashboard/core/notifications/metrics/base.py +528 -0
  56. truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
  57. truthound_dashboard/core/notifications/routing/__init__.py +169 -0
  58. truthound_dashboard/core/notifications/routing/combinators.py +184 -0
  59. truthound_dashboard/core/notifications/routing/config.py +375 -0
  60. truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
  61. truthound_dashboard/core/notifications/routing/engine.py +382 -0
  62. truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
  63. truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
  64. truthound_dashboard/core/notifications/routing/rules.py +625 -0
  65. truthound_dashboard/core/notifications/routing/validator.py +678 -0
  66. truthound_dashboard/core/notifications/service.py +2 -0
  67. truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
  68. truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
  69. truthound_dashboard/core/notifications/throttling/builder.py +311 -0
  70. truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
  71. truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
  72. truthound_dashboard/core/openlineage.py +1028 -0
  73. truthound_dashboard/core/plugins/__init__.py +39 -0
  74. truthound_dashboard/core/plugins/docs/__init__.py +39 -0
  75. truthound_dashboard/core/plugins/docs/extractor.py +703 -0
  76. truthound_dashboard/core/plugins/docs/renderers.py +804 -0
  77. truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
  78. truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
  79. truthound_dashboard/core/plugins/hooks/manager.py +403 -0
  80. truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
  81. truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
  82. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
  83. truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
  84. truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
  85. truthound_dashboard/core/plugins/loader.py +504 -0
  86. truthound_dashboard/core/plugins/registry.py +810 -0
  87. truthound_dashboard/core/plugins/reporter_executor.py +588 -0
  88. truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
  89. truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
  90. truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
  91. truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
  92. truthound_dashboard/core/plugins/sandbox.py +617 -0
  93. truthound_dashboard/core/plugins/security/__init__.py +68 -0
  94. truthound_dashboard/core/plugins/security/analyzer.py +535 -0
  95. truthound_dashboard/core/plugins/security/policies.py +311 -0
  96. truthound_dashboard/core/plugins/security/protocols.py +296 -0
  97. truthound_dashboard/core/plugins/security/signing.py +842 -0
  98. truthound_dashboard/core/plugins/security.py +446 -0
  99. truthound_dashboard/core/plugins/validator_executor.py +401 -0
  100. truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
  101. truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
  102. truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
  103. truthound_dashboard/core/plugins/versioning/semver.py +266 -0
  104. truthound_dashboard/core/profile_comparison.py +601 -0
  105. truthound_dashboard/core/report_history.py +570 -0
  106. truthound_dashboard/core/reporters/__init__.py +57 -0
  107. truthound_dashboard/core/reporters/base.py +296 -0
  108. truthound_dashboard/core/reporters/csv_reporter.py +155 -0
  109. truthound_dashboard/core/reporters/html_reporter.py +598 -0
  110. truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
  111. truthound_dashboard/core/reporters/i18n/base.py +494 -0
  112. truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
  113. truthound_dashboard/core/reporters/json_reporter.py +160 -0
  114. truthound_dashboard/core/reporters/junit_reporter.py +233 -0
  115. truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
  116. truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
  117. truthound_dashboard/core/reporters/registry.py +272 -0
  118. truthound_dashboard/core/rule_generator.py +2088 -0
  119. truthound_dashboard/core/scheduler.py +822 -12
  120. truthound_dashboard/core/schema_evolution.py +858 -0
  121. truthound_dashboard/core/services.py +152 -9
  122. truthound_dashboard/core/statistics.py +718 -0
  123. truthound_dashboard/core/streaming_anomaly.py +883 -0
  124. truthound_dashboard/core/triggers/__init__.py +45 -0
  125. truthound_dashboard/core/triggers/base.py +226 -0
  126. truthound_dashboard/core/triggers/evaluators.py +609 -0
  127. truthound_dashboard/core/triggers/factory.py +363 -0
  128. truthound_dashboard/core/unified_alerts.py +870 -0
  129. truthound_dashboard/core/validation_limits.py +509 -0
  130. truthound_dashboard/core/versioning.py +709 -0
  131. truthound_dashboard/core/websocket/__init__.py +59 -0
  132. truthound_dashboard/core/websocket/manager.py +512 -0
  133. truthound_dashboard/core/websocket/messages.py +130 -0
  134. truthound_dashboard/db/__init__.py +30 -0
  135. truthound_dashboard/db/models.py +3375 -3
  136. truthound_dashboard/main.py +22 -0
  137. truthound_dashboard/schemas/__init__.py +396 -1
  138. truthound_dashboard/schemas/anomaly.py +1258 -0
  139. truthound_dashboard/schemas/base.py +4 -0
  140. truthound_dashboard/schemas/cross_alerts.py +334 -0
  141. truthound_dashboard/schemas/drift_monitor.py +890 -0
  142. truthound_dashboard/schemas/lineage.py +428 -0
  143. truthound_dashboard/schemas/maintenance.py +154 -0
  144. truthound_dashboard/schemas/model_monitoring.py +374 -0
  145. truthound_dashboard/schemas/notifications_advanced.py +1363 -0
  146. truthound_dashboard/schemas/openlineage.py +704 -0
  147. truthound_dashboard/schemas/plugins.py +1293 -0
  148. truthound_dashboard/schemas/profile.py +420 -34
  149. truthound_dashboard/schemas/profile_comparison.py +242 -0
  150. truthound_dashboard/schemas/reports.py +285 -0
  151. truthound_dashboard/schemas/rule_suggestion.py +434 -0
  152. truthound_dashboard/schemas/schema_evolution.py +164 -0
  153. truthound_dashboard/schemas/source.py +117 -2
  154. truthound_dashboard/schemas/triggers.py +511 -0
  155. truthound_dashboard/schemas/unified_alerts.py +223 -0
  156. truthound_dashboard/schemas/validation.py +25 -1
  157. truthound_dashboard/schemas/validators/__init__.py +11 -0
  158. truthound_dashboard/schemas/validators/base.py +151 -0
  159. truthound_dashboard/schemas/versioning.py +152 -0
  160. truthound_dashboard/static/index.html +2 -2
  161. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/METADATA +147 -23
  162. truthound_dashboard-1.4.1.dist-info/RECORD +239 -0
  163. truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
  164. truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
  165. truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
  166. truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
  167. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/entry_points.txt +0 -0
  169. {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()