Files
insightflow/backend/localization_manager.py
OpenClaw Bot ea58b6fe43 fix: auto-fix code issues (cron)
- 修复重复导入/字段
- 修复异常处理
- 修复PEP8格式问题
- 添加类型注解
2026-03-01 00:08:06 +08:00

1693 lines
65 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
InsightFlow Phase 8 - 全球化与本地化管理模块
功能:
1. 多语言支持i18n支持10+语言)
2. 区域数据中心配置(北美、欧洲、亚太)
3. 本地化支付方式管理
4. 时区与日历本地化
作者: InsightFlow Team
"""
import json
import logging
import sqlite3
import uuid
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
from typing import Any
try:
import pytz
PYTZ_AVAILABLE = True
except ImportError:
PYTZ_AVAILABLE = False
try:
from babel import Locale, dates, numbers
BABEL_AVAILABLE = True
except ImportError:
BABEL_AVAILABLE = False
logger = logging.getLogger(__name__)
class LanguageCode(StrEnum):
"""支持的语言代码"""
EN = "en"
ZH_CN = "zh_CN"
ZH_TW = "zh_TW"
JA = "ja"
KO = "ko"
DE = "de"
FR = "fr"
ES = "es"
PT = "pt"
RU = "ru"
AR = "ar"
HI = "hi"
class RegionCode(StrEnum):
"""区域代码"""
GLOBAL = "global"
NORTH_AMERICA = "na"
EUROPE = "eu"
ASIA_PACIFIC = "apac"
CHINA = "cn"
LATIN_AMERICA = "latam"
MIDDLE_EAST = "me"
class DataCenterRegion(StrEnum):
"""数据中心区域"""
US_EAST = "us-east"
US_WEST = "us-west"
EU_WEST = "eu-west"
EU_CENTRAL = "eu-central"
AP_SOUTHEAST = "ap-southeast"
AP_NORTHEAST = "ap-northeast"
AP_SOUTH = "ap-south"
CN_NORTH = "cn-north"
CN_EAST = "cn-east"
class PaymentProvider(StrEnum):
"""支付提供商"""
STRIPE = "stripe"
ALIPAY = "alipay"
WECHAT_PAY = "wechat_pay"
PAYPAL = "paypal"
APPLE_PAY = "apple_pay"
GOOGLE_PAY = "google_pay"
KLARNA = "klarna"
IDEAL = "ideal"
BANCONTACT = "bancontact"
GIROPAY = "giropay"
SEPA = "sepa"
UNIONPAY = "unionpay"
class CalendarType(StrEnum):
"""日历类型"""
GREGORIAN = "gregorian"
CHINESE_LUNAR = "chinese_lunar"
ISLAMIC = "islamic"
HEBREW = "hebrew"
INDIAN = "indian"
PERSIAN = "persian"
BUDDHIST = "buddhist"
@dataclass
class Translation:
id: str
key: str
language: str
value: str
namespace: str
context: str | None
created_at: datetime
updated_at: datetime
is_reviewed: bool
reviewed_by: str | None
reviewed_at: datetime | None
@dataclass
class LanguageConfig:
code: str
name: str
name_local: str
is_rtl: bool
is_active: bool
is_default: bool
fallback_language: str | None
date_format: str
time_format: str
datetime_format: str
number_format: str
currency_format: str
first_day_of_week: int
calendar_type: str
@dataclass
class DataCenter:
id: str
region_code: str
name: str
location: str
endpoint: str
status: str
priority: int
supported_regions: list[str]
capabilities: dict[str, Any]
created_at: datetime
updated_at: datetime
@dataclass
class TenantDataCenterMapping:
id: str
tenant_id: str
primary_dc_id: str
secondary_dc_id: str | None
region_code: str
data_residency: str
created_at: datetime
updated_at: datetime
@dataclass
class LocalizedPaymentMethod:
id: str
provider: str
name: str
name_local: dict[str, str]
supported_countries: list[str]
supported_currencies: list[str]
is_active: bool
config: dict[str, Any]
icon_url: str | None
display_order: int
min_amount: float | None
max_amount: float | None
created_at: datetime
updated_at: datetime
@dataclass
class CountryConfig:
code: str
code3: str
name: str
name_local: dict[str, str]
region: str
default_language: str
supported_languages: list[str]
default_currency: str
supported_currencies: list[str]
timezone: str
calendar_type: str
date_format: str | None
time_format: str | None
number_format: str | None
address_format: str | None
phone_format: str | None
vat_rate: float | None
is_active: bool
@dataclass
class TimezoneConfig:
id: str
timezone: str
utc_offset: str
dst_offset: str | None
country_code: str
region: str
is_active: bool
@dataclass
class CurrencyConfig:
code: str
name: str
name_local: dict[str, str]
symbol: str
decimal_places: int
decimal_separator: str
thousands_separator: str
is_active: bool
@dataclass
class LocalizationSettings:
id: str
tenant_id: str
default_language: str
supported_languages: list[str]
default_currency: str
supported_currencies: list[str]
default_timezone: str
default_date_format: str | None
default_time_format: str | None
default_number_format: str | None
calendar_type: str
first_day_of_week: int
region_code: str
data_residency: str
created_at: datetime
updated_at: datetime
class LocalizationManager:
DEFAULT_LANGUAGES = {
LanguageCode.EN: {
"name": "English",
"name_local": "English",
"is_rtl": False,
"date_format": "MM/dd/yyyy",
"time_format": "h:mm a",
"datetime_format": "MM/dd/yyyy h:mm a",
"number_format": "#,##0.##",
"currency_format": "$#,##0.00",
"first_day_of_week": 0,
"calendar_type": CalendarType.GREGORIAN.value,
},
LanguageCode.ZH_CN: {
"name": "Chinese (Simplified)",
"name_local": "简体中文",
"is_rtl": False,
"date_format": "yyyy-MM-dd",
"time_format": "HH:mm",
"datetime_format": "yyyy-MM-dd HH:mm",
"number_format": "#,##0.##",
"currency_format": "¥#,##0.00",
"first_day_of_week": 1,
"calendar_type": CalendarType.GREGORIAN.value,
},
LanguageCode.ZH_TW: {
"name": "Chinese (Traditional)",
"name_local": "繁體中文",
"is_rtl": False,
"date_format": "yyyy/MM/dd",
"time_format": "HH:mm",
"datetime_format": "yyyy/MM/dd HH:mm",
"number_format": "#,##0.##",
"currency_format": "NT$#,##0.00",
"first_day_of_week": 0,
"calendar_type": CalendarType.GREGORIAN.value,
},
LanguageCode.JA: {
"name": "Japanese",
"name_local": "日本語",
"is_rtl": False,
"date_format": "yyyy/MM/dd",
"time_format": "HH:mm",
"datetime_format": "yyyy/MM/dd HH:mm",
"number_format": "#,##0.##",
"currency_format": "¥#,##0",
"first_day_of_week": 0,
"calendar_type": CalendarType.GREGORIAN.value,
},
LanguageCode.KO: {
"name": "Korean",
"name_local": "한국어",
"is_rtl": False,
"date_format": "yyyy. MM. dd",
"time_format": "HH:mm",
"datetime_format": "yyyy. MM. dd HH:mm",
"number_format": "#,##0.##",
"currency_format": "₩#,##0",
"first_day_of_week": 0,
"calendar_type": CalendarType.GREGORIAN.value,
},
LanguageCode.DE: {
"name": "German",
"name_local": "Deutsch",
"is_rtl": False,
"date_format": "dd.MM.yyyy",
"time_format": "HH:mm",
"datetime_format": "dd.MM.yyyy HH:mm",
"number_format": "#,##0.##",
"currency_format": "#,##0.00 €",
"first_day_of_week": 1,
"calendar_type": CalendarType.GREGORIAN.value,
},
LanguageCode.FR: {
"name": "French",
"name_local": "Français",
"is_rtl": False,
"date_format": "dd/MM/yyyy",
"time_format": "HH:mm",
"datetime_format": "dd/MM/yyyy HH:mm",
"number_format": "#,##0.##",
"currency_format": "#,##0.00 €",
"first_day_of_week": 1,
"calendar_type": CalendarType.GREGORIAN.value,
},
LanguageCode.ES: {
"name": "Spanish",
"name_local": "Español",
"is_rtl": False,
"date_format": "dd/MM/yyyy",
"time_format": "HH:mm",
"datetime_format": "dd/MM/yyyy HH:mm",
"number_format": "#,##0.##",
"currency_format": "#,##0.00 €",
"first_day_of_week": 1,
"calendar_type": CalendarType.GREGORIAN.value,
},
LanguageCode.PT: {
"name": "Portuguese",
"name_local": "Português",
"is_rtl": False,
"date_format": "dd/MM/yyyy",
"time_format": "HH:mm",
"datetime_format": "dd/MM/yyyy HH:mm",
"number_format": "#,##0.##",
"currency_format": "R$#,##0.00",
"first_day_of_week": 0,
"calendar_type": CalendarType.GREGORIAN.value,
},
LanguageCode.RU: {
"name": "Russian",
"name_local": "Русский",
"is_rtl": False,
"date_format": "dd.MM.yyyy",
"time_format": "HH:mm",
"datetime_format": "dd.MM.yyyy HH:mm",
"number_format": "#,##0.##",
"currency_format": "#,##0.00 ₽",
"first_day_of_week": 1,
"calendar_type": CalendarType.GREGORIAN.value,
},
LanguageCode.AR: {
"name": "Arabic",
"name_local": "العربية",
"is_rtl": True,
"date_format": "dd/MM/yyyy",
"time_format": "hh:mm a",
"datetime_format": "dd/MM/yyyy hh:mm a",
"number_format": "#,##0.##",
"currency_format": "#,##0.00 ر.س",
"first_day_of_week": 6,
"calendar_type": CalendarType.ISLAMIC.value,
},
LanguageCode.HI: {
"name": "Hindi",
"name_local": "हिन्दी",
"is_rtl": False,
"date_format": "dd/MM/yyyy",
"time_format": "hh:mm a",
"datetime_format": "dd/MM/yyyy hh:mm a",
"number_format": "#,##0.##",
"currency_format": "₹#,##0.00",
"first_day_of_week": 0,
"calendar_type": CalendarType.INDIAN.value,
},
}
DEFAULT_DATA_CENTERS = {
DataCenterRegion.US_EAST: {
"name": "US East (Virginia)",
"location": "Virginia, USA",
"endpoint": "https://api-us-east.insightflow.io",
"priority": 1,
"supported_regions": [RegionCode.NORTH_AMERICA.value, RegionCode.GLOBAL.value],
"capabilities": {"storage": True, "compute": True, "ml": True},
},
DataCenterRegion.US_WEST: {
"name": "US West (California)",
"location": "California, USA",
"endpoint": "https://api-us-west.insightflow.io",
"priority": 2,
"supported_regions": [RegionCode.NORTH_AMERICA.value, RegionCode.GLOBAL.value],
"capabilities": {"storage": True, "compute": True, "ml": False},
},
DataCenterRegion.EU_WEST: {
"name": "EU West (Ireland)",
"location": "Dublin, Ireland",
"endpoint": "https://api-eu-west.insightflow.io",
"priority": 1,
"supported_regions": [RegionCode.EUROPE.value, RegionCode.GLOBAL.value],
"capabilities": {"storage": True, "compute": True, "ml": True},
},
DataCenterRegion.EU_CENTRAL: {
"name": "EU Central (Frankfurt)",
"location": "Frankfurt, Germany",
"endpoint": "https://api-eu-central.insightflow.io",
"priority": 2,
"supported_regions": [RegionCode.EUROPE.value, RegionCode.GLOBAL.value],
"capabilities": {"storage": True, "compute": True, "ml": False},
},
DataCenterRegion.AP_SOUTHEAST: {
"name": "Asia Pacific (Singapore)",
"location": "Singapore",
"endpoint": "https://api-ap-southeast.insightflow.io",
"priority": 1,
"supported_regions": [RegionCode.ASIA_PACIFIC.value, RegionCode.GLOBAL.value],
"capabilities": {"storage": True, "compute": True, "ml": True},
},
DataCenterRegion.AP_NORTHEAST: {
"name": "Asia Pacific (Tokyo)",
"location": "Tokyo, Japan",
"endpoint": "https://api-ap-northeast.insightflow.io",
"priority": 2,
"supported_regions": [RegionCode.ASIA_PACIFIC.value, RegionCode.GLOBAL.value],
"capabilities": {"storage": True, "compute": True, "ml": False},
},
DataCenterRegion.AP_SOUTH: {
"name": "Asia Pacific (Mumbai)",
"location": "Mumbai, India",
"endpoint": "https://api-ap-south.insightflow.io",
"priority": 3,
"supported_regions": [RegionCode.ASIA_PACIFIC.value, RegionCode.GLOBAL.value],
"capabilities": {"storage": True, "compute": True, "ml": False},
},
DataCenterRegion.CN_NORTH: {
"name": "China (Beijing)",
"location": "Beijing, China",
"endpoint": "https://api-cn-north.insightflow.cn",
"priority": 1,
"supported_regions": [RegionCode.CHINA.value],
"capabilities": {"storage": True, "compute": True, "ml": True},
},
DataCenterRegion.CN_EAST: {
"name": "China (Shanghai)",
"location": "Shanghai, China",
"endpoint": "https://api-cn-east.insightflow.cn",
"priority": 2,
"supported_regions": [RegionCode.CHINA.value],
"capabilities": {"storage": True, "compute": True, "ml": False},
},
}
DEFAULT_PAYMENT_METHODS = {
PaymentProvider.STRIPE: {
"name": "Credit Card",
"name_local": {
"en": "Credit Card",
"zh_CN": "信用卡",
"zh_TW": "信用卡",
"ja": "クレジットカード",
"ko": "신용카드",
"de": "Kreditkarte",
"fr": "Carte de crédit",
"es": "Tarjeta de crédito",
"pt": "Cartão de crédito",
"ru": "Кредитная карта",
},
"supported_countries": ["*"],
"supported_currencies": ["USD", "EUR", "GBP", "CAD", "AUD", "JPY"],
"display_order": 1,
},
PaymentProvider.ALIPAY: {
"name": "Alipay",
"name_local": {"en": "Alipay", "zh_CN": "支付宝", "zh_TW": "支付寶"},
"supported_countries": ["CN", "HK", "MO", "TW", "SG", "MY", "TH"],
"supported_currencies": ["CNY", "HKD", "USD"],
"display_order": 2,
},
PaymentProvider.WECHAT_PAY: {
"name": "WeChat Pay",
"name_local": {"en": "WeChat Pay", "zh_CN": "微信支付", "zh_TW": "微信支付"},
"supported_countries": ["CN", "HK", "MO"],
"supported_currencies": ["CNY", "HKD"],
"display_order": 3,
},
PaymentProvider.PAYPAL: {
"name": "PayPal",
"name_local": {"en": "PayPal"},
"supported_countries": ["*"],
"supported_currencies": ["USD", "EUR", "GBP", "CAD", "AUD", "JPY"],
"display_order": 4,
},
PaymentProvider.APPLE_PAY: {
"name": "Apple Pay",
"name_local": {"en": "Apple Pay"},
"supported_countries": ["US", "CA", "GB", "AU", "JP", "DE", "FR"],
"supported_currencies": ["USD", "EUR", "GBP", "CAD", "AUD", "JPY"],
"display_order": 5,
},
PaymentProvider.GOOGLE_PAY: {
"name": "Google Pay",
"name_local": {"en": "Google Pay"},
"supported_countries": ["US", "CA", "GB", "AU", "JP", "DE", "FR"],
"supported_currencies": ["USD", "EUR", "GBP", "CAD", "AUD", "JPY"],
"display_order": 6,
},
PaymentProvider.KLARNA: {
"name": "Klarna",
"name_local": {"en": "Klarna", "de": "Klarna", "fr": "Klarna"},
"supported_countries": ["DE", "AT", "NL", "BE", "FI", "SE", "NO", "DK", "GB"],
"supported_currencies": ["EUR", "GBP"],
"display_order": 7,
},
PaymentProvider.IDEAL: {
"name": "iDEAL",
"name_local": {"en": "iDEAL", "de": "iDEAL"},
"supported_countries": ["NL"],
"supported_currencies": ["EUR"],
"display_order": 8,
},
PaymentProvider.BANCONTACT: {
"name": "Bancontact",
"name_local": {"en": "Bancontact", "de": "Bancontact"},
"supported_countries": ["BE"],
"supported_currencies": ["EUR"],
"display_order": 9,
},
PaymentProvider.GIROPAY: {
"name": "giropay",
"name_local": {"en": "giropay", "de": "giropay"},
"supported_countries": ["DE"],
"supported_currencies": ["EUR"],
"display_order": 10,
},
PaymentProvider.SEPA: {
"name": "SEPA Direct Debit",
"name_local": {"en": "SEPA Direct Debit", "de": "SEPA-Lastschrift"},
"supported_countries": ["DE", "AT", "NL", "BE", "FR", "ES", "IT"],
"supported_currencies": ["EUR"],
"display_order": 11,
},
PaymentProvider.UNIONPAY: {
"name": "UnionPay",
"name_local": {"en": "UnionPay", "zh_CN": "银联", "zh_TW": "銀聯"},
"supported_countries": ["CN", "HK", "MO", "TW"],
"supported_currencies": ["CNY", "USD"],
"display_order": 12,
},
}
DEFAULT_COUNTRIES = {
"US": {
"name": "United States",
"name_local": {"en": "United States"},
"region": RegionCode.NORTH_AMERICA.value,
"default_language": LanguageCode.EN.value,
"supported_languages": [LanguageCode.EN.value],
"default_currency": "USD",
"supported_currencies": ["USD"],
"timezone": "America/New_York",
"calendar_type": CalendarType.GREGORIAN.value,
"vat_rate": None,
},
"CN": {
"name": "China",
"name_local": {"zh_CN": "中国"},
"region": RegionCode.CHINA.value,
"default_language": LanguageCode.ZH_CN.value,
"supported_languages": [LanguageCode.ZH_CN.value, LanguageCode.EN.value],
"default_currency": "CNY",
"supported_currencies": ["CNY", "USD"],
"timezone": "Asia/Shanghai",
"calendar_type": CalendarType.GREGORIAN.value,
"vat_rate": 0.13,
},
"JP": {
"name": "Japan",
"name_local": {"ja": "日本"},
"region": RegionCode.ASIA_PACIFIC.value,
"default_language": LanguageCode.JA.value,
"supported_languages": [LanguageCode.JA.value, LanguageCode.EN.value],
"default_currency": "JPY",
"supported_currencies": ["JPY", "USD"],
"timezone": "Asia/Tokyo",
"calendar_type": CalendarType.GREGORIAN.value,
"vat_rate": 0.10,
},
"DE": {
"name": "Germany",
"name_local": {"de": "Deutschland"},
"region": RegionCode.EUROPE.value,
"default_language": LanguageCode.DE.value,
"supported_languages": [LanguageCode.DE.value, LanguageCode.EN.value],
"default_currency": "EUR",
"supported_currencies": ["EUR", "USD"],
"timezone": "Europe/Berlin",
"calendar_type": CalendarType.GREGORIAN.value,
"vat_rate": 0.19,
},
"GB": {
"name": "United Kingdom",
"name_local": {"en": "United Kingdom"},
"region": RegionCode.EUROPE.value,
"default_language": LanguageCode.EN.value,
"supported_languages": [LanguageCode.EN.value],
"default_currency": "GBP",
"supported_currencies": ["GBP", "EUR", "USD"],
"timezone": "Europe/London",
"calendar_type": CalendarType.GREGORIAN.value,
"vat_rate": 0.20,
},
"FR": {
"name": "France",
"name_local": {"fr": "France"},
"region": RegionCode.EUROPE.value,
"default_language": LanguageCode.FR.value,
"supported_languages": [LanguageCode.FR.value, LanguageCode.EN.value],
"default_currency": "EUR",
"supported_currencies": ["EUR", "USD"],
"timezone": "Europe/Paris",
"calendar_type": CalendarType.GREGORIAN.value,
"vat_rate": 0.20,
},
"SG": {
"name": "Singapore",
"name_local": {"en": "Singapore"},
"region": RegionCode.ASIA_PACIFIC.value,
"default_language": LanguageCode.EN.value,
"supported_languages": [LanguageCode.EN.value, LanguageCode.ZH_CN.value],
"default_currency": "SGD",
"supported_currencies": ["SGD", "USD"],
"timezone": "Asia/Singapore",
"calendar_type": CalendarType.GREGORIAN.value,
"vat_rate": 0.08,
},
"AU": {
"name": "Australia",
"name_local": {"en": "Australia"},
"region": RegionCode.ASIA_PACIFIC.value,
"default_language": LanguageCode.EN.value,
"supported_languages": [LanguageCode.EN.value],
"default_currency": "AUD",
"supported_currencies": ["AUD", "USD"],
"timezone": "Australia/Sydney",
"calendar_type": CalendarType.GREGORIAN.value,
"vat_rate": 0.10,
},
"CA": {
"name": "Canada",
"name_local": {"en": "Canada", "fr": "Canada"},
"region": RegionCode.NORTH_AMERICA.value,
"default_language": LanguageCode.EN.value,
"supported_languages": [LanguageCode.EN.value, LanguageCode.FR.value],
"default_currency": "CAD",
"supported_currencies": ["CAD", "USD"],
"timezone": "America/Toronto",
"calendar_type": CalendarType.GREGORIAN.value,
"vat_rate": 0.05,
},
"BR": {
"name": "Brazil",
"name_local": {"pt": "Brasil"},
"region": RegionCode.LATIN_AMERICA.value,
"default_language": LanguageCode.PT.value,
"supported_languages": [LanguageCode.PT.value, LanguageCode.EN.value],
"default_currency": "BRL",
"supported_currencies": ["BRL", "USD"],
"timezone": "America/Sao_Paulo",
"calendar_type": CalendarType.GREGORIAN.value,
"vat_rate": 0.17,
},
"IN": {
"name": "India",
"name_local": {"hi": "भारत"},
"region": RegionCode.ASIA_PACIFIC.value,
"default_language": LanguageCode.EN.value,
"supported_languages": [LanguageCode.EN.value, LanguageCode.HI.value],
"default_currency": "INR",
"supported_currencies": ["INR", "USD"],
"timezone": "Asia/Kolkata",
"calendar_type": CalendarType.GREGORIAN.value,
"vat_rate": 0.18,
},
"AE": {
"name": "United Arab Emirates",
"name_local": {"ar": "الإمارات العربية المتحدة"},
"region": RegionCode.MIDDLE_EAST.value,
"default_language": LanguageCode.EN.value,
"supported_languages": [LanguageCode.EN.value, LanguageCode.AR.value],
"default_currency": "AED",
"supported_currencies": ["AED", "USD"],
"timezone": "Asia/Dubai",
"calendar_type": CalendarType.ISLAMIC.value,
"vat_rate": 0.05,
},
}
def __init__(self, db_path: str = "insightflow.db"):
self.db_path = db_path
self._is_memory_db = db_path == ":memory:"
self._conn = None
self._init_db()
self._init_default_data()
def _get_connection(self) -> sqlite3.Connection:
if self._is_memory_db:
if self._conn is None:
self._conn = sqlite3.connect(self.db_path)
self._conn.row_factory = sqlite3.Row
return self._conn
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
return conn
def _close_if_file_db(self, conn):
if not self._is_memory_db:
conn.close()
def _init_db(self):
conn = self._get_connection()
try:
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS translations (
id TEXT PRIMARY KEY, key TEXT NOT NULL, language TEXT NOT NULL,
value TEXT NOT NULL, namespace TEXT DEFAULT 'common', context TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, is_reviewed INTEGER DEFAULT 0,
reviewed_by TEXT, reviewed_at TIMESTAMP, UNIQUE(key, language, namespace)
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS language_configs (
code TEXT PRIMARY KEY, name TEXT NOT NULL, name_local TEXT NOT NULL,
is_rtl INTEGER DEFAULT 0, is_active INTEGER DEFAULT 1, is_default INTEGER DEFAULT 0,
fallback_language TEXT, date_format TEXT, time_format TEXT, datetime_format TEXT,
number_format TEXT, currency_format TEXT, first_day_of_week INTEGER DEFAULT 1,
calendar_type TEXT DEFAULT 'gregorian'
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS data_centers (
id TEXT PRIMARY KEY, region_code TEXT NOT NULL UNIQUE, name TEXT NOT NULL,
location TEXT NOT NULL, endpoint TEXT NOT NULL, status TEXT DEFAULT 'active',
priority INTEGER DEFAULT 1, supported_regions TEXT DEFAULT '[]',
capabilities TEXT DEFAULT '{}', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS tenant_data_center_mappings (
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL UNIQUE, primary_dc_id TEXT NOT NULL,
secondary_dc_id TEXT, region_code TEXT NOT NULL, data_residency TEXT DEFAULT 'regional',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
FOREIGN KEY (primary_dc_id) REFERENCES data_centers(id),
FOREIGN KEY (secondary_dc_id) REFERENCES data_centers(id)
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS localized_payment_methods (
id TEXT PRIMARY KEY, provider TEXT NOT NULL UNIQUE, name TEXT NOT NULL,
name_local TEXT DEFAULT '{}', supported_countries TEXT DEFAULT '[]',
supported_currencies TEXT DEFAULT '[]', is_active INTEGER DEFAULT 1,
config TEXT DEFAULT '{}', icon_url TEXT, display_order INTEGER DEFAULT 0,
min_amount REAL, max_amount REAL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS country_configs (
code TEXT PRIMARY KEY, code3 TEXT NOT NULL, name TEXT NOT NULL,
name_local TEXT DEFAULT '{}', region TEXT NOT NULL, default_language TEXT NOT NULL,
supported_languages TEXT DEFAULT '[]', default_currency TEXT NOT NULL,
supported_currencies TEXT DEFAULT '[]', timezone TEXT NOT NULL,
calendar_type TEXT DEFAULT 'gregorian', date_format TEXT, time_format TEXT,
number_format TEXT, address_format TEXT, phone_format TEXT, vat_rate REAL,
is_active INTEGER DEFAULT 1
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS timezone_configs (
id TEXT PRIMARY KEY, timezone TEXT NOT NULL UNIQUE, utc_offset TEXT NOT NULL, dst_offset TEXT,
country_code TEXT NOT NULL, region TEXT NOT NULL, is_active INTEGER DEFAULT 1
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS currency_configs (
code TEXT PRIMARY KEY, name TEXT NOT NULL, name_local TEXT DEFAULT '{}', symbol TEXT NOT NULL,
decimal_places INTEGER DEFAULT 2, decimal_separator TEXT DEFAULT '.',
thousands_separator TEXT DEFAULT ',', is_active INTEGER DEFAULT 1
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS localization_settings (
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL UNIQUE, default_language TEXT DEFAULT 'en',
supported_languages TEXT DEFAULT '["en"]', default_currency TEXT DEFAULT 'USD',
supported_currencies TEXT DEFAULT '["USD"]', default_timezone TEXT DEFAULT 'UTC',
default_date_format TEXT, default_time_format TEXT, default_number_format TEXT,
calendar_type TEXT DEFAULT 'gregorian', first_day_of_week INTEGER DEFAULT 1,
region_code TEXT DEFAULT 'global', data_residency TEXT DEFAULT 'regional',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
)
""")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_translations_key ON translations(key)")
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_translations_lang ON translations(language)"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_translations_ns ON translations(namespace)"
)
cursor.execute("CREATE INDEX IF NOT EXISTS idx_dc_region ON data_centers(region_code)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_dc_status ON data_centers(status)")
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_tenant_dc ON tenant_data_center_mappings(tenant_id)"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_payment_provider ON localized_payment_methods(provider)"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_payment_active ON localized_payment_methods(is_active)"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_country_region ON country_configs(region)"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_tz_country ON timezone_configs(country_code)"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_locale_settings_tenant ON localization_settings(tenant_id)"
)
conn.commit()
logger.info("Localization tables initialized successfully")
except Exception as e:
logger.error(f"Error initializing localization tables: {e}")
raise
finally:
self._close_if_file_db(conn)
def _init_default_data(self):
conn = self._get_connection()
try:
cursor = conn.cursor()
for code, config in self.DEFAULT_LANGUAGES.items():
cursor.execute(
"""
INSERT OR IGNORE INTO language_configs
(code, name, name_local, is_rtl, is_active, is_default, fallback_language,
date_format, time_format, datetime_format, number_format, currency_format,
first_day_of_week, calendar_type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
code.value,
config["name"],
config["name_local"],
int(config["is_rtl"]),
1,
1 if code == LanguageCode.EN else 0,
"en" if code != LanguageCode.EN else None,
config["date_format"],
config["time_format"],
config["datetime_format"],
config["number_format"],
config["currency_format"],
config["first_day_of_week"],
config["calendar_type"],
),
)
for region_code, config in self.DEFAULT_DATA_CENTERS.items():
dc_id = str(uuid.uuid4())
cursor.execute(
"""
INSERT OR IGNORE INTO data_centers
(id, region_code, name, location, endpoint, priority, supported_regions, capabilities)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
dc_id,
region_code.value,
config["name"],
config["location"],
config["endpoint"],
config["priority"],
json.dumps(config["supported_regions"]),
json.dumps(config["capabilities"]),
),
)
for provider, config in self.DEFAULT_PAYMENT_METHODS.items():
pm_id = str(uuid.uuid4())
cursor.execute(
"""
INSERT OR IGNORE INTO localized_payment_methods
(id, provider, name, name_local, supported_countries,
supported_currencies, is_active, display_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
pm_id,
provider.value,
config["name"],
json.dumps(config["name_local"]),
json.dumps(config["supported_countries"]),
json.dumps(config["supported_currencies"]),
1,
config["display_order"],
),
)
for code, config in self.DEFAULT_COUNTRIES.items():
cursor.execute(
"""
INSERT OR IGNORE INTO country_configs
(code, code3, name, name_local, region, default_language, supported_languages,
default_currency, supported_currencies, timezone, calendar_type, vat_rate)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
code,
code,
config["name"],
json.dumps(config["name_local"]),
config["region"],
config["default_language"],
json.dumps(config["supported_languages"]),
config["default_currency"],
json.dumps(config["supported_currencies"]),
config["timezone"],
config["calendar_type"],
config["vat_rate"],
),
)
conn.commit()
logger.info("Default localization data initialized")
except Exception as e:
logger.error(f"Error initializing default localization data: {e}")
finally:
self._close_if_file_db(conn)
def get_translation(
self, key: str, language: str, namespace: str = "common", fallback: bool = True
) -> str | None:
conn = self._get_connection()
try:
cursor = conn.cursor()
cursor.execute(
"SELECT value FROM translations WHERE key = ? AND language = ? AND namespace = ?",
(key, language, namespace),
)
row = cursor.fetchone()
if row:
return row["value"]
if fallback:
lang_config = self.get_language_config(language)
if lang_config and lang_config.fallback_language:
return self.get_translation(
key, lang_config.fallback_language, namespace, False
)
if language != "en":
return self.get_translation(key, "en", namespace, False)
return None
finally:
self._close_if_file_db(conn)
def set_translation(
self,
key: str,
language: str,
value: str,
namespace: str = "common",
context: str | None = None,
) -> Translation:
conn = self._get_connection()
try:
translation_id = str(uuid.uuid4())
now = datetime.now()
cursor = conn.cursor()
cursor.execute(
"""
INSERT INTO translations
(id, key, language, value, namespace, context, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(key, language, namespace) DO UPDATE SET
value = excluded.value,
context = excluded.context,
updated_at = excluded.updated_at,
is_reviewed = 0
""",
(translation_id, key, language, value, namespace, context, now, now),
)
conn.commit()
return self._get_translation_internal(conn, key, language, namespace)
finally:
self._close_if_file_db(conn)
def _get_translation_internal(
self, conn: sqlite3.Connection, key: str, language: str, namespace: str
) -> Translation | None:
cursor = conn.cursor()
cursor.execute(
"SELECT * FROM translations WHERE key = ? AND language = ? AND namespace = ?",
(key, language, namespace),
)
row = cursor.fetchone()
if row:
return self._row_to_translation(row)
return None
def delete_translation(self, key: str, language: str, namespace: str = "common") -> bool:
conn = self._get_connection()
try:
cursor = conn.cursor()
cursor.execute(
"DELETE FROM translations WHERE key = ? AND language = ? AND namespace = ?",
(key, language, namespace),
)
conn.commit()
return cursor.rowcount > 0
finally:
self._close_if_file_db(conn)
def list_translations(
self,
language: str | None = None,
namespace: str | None = None,
limit: int = 1000,
offset: int = 0,
) -> list[Translation]:
conn = self._get_connection()
try:
cursor = conn.cursor()
query = "SELECT * FROM translations WHERE 1=1"
params = []
if language:
query += " AND language = ?"
params.append(language)
if namespace:
query += " AND namespace = ?"
params.append(namespace)
query += " ORDER BY namespace, key LIMIT ? OFFSET ?"
params.extend([limit, offset])
cursor.execute(query, params)
rows = cursor.fetchall()
return [self._row_to_translation(row) for row in rows]
finally:
self._close_if_file_db(conn)
def get_language_config(self, code: str) -> LanguageConfig | None:
conn = self._get_connection()
try:
cursor = conn.cursor()
cursor.execute("SELECT * FROM language_configs WHERE code = ?", (code,))
row = cursor.fetchone()
if row:
return self._row_to_language_config(row)
return None
finally:
self._close_if_file_db(conn)
def list_language_configs(self, active_only: bool = True) -> list[LanguageConfig]:
conn = self._get_connection()
try:
cursor = conn.cursor()
query = "SELECT * FROM language_configs"
if active_only:
query += " WHERE is_active = 1"
query += " ORDER BY name"
cursor.execute(query)
rows = cursor.fetchall()
return [self._row_to_language_config(row) for row in rows]
finally:
self._close_if_file_db(conn)
def get_data_center(self, dc_id: str) -> DataCenter | None:
conn = self._get_connection()
try:
cursor = conn.cursor()
cursor.execute("SELECT * FROM data_centers WHERE id = ?", (dc_id,))
row = cursor.fetchone()
if row:
return self._row_to_data_center(row)
return None
finally:
self._close_if_file_db(conn)
def get_data_center_by_region(self, region_code: str) -> DataCenter | None:
conn = self._get_connection()
try:
cursor = conn.cursor()
cursor.execute("SELECT * FROM data_centers WHERE region_code = ?", (region_code,))
row = cursor.fetchone()
if row:
return self._row_to_data_center(row)
return None
finally:
self._close_if_file_db(conn)
def list_data_centers(
self, status: str | None = None, region: str | None = None
) -> list[DataCenter]:
conn = self._get_connection()
try:
cursor = conn.cursor()
query = "SELECT * FROM data_centers WHERE 1=1"
params = []
if status:
query += " AND status = ?"
params.append(status)
if region:
query += " AND supported_regions LIKE ?"
params.append(f'%"{region}"%')
query += " ORDER BY priority"
cursor.execute(query, params)
rows = cursor.fetchall()
return [self._row_to_data_center(row) for row in rows]
finally:
self._close_if_file_db(conn)
def get_tenant_data_center(self, tenant_id: str) -> TenantDataCenterMapping | None:
conn = self._get_connection()
try:
cursor = conn.cursor()
cursor.execute(
"SELECT * FROM tenant_data_center_mappings WHERE tenant_id = ?", (tenant_id,)
)
row = cursor.fetchone()
if row:
return self._row_to_tenant_dc_mapping(row)
return None
finally:
self._close_if_file_db(conn)
def set_tenant_data_center(
self, tenant_id: str, region_code: str, data_residency: str = "regional"
) -> TenantDataCenterMapping:
conn = self._get_connection()
try:
cursor = conn.cursor()
cursor.execute(
"""
SELECT * FROM data_centers WHERE supported_regions LIKE ? AND status = 'active'
ORDER BY priority LIMIT 1
""",
(f'%"{region_code}"%',),
)
row = cursor.fetchone()
if not row:
cursor.execute("""
SELECT * FROM data_centers WHERE supported_regions LIKE '%"global"%' AND status = 'active'
ORDER BY priority LIMIT 1
""")
row = cursor.fetchone()
if not row:
raise ValueError(f"No data center available for region: {region_code}")
primary_dc_id = row["id"]
cursor.execute(
"""
SELECT * FROM data_centers WHERE id != ? AND status = 'active' ORDER BY priority LIMIT 1
""",
(primary_dc_id,),
)
secondary_row = cursor.fetchone()
secondary_dc_id = secondary_row["id"] if secondary_row else None
mapping_id = str(uuid.uuid4())
now = datetime.now()
cursor.execute(
"""
INSERT INTO tenant_data_center_mappings
(id, tenant_id, primary_dc_id, secondary_dc_id, region_code,
data_residency, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(tenant_id) DO UPDATE SET
primary_dc_id = excluded.primary_dc_id,
secondary_dc_id = excluded.secondary_dc_id,
region_code = excluded.region_code,
data_residency = excluded.data_residency,
updated_at = excluded.updated_at
""",
(
mapping_id,
tenant_id,
primary_dc_id,
secondary_dc_id,
region_code,
data_residency,
now,
now,
),
)
conn.commit()
return self.get_tenant_data_center(tenant_id)
finally:
self._close_if_file_db(conn)
def get_payment_method(self, provider: str) -> LocalizedPaymentMethod | None:
conn = self._get_connection()
try:
cursor = conn.cursor()
cursor.execute(
"SELECT * FROM localized_payment_methods WHERE provider = ?", (provider,)
)
row = cursor.fetchone()
if row:
return self._row_to_payment_method(row)
return None
finally:
self._close_if_file_db(conn)
def list_payment_methods(
self, country_code: str | None = None, currency: str | None = None, active_only: bool = True
) -> list[LocalizedPaymentMethod]:
conn = self._get_connection()
try:
cursor = conn.cursor()
query = "SELECT * FROM localized_payment_methods WHERE 1=1"
params = []
if active_only:
query += " AND is_active = 1"
if country_code:
query += " AND (supported_countries LIKE ? OR supported_countries LIKE '%\"*\"%')"
params.append(f'%"{country_code}"%')
if currency:
query += " AND supported_currencies LIKE ?"
params.append(f'%"{currency}"%')
query += " ORDER BY display_order"
cursor.execute(query, params)
rows = cursor.fetchall()
return [self._row_to_payment_method(row) for row in rows]
finally:
self._close_if_file_db(conn)
def get_localized_payment_methods(
self, country_code: str, language: str = "en"
) -> list[dict[str, Any]]:
methods = self.list_payment_methods(country_code=country_code)
result = []
for method in methods:
name_local = method.name_local.get(language, method.name)
result.append(
{
"id": method.id,
"provider": method.provider,
"name": name_local,
"icon_url": method.icon_url,
"min_amount": method.min_amount,
"max_amount": method.max_amount,
"supported_currencies": method.supported_currencies,
}
)
return result
def get_country_config(self, code: str) -> CountryConfig | None:
conn = self._get_connection()
try:
cursor = conn.cursor()
cursor.execute("SELECT * FROM country_configs WHERE code = ?", (code,))
row = cursor.fetchone()
if row:
return self._row_to_country_config(row)
return None
finally:
self._close_if_file_db(conn)
def list_country_configs(
self, region: str | None = None, active_only: bool = True
) -> list[CountryConfig]:
conn = self._get_connection()
try:
cursor = conn.cursor()
query = "SELECT * FROM country_configs WHERE 1=1"
params = []
if active_only:
query += " AND is_active = 1"
if region:
query += " AND region = ?"
params.append(region)
query += " ORDER BY name"
cursor.execute(query, params)
rows = cursor.fetchall()
return [self._row_to_country_config(row) for row in rows]
finally:
self._close_if_file_db(conn)
def format_datetime(
self,
dt: datetime,
language: str = "en",
timezone: str | None = None,
format_type: str = "datetime",
) -> str:
try:
if timezone and PYTZ_AVAILABLE:
tz = pytz.timezone(timezone)
if dt.tzinfo is None:
dt = pytz.UTC.localize(dt)
dt = dt.astimezone(tz)
lang_config = self.get_language_config(language)
if not lang_config:
lang_config = self.get_language_config("en")
if format_type == "date":
fmt = lang_config.date_format if lang_config else "%Y-%m-%d"
elif format_type == "time":
fmt = lang_config.time_format if lang_config else "%H:%M"
else:
fmt = lang_config.datetime_format if lang_config else "%Y-%m-%d %H:%M"
if BABEL_AVAILABLE:
try:
locale = Locale.parse(language.replace("_", "-"))
if format_type == "date":
return dates.format_date(dt, locale=locale)
elif format_type == "time":
return dates.format_time(dt, locale=locale)
else:
return dates.format_datetime(dt, locale=locale)
except (ValueError, AttributeError):
pass
return dt.strftime(fmt)
except Exception as e:
logger.error(f"Error formatting datetime: {e}")
return dt.strftime("%Y-%m-%d %H:%M")
def format_number(
self, number: float, language: str = "en", decimal_places: int | None = None
) -> str:
try:
if BABEL_AVAILABLE:
try:
locale = Locale.parse(language.replace("_", "-"))
return numbers.format_decimal(
number, locale=locale, decimal_quantization=(decimal_places is not None)
)
except (ValueError, AttributeError):
pass
if decimal_places is not None:
return f"{number:,.{decimal_places}f}"
return f"{number:,}"
except Exception as e:
logger.error(f"Error formatting number: {e}")
return str(number)
def format_currency(self, amount: float, currency: str, language: str = "en") -> str:
try:
if BABEL_AVAILABLE:
try:
locale = Locale.parse(language.replace("_", "-"))
return numbers.format_currency(amount, currency, locale=locale)
except (ValueError, AttributeError):
pass
return f"{currency} {amount:,.2f}"
except Exception as e:
logger.error(f"Error formatting currency: {e}")
return f"{currency} {amount:.2f}"
def convert_timezone(self, dt: datetime, from_tz: str, to_tz: str) -> datetime:
try:
if PYTZ_AVAILABLE:
from_zone = pytz.timezone(from_tz)
to_zone = pytz.timezone(to_tz)
if dt.tzinfo is None:
dt = from_zone.localize(dt)
return dt.astimezone(to_zone)
return dt
except Exception as e:
logger.error(f"Error converting timezone: {e}")
return dt
def get_calendar_info(self, calendar_type: str, year: int, month: int) -> dict[str, Any]:
import calendar
cal = calendar.Calendar()
month_days = cal.monthdayscalendar(year, month)
return {
"calendar_type": calendar_type,
"year": year,
"month": month,
"month_name": calendar.month_name[month],
"days_in_month": calendar.monthrange(year, month)[1],
"first_day_of_week": calendar.monthrange(year, month)[0],
"weeks": month_days,
}
def get_localization_settings(self, tenant_id: str) -> LocalizationSettings | None:
conn = self._get_connection()
try:
cursor = conn.cursor()
cursor.execute("SELECT * FROM localization_settings WHERE tenant_id = ?", (tenant_id,))
row = cursor.fetchone()
if row:
return self._row_to_localization_settings(row)
return None
finally:
self._close_if_file_db(conn)
def create_localization_settings(
self,
tenant_id: str,
default_language: str = "en",
supported_languages: list[str] | None = None,
default_currency: str = "USD",
supported_currencies: list[str] | None = None,
default_timezone: str = "UTC",
region_code: str = "global",
data_residency: str = "regional",
) -> LocalizationSettings:
conn = self._get_connection()
try:
settings_id = str(uuid.uuid4())
now = datetime.now()
supported_languages = supported_languages or [default_language]
supported_currencies = supported_currencies or [default_currency]
lang_config = self.get_language_config(default_language)
cursor = conn.cursor()
cursor.execute(
"""
INSERT INTO localization_settings
(id, tenant_id, default_language, supported_languages, default_currency, supported_currencies,
default_timezone, default_date_format, default_time_format, default_number_format, calendar_type,
first_day_of_week, region_code, data_residency, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
settings_id,
tenant_id,
default_language,
json.dumps(supported_languages),
default_currency,
json.dumps(supported_currencies),
default_timezone,
lang_config.date_format if lang_config else "%Y-%m-%d",
lang_config.time_format if lang_config else "%H:%M",
lang_config.number_format if lang_config else "#,##0.##",
lang_config.calendar_type if lang_config else CalendarType.GREGORIAN.value,
lang_config.first_day_of_week if lang_config else 1,
region_code,
data_residency,
now,
now,
),
)
conn.commit()
return self.get_localization_settings(tenant_id)
finally:
self._close_if_file_db(conn)
def update_localization_settings(self, tenant_id: str, **kwargs) -> LocalizationSettings | None:
conn = self._get_connection()
try:
settings = self.get_localization_settings(tenant_id)
if not settings:
return None
updates = []
params = []
allowed_fields = [
"default_language",
"supported_languages",
"default_currency",
"supported_currencies",
"default_timezone",
"default_date_format",
"default_time_format",
"default_number_format",
"calendar_type",
"first_day_of_week",
"region_code",
"data_residency",
]
for key, value in kwargs.items():
if key in allowed_fields:
updates.append(f"{key} = ?")
if key in ["supported_languages", "supported_currencies"]:
params.append(json.dumps(value) if value else "[]")
elif key == "first_day_of_week":
params.append(int(value))
else:
params.append(value)
if not updates:
return settings
updates.append("updated_at = ?")
params.append(datetime.now())
params.append(tenant_id)
cursor = conn.cursor()
cursor.execute(
f"UPDATE localization_settings SET {', '.join(updates)} WHERE tenant_id = ?", params
)
conn.commit()
return self.get_localization_settings(tenant_id)
finally:
self._close_if_file_db(conn)
def detect_user_preferences(
self, accept_language: str | None = None, ip_country: str | None = None
) -> dict[str, str]:
preferences = {"language": "en", "country": "US", "timezone": "UTC", "currency": "USD"}
if accept_language:
langs = accept_language.split(",")
for lang in langs:
lang_code = lang.split(";")[0].strip().replace("-", "_")
lang_config = self.get_language_config(lang_code)
if lang_config and lang_config.is_active:
preferences["language"] = lang_code
break
if ip_country:
country = self.get_country_config(ip_country)
if country:
preferences["country"] = ip_country
preferences["currency"] = country.default_currency
preferences["timezone"] = country.timezone
if country.default_language not in preferences["language"]:
preferences["language"] = country.default_language
return preferences
def _row_to_translation(self, row: sqlite3.Row) -> Translation:
return Translation(
id=row["id"],
key=row["key"],
language=row["language"],
value=row["value"],
namespace=row["namespace"],
context=row["context"],
created_at=(
datetime.fromisoformat(row["created_at"])
if isinstance(row["created_at"], str)
else row["created_at"]
),
updated_at=(
datetime.fromisoformat(row["updated_at"])
if isinstance(row["updated_at"], str)
else row["updated_at"]
),
is_reviewed=bool(row["is_reviewed"]),
reviewed_by=row["reviewed_by"],
reviewed_at=(
datetime.fromisoformat(row["reviewed_at"])
if row["reviewed_at"] and isinstance(row["reviewed_at"], str)
else row["reviewed_at"]
),
)
def _row_to_language_config(self, row: sqlite3.Row) -> LanguageConfig:
return LanguageConfig(
code=row["code"],
name=row["name"],
name_local=row["name_local"],
is_rtl=bool(row["is_rtl"]),
is_active=bool(row["is_active"]),
is_default=bool(row["is_default"]),
fallback_language=row["fallback_language"],
date_format=row["date_format"],
time_format=row["time_format"],
datetime_format=row["datetime_format"],
number_format=row["number_format"],
currency_format=row["currency_format"],
first_day_of_week=row["first_day_of_week"],
calendar_type=row["calendar_type"],
)
def _row_to_data_center(self, row: sqlite3.Row) -> DataCenter:
return DataCenter(
id=row["id"],
region_code=row["region_code"],
name=row["name"],
location=row["location"],
endpoint=row["endpoint"],
status=row["status"],
priority=row["priority"],
supported_regions=json.loads(row["supported_regions"] or "[]"),
capabilities=json.loads(row["capabilities"] or "{}"),
created_at=(
datetime.fromisoformat(row["created_at"])
if isinstance(row["created_at"], str)
else row["created_at"]
),
updated_at=(
datetime.fromisoformat(row["updated_at"])
if isinstance(row["updated_at"], str)
else row["updated_at"]
),
)
def _row_to_tenant_dc_mapping(self, row: sqlite3.Row) -> TenantDataCenterMapping:
return TenantDataCenterMapping(
id=row["id"],
tenant_id=row["tenant_id"],
primary_dc_id=row["primary_dc_id"],
secondary_dc_id=row["secondary_dc_id"],
region_code=row["region_code"],
data_residency=row["data_residency"],
created_at=(
datetime.fromisoformat(row["created_at"])
if isinstance(row["created_at"], str)
else row["created_at"]
),
updated_at=(
datetime.fromisoformat(row["updated_at"])
if isinstance(row["updated_at"], str)
else row["updated_at"]
),
)
def _row_to_payment_method(self, row: sqlite3.Row) -> LocalizedPaymentMethod:
return LocalizedPaymentMethod(
id=row["id"],
provider=row["provider"],
name=row["name"],
name_local=json.loads(row["name_local"] or "{}"),
supported_countries=json.loads(row["supported_countries"] or "[]"),
supported_currencies=json.loads(row["supported_currencies"] or "[]"),
is_active=bool(row["is_active"]),
config=json.loads(row["config"] or "{}"),
icon_url=row["icon_url"],
display_order=row["display_order"],
min_amount=row["min_amount"],
max_amount=row["max_amount"],
created_at=(
datetime.fromisoformat(row["created_at"])
if isinstance(row["created_at"], str)
else row["created_at"]
),
updated_at=(
datetime.fromisoformat(row["updated_at"])
if isinstance(row["updated_at"], str)
else row["updated_at"]
),
)
def _row_to_country_config(self, row: sqlite3.Row) -> CountryConfig:
return CountryConfig(
code=row["code"],
code3=row["code3"],
name=row["name"],
name_local=json.loads(row["name_local"] or "{}"),
region=row["region"],
default_language=row["default_language"],
supported_languages=json.loads(row["supported_languages"] or "[]"),
default_currency=row["default_currency"],
supported_currencies=json.loads(row["supported_currencies"] or "[]"),
timezone=row["timezone"],
calendar_type=row["calendar_type"],
date_format=row["date_format"],
time_format=row["time_format"],
number_format=row["number_format"],
address_format=row["address_format"],
phone_format=row["phone_format"],
vat_rate=row["vat_rate"],
is_active=bool(row["is_active"]),
)
def _row_to_localization_settings(self, row: sqlite3.Row) -> LocalizationSettings:
return LocalizationSettings(
id=row["id"],
tenant_id=row["tenant_id"],
default_language=row["default_language"],
supported_languages=json.loads(row["supported_languages"] or '["en"]'),
default_currency=row["default_currency"],
supported_currencies=json.loads(row["supported_currencies"] or '["USD"]'),
default_timezone=row["default_timezone"],
default_date_format=row["default_date_format"],
default_time_format=row["default_time_format"],
default_number_format=row["default_number_format"],
calendar_type=row["calendar_type"],
first_day_of_week=row["first_day_of_week"],
region_code=row["region_code"],
data_residency=row["data_residency"],
created_at=(
datetime.fromisoformat(row["created_at"])
if isinstance(row["created_at"], str)
else row["created_at"]
),
updated_at=(
datetime.fromisoformat(row["updated_at"])
if isinstance(row["updated_at"], str)
else row["updated_at"]
),
)
_localization_manager = None
def get_localization_manager(db_path: str = "insightflow.db") -> LocalizationManager:
global _localization_manager
if _localization_manager is None:
_localization_manager = LocalizationManager(db_path)
return _localization_manager