1701 lines
64 KiB
Python
1701 lines
64 KiB
Python
"""
|
||
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 BaseException:
|
||
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 BaseException:
|
||
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 BaseException:
|
||
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
|