const LANDING_LOCALE_STORAGE_KEY = 'fortec.locale';
const LANDING_LOCALE_COOKIE_NAME = 'fortec.locale';
const LANDING_LOCALE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
const LANDING_DEFAULT_LOCALE = 'en';
const LANDING_TRANSLATABLE_PROPS = ['placeholder', 'title', 'aria-label', 'aria-description', 'alt'];
const LANDING_TEXT_PATTERN = /[A-Za-zÀ-ÖØ-öø-ÿ]/;
const LANDING_NON_TRANSLATABLE_PATTERNS = [
  /^[\d\s.,:/%+()\-−]+$/,
  /^\/[\w/-]+$/,
  /^[A-Z]{2,10}(?:\/[A-Z]{2,10})?$/,
];

const landingPtTranslations = {
  'Fortec — The fortress for modern wealth': 'Fortec — A fortaleza da riqueza moderna',
  'Platform': 'Plataforma',
  'AI': 'IA',
  'Pricing': 'Preços',
  'Security': 'Segurança',
  'Sign in': 'Entrar',
  'Start free': 'Começar grátis',
  'Now in open beta · EU + US': 'Agora em beta aberta · UE + EUA',
  'The fortress for': 'A fortaleza da',
  'modern wealth': 'riqueza moderna',
  'Investments, banking, and rentals — unified by an AI that understands markets, not just numbers. Institutional-grade analytics, built for how you actually invest.': 'Investimentos, banca e arrendamentos — unificados por uma IA que entende mercados, não apenas números. Análise de nível institucional, feita para a forma como realmente investe.',
  'Watch 2-min demo': 'Ver demo de 2 min',
  'Connects with': 'Liga-se a',
  'accounts reconciled': 'contas reconciliadas',
  'models live': 'modelos ativos',
  'Why Fortec': 'Porquê Fortec',
  'Three problems. One ledger.': 'Três problemas. Um só registo.',
  'Scattered wealth.': 'Património disperso.',
  'Your money lives in six apps. Your decisions suffer. Fortec merges brokerages, banks, and property ledgers into one net-worth view that reconciles itself nightly.': 'O seu dinheiro vive em seis aplicações. As suas decisões sofrem com isso. A Fortec junta corretoras, bancos e registos imobiliários numa única visão do património líquido, reconciliada todas as noites.',
  'Noise, not signal.': 'Ruído, não sinal.',
  'Markets shout. Most dashboards echo. Fortec listens — with seven models running per-ticker and an ensemble that only calls when it has conviction.': 'Os mercados gritam. A maioria dos dashboards apenas ecoa. A Fortec escuta — com sete modelos por ticker e um ensemble que só sinaliza quando tem convicção.',
  'Numbers without narrative.': 'Números sem narrativa.',
  "Balances aren't strategy. Fortec explains — in two paragraphs, grounded in the same data — what moved, what changed, and what to do next.": 'Saldos não são estratégia. A Fortec explica — em dois parágrafos, sustentados pelos mesmos dados — o que mexeu, o que mudou e o que fazer a seguir.',
  'One platform': 'Uma plataforma',
  'Every asset, every account — one ledger.': 'Cada ativo, cada conta — um só registo.',
  'Five asset classes, computed with the same rigor. Switch the lens; the data stays consistent.': 'Cinco classes de ativos, calculadas com o mesmo rigor. Mude a perspetiva; os dados mantêm-se consistentes.',
  'The AI inside Fortec': 'A IA dentro da Fortec',
  'Seven models. One thesis per ticker.': 'Sete modelos. Uma tese por ticker.',
  'Every holding runs through a ranker, a volatility forecaster, a regime detector, a sector model, an ensemble, a news feature extractor, and a Monte Carlo simulator. The thesis cites its own working.': 'Cada posição passa por um ranker, um modelo de volatilidade, um detetor de regime, um modelo setorial, um ensemble, um extrator de sinais de notícias e um simulador de Monte Carlo. A tese cita o próprio racional.',
  'Model stack': 'Pilha de modelos',
  'Ranker': 'Ranker',
  'Gradient-boosted trees, 430 features': 'Árvores gradient-boosted, 430 variáveis',
  'Volatility': 'Volatilidade',
  'Darts · GARCH + Prophet': 'Darts · GARCH + Prophet',
  'Regime': 'Regime',
  'HMM + change-point detection': 'HMM + deteção de mudanças de regime',
  'Sector rotation': 'Rotação setorial',
  'Relative-strength factor model': 'Modelo fatorial de força relativa',
  'Ensemble': 'Ensemble',
  'Stacked meta-learner, voting': 'Meta-modelo empilhado com votação',
  'News features': 'Sinais de notícias',
  'Entity + sentiment + novelty': 'Entidades + sentimento + novidade',
  'Monte Carlo': 'Monte Carlo',
  '10,000-path forward simulator': 'Simulador forward com 10.000 trajetórias',
  'live': 'ativo',
  'Built for how you actually invest': 'Feita para a forma como realmente investe',
  'Three people. One platform.': 'Três perfis. Uma plataforma.',
  'Security & trust': 'Segurança e confiança',
  'Bank-grade, from day one.': 'Nível bancário desde o primeiro dia.',
  'AES-256 encryption': 'Encriptação AES-256',
  'Data encrypted at rest and in transit. Keys rotated quarterly.': 'Dados cifrados em repouso e em trânsito. Chaves rodadas trimestralmente.',
  'Read-only broker links': 'Ligações de corretora apenas de leitura',
  'We never hold trade authority unless you explicitly grant it.': 'Nunca detemos autoridade de negociação, exceto se a conceder explicitamente.',
  'SOC 2 on the roadmap': 'SOC 2 no roteiro',
  'Type 1 audit in progress. Public status page when live.': 'Auditoria Type 1 em curso. Página pública de estado quando estiver ativa.',
  'EU-hosted': 'Alojado na UE',
  'Frankfurt and Dublin. GDPR-compliant data residency by default.': 'Frankfurt e Dublin. Residência de dados compatível com o RGPD por defeito.',
  'PSD2 open banking': 'Open banking PSD2',
  'Licensed AIS provider under Article 33 across EU jurisdictions.': 'Fornecedor AIS licenciado ao abrigo do Artigo 33 em jurisdições da UE.',
  'Free forever for tracking.': 'Grátis para sempre para acompanhar.',
  "Pro when you're ready to act. No hidden fees. Cancel any time.": 'Pro quando estiver pronto para agir. Sem taxas escondidas. Cancele quando quiser.',
  'Fortec Free': 'Fortec Free',
  'Unlimited accounts. No card required.': 'Contas ilimitadas. Sem cartão.',
  'Everything you need to see your full net-worth picture in one place.': 'Tudo o que precisa para ver o seu património líquido completo num só lugar.',
  'Portfolio tracking · all asset classes': 'Acompanhamento de portefólio · todas as classes de ativos',
  'Banking sync · unlimited accounts': 'Sincronização bancária · contas ilimitadas',
  'Rental property ledgers': 'Registos de imóveis arrendados',
  'Basic analytics and exports': 'Análise básica e exportações',
  'Fortec Pro': 'Fortec Pro',
  'Billed annually · €228/year': 'Cobrado anualmente · 228 €/ano',
  'The seven-model engine, Monte Carlo, and unlimited brokers.': 'O motor de sete modelos, Monte Carlo e corretoras ilimitadas.',
  'AI ranker, regime, and ensemble signals': 'Sinais de ranker, regime e ensemble por IA',
  'Monte Carlo outcome modeling': 'Modelação de cenários com Monte Carlo',
  'Unlimited broker connections': 'Ligações ilimitadas a corretoras',
  'Priority support · 24h SLA': 'Suporte prioritário · SLA de 24h',
  'Get Pro': 'Obter Pro',
  'See full pricing': 'Ver preços completos',
  'In use': 'Em uso',
  'From people managing real money.': 'De pessoas que gerem dinheiro real.',
  'Start tracking today': 'Comece a acompanhar hoje',
  'Build your fortress.': 'Construa a sua fortaleza.',
  "Free forever for tracking. Pro when you're ready to act.": 'Grátis para sempre para acompanhar. Pro quando estiver pronto para agir.',
  'Get started': 'Começar',
  'The fortress for modern wealth. Investments, banking, and rentals, unified by AI.': 'A fortaleza da riqueza moderna. Investimentos, banca e arrendamentos, unidos por IA.',
  'Email': 'Email',
  'Subscribe': 'Subscrever',
  'Product': 'Produto',
  'Investments': 'Investimentos',
  'Banking': 'Banca',
  'Rentals': 'Arrendamentos',
  'AI engine': 'Motor de IA',
  'Company': 'Empresa',
  'About': 'Sobre',
  'Careers': 'Carreiras',
  'Press': 'Imprensa',
  'Contact': 'Contacto',
  'Resources': 'Recursos',
  'Docs': 'Documentação',
  'API': 'API',
  'Changelog': 'Registo de alterações',
  'Status': 'Estado',
  'Legal': 'Legal',
  'Terms': 'Termos',
  'Privacy': 'Privacidade',
  'Compliance': 'Conformidade',
  'Language': 'Idioma',
  'Assets tracked across private beta': 'Ativos acompanhados durante a beta privada',
  'Models voting per ticker, every hour': 'Modelos a votar por ticker, a cada hora',
  'Reconciliation accuracy, YTD': 'Precisão de reconciliação, YTD',
  'Brokers and banks connected on day one': 'Corretoras e bancos ligados desde o primeiro dia',
  'Ship / week 07 · April 2026': 'Entrega / semana 07 · abril de 2026',
  'Ensemble v3 just shipped. Every holding now carries a thesis it can defend.': 'O Ensemble v3 acabou de ser lançado. Cada posição passa agora a ter uma tese que consegue defender.',
  'Read release notes': 'Ler notas de lançamento',
  'Fortec global network': 'Rede global Fortec',
  'Fortec global network · synced to': 'Rede global Fortec · sincronizada com',
  'exchanges & banks': 'bolsas e bancos',
  'LIVE': 'AO VIVO',
  'Equities': 'Ações',
  'Real Estate': 'Imobiliário',
  'Bonds': 'Obrigações',
  'AI Signals': 'Sinais de IA',
  'Commodities': 'Commodities',
  'Net-Worth Ledger': 'Registo de património líquido',
  'Portfolio value · last 12 months': 'Valor do portefólio · últimos 12 meses',
  'Live': 'Ao vivo',
  'Property': 'Imóvel',
  'Live market ticker': 'Ticker de mercado em direto',
  'AI signal · NVDA': 'Sinal de IA · NVDA',
  'Model ensemble flags a regime change. Consider trimming 15%.': 'O ensemble de modelos sinaliza uma mudança de regime. Considere reduzir 15%.',
  'Confidence 0.82 · 3 of 7 models agree': 'Confiança 0,82 · 3 de 7 modelos concordam',
  'Portfolio curve showing {{gain}}% growth': 'Curva do portefólio com crescimento de {{gain}}%',
  'Seven-model ensemble · {{name}}': 'Ensemble de sete modelos · {{name}}',
  'Computing': 'A calcular',
  'Ranker score': 'Pontuação do ranker',
  'th percentile · S&P 500': 'º percentil · S&P 500',
  'OVERWEIGHT': 'SOBREPONDERAR',
  'UNDERWEIGHT': 'SUBPONDERAR',
  'HOLD': 'MANTER',
  'Volatility band': 'Banda de volatilidade',
  'Darts · 20-day realized + GARCH': 'Darts · realizada a 20 dias + GARCH',
  'BULL': 'ALTA',
  'BEAR': 'BAIXA',
  'NEUTRAL': 'NEUTRO',
  'News sentiment': 'Sentimento das notícias',
  '7-day rolling · 412 articles': 'janela móvel de 7 dias · 412 artigos',
  'Monte Carlo p50': 'Monte Carlo p50',
  '10,000 paths · 90 days · 2σ envelope': '10.000 trajetórias · 90 dias · envelope 2σ',
  'Ensemble thesis': 'Tese do ensemble',
  'Stocks, ETFs, and funds — with an AI that ranks them.': 'Ações, ETFs e fundos — com uma IA que os classifica.',
  'Every holding scored by a seven-model ensemble. The ranker surfaces conviction, not noise — tabular, reproducible, and shown with its working.': 'Cada posição é pontuada por um ensemble de sete modelos. O ranker mostra convicção, não ruído — de forma tabular, reprodutível e com o racional exposto.',
  'ML stock ranker with confidence scoring': 'Ranker de ações com ML e pontuação de confiança',
  'Volatility forecasting (Darts)': 'Previsão de volatilidade (Darts)',
  'Sector rotation & regime detection': 'Rotação setorial e deteção de regime',
  'Every account, reconciled automatically.': 'Cada conta, reconciliada automaticamente.',
  'Open-banking connections across EU and US. Categorized, deduplicated, and reconciled against your investments and rental ledgers — one ledger, one truth.': 'Ligações de open banking na UE e nos EUA. Categorizadas, deduplicadas e reconciliadas com os seus investimentos e registos de arrendamento — um registo, uma só verdade.',
  'PSD2 open banking (EU) and Plaid (US)': 'Open banking PSD2 (UE) e Plaid (EUA)',
  'Auto-categorization with ML rules': 'Categorização automática com regras de ML',
  'Cashflow forecasting to 90 days': 'Previsão de cashflow a 90 dias',
  'Custom budgets with envelope carry-over': 'Orçamentos personalizados com saldo transitado',
  'Collect rent, meter utilities, close the books.': 'Receba rendas, monitorize consumos, feche as contas.',
  'Track tenants, payments, and utility telemetry across every unit. Bank statement lines auto-match to invoices; late rent shows before you notice.': 'Acompanhe inquilinos, pagamentos e telemetria de utilidades em cada unidade. Linhas do extrato bancário ligam-se automaticamente às faturas; rendas em atraso aparecem antes de dar por isso.',
  'Tenant roster and lease management': 'Gestão de inquilinos e contratos',
  'Utility telemetry (water, gas, power)': 'Telemetria de utilidades (água, gás, energia)',
  'Bank-to-invoice reconciliation': 'Reconciliação banco-fatura',
  'Per-unit P&L and tax-ready exports': 'P&L por unidade e exportações prontas para impostos',
  'Wallets and exchanges, in one ledger.': 'Carteiras e exchanges, num só registo.',
  'Read-only keys connect to major exchanges and wallets. Cost basis is computed per-lot; P&L flows into your overall portfolio with the same rigor as equities.': 'Chaves apenas de leitura ligam-se às principais exchanges e carteiras. O custo base é calculado por lote; o P&L entra no portefólio global com o mesmo rigor das ações.',
  'Binance, Kraken, Coinbase, wallet addresses': 'Binance, Kraken, Coinbase, endereços de carteira',
  'FIFO / HIFO / specific-lot cost basis': 'Custo base FIFO / HIFO / por lote específico',
  'Staking rewards tracked as income': 'Recompensas de staking tratadas como rendimento',
  'Tax reports: Portugal, Germany, UK, US': 'Relatórios fiscais: Portugal, Alemanha, Reino Unido, EUA',
  'Properties as first-class assets.': 'Imóveis como ativos de primeira linha.',
  'Every property carries its own valuation curve, mortgage ledger, maintenance log, and yield calculation. Know your true ROI, not just the Zillow number.': 'Cada imóvel tem a sua curva de valorização, registo hipotecário, histórico de manutenção e cálculo de yield. Saiba o seu ROI real, não apenas o número do portal.',
  'Mortgage amortization + interest tracking': 'Amortização da hipoteca + acompanhamento de juros',
  'Maintenance and capex ledger': 'Registo de manutenção e capex',
  'Gross and net yield per property': 'Yield bruto e líquido por imóvel',
  'Valuation updates via market comps': 'Atualizações de valorização por comparáveis de mercado',
  'See how it works': 'Ver como funciona',
  'Holding': 'Posição',
  'Conviction': 'Convicção',
  'Landlord': 'Senhorio',
  '"I own a rental in Sintra."': '"Tenho um arrendamento em Sintra."',
  'Four units, two cities. Rent posts to Fortec the day it clears your bank. Late rent surfaces at the top of the dashboard, with one-tap reminders in Portuguese or English.': 'Quatro unidades, duas cidades. A renda entra na Fortec no dia em que liquida no banco. Rendas em atraso aparecem no topo do dashboard, com lembretes em português ou inglês num toque.',
  'Active trader': 'Trader ativo',
  '"I trade with Alpaca and IBKR."': '"Negocio com Alpaca e IBKR."',
  'One order ticket, smart-routed across your connected brokers for best execution. Positions merge into a single view — no more reconciling two platforms by hand.': 'Um único ticket de ordem, roteado de forma inteligente entre as suas corretoras ligadas para melhor execução. As posições juntam-se numa só vista — sem reconciliar duas plataformas à mão.',
  'Decision support': 'Suporte à decisão',
  '"I want to know what to do next."': '"Quero saber o que fazer a seguir."',
  'The reasoner answers grounded questions — "should I rebalance?", "am I overweight tech?" — with citations to the model outputs behind the answer. No advice without evidence.': 'O reasoner responde a perguntas ancoradas nos dados — "devo rebalancear?", "estou com excesso de tecnologia?" — com citações aos modelos por trás da resposta. Sem conselhos sem evidência.',
  'Rent ledger · October': 'Registo de rendas · outubro',
  '2 days late': '2 dias de atraso',
  'Order ticket · NVDA': 'Ticket de ordem · NVDA',
  'Reasoner': 'Reasoner',
  'Should I rebalance my tech exposure?': 'Devo rebalancear a minha exposição a tecnologia?',
  'Tech is 47% of your equities — 12pp over your target. The ranker still overweights NVDA and MSFT, but regime detector flags sector momentum cooling. Consider trimming AAPL (score 0.74) to your target band.': 'Tecnologia representa 47% das suas ações — 12 p.p. acima do objetivo. O ranker ainda sobrepondera NVDA e MSFT, mas o detetor de regime sinaliza arrefecimento do momentum setorial. Considere reduzir AAPL (score 0,74) até à sua banda-alvo.',
  'CITES: ranker · regime · sector · allocation': 'CITA: ranker · regime · setor · alocação',
  'Retail investor · Lisbon': 'Investidora particular · Lisboa',
  'Landlord · Sintra': 'Senhorio · Sintra',
  'Financial advisor · Porto': 'Consultora financeira · Porto',
  'Fortec is the only place I see my brokerage, my rentals, and my savings in one number — and the only place where that number comes with a reason.': 'A Fortec é o único lugar onde vejo a minha corretora, os meus arrendamentos e as minhas poupanças num só número — e o único onde esse número vem com uma explicação.',
  'I used to run a spreadsheet with six tabs. Fortec reconciled three years of rental payments in an afternoon. Now I know my real yield, not my imagined one.': 'Tinha uma folha de cálculo com seis separadores. A Fortec reconciliou três anos de pagamentos de renda numa tarde. Agora sei a minha yield real, não a imaginada.',
  'The ensemble outputs are the honest kind. It tells me confidence, what models disagreed, and the volatility band. My clients ask better questions because of it.': 'Os outputs do ensemble são do tipo honesto. Dizem-me a confiança, que modelos discordaram e a banda de volatilidade. Os meus clientes fazem perguntas melhores por causa disso.',
  'Show quote {{n}}': 'Mostrar citação {{n}}',
  'Portfolio · 4 properties': 'Portefólio · 4 imóveis',
  'Mortgage outstanding': 'Hipoteca em dívida',
  'Net equity': 'Capital líquido',
  'Gross yield': 'Yield bruto',
  'Net yield (TTM)': 'Yield líquido (TTM)',
  'Occupancy': 'Ocupação',
  '01 · Why Fortec': '01 · Porquê Fortec',
  '02 · One platform': '02 · Uma plataforma',
  '03 · The AI inside Fortec': '03 · A IA dentro da Fortec',
  '04 · Built for how you actually invest': '04 · Feita para a forma como realmente investe',
  '05 · Security & trust': '05 · Segurança e confiança',
  '06 · Pricing': '06 · Preços',
  '07 · In use': '07 · Em uso',
  '+24.3% YTD': '+24,3% YTD',
  '12 accounts reconciled': '12 contas reconciliadas',
  '7 models live': '7 modelos ativos',
  '● live': '● ativo',
  'fortec.app / portfolio': 'fortec.app / portfolio',
  'Crypto': 'Cripto',
  'Real estate': 'Imobiliário',
  'HMM + change-point detector': 'HMM + detetor de mudanças de regime',
  'Trend · neutral': 'Tendência · neutro',
  'Momentum · bull': 'Momentum · alta',
  'Range · mean-revert': 'Intervalo · reversão à média',
  'Margins expanded Q3; services tier supports ranker confidence. Volatility has compressed — ensemble treats current level as fairly priced.': 'As margens expandiram no T3; a camada de serviços sustenta a confiança do ranker. A volatilidade comprimiu-se — o ensemble trata o nível atual como corretamente avaliado.',
  'Five of seven models overweight. Realized vol is elevated — position sizing matters more than entry. Monte Carlo p50 implies +24% over 90 days.': 'Cinco de sete modelos sobreponderam. A volatilidade realizada está elevada — o dimensionamento da posição importa mais do que o ponto de entrada. O p50 de Monte Carlo implica +24% em 90 dias.',
  'Ranker underweight. News features show delivery-miss drag; regime detector flags narrow range. Avoid aggressive adds; hedged exposure only.': 'O ranker subpondera. Os sinais de notícias mostram pressão por falha nas entregas; o detetor de regime sinaliza intervalo estreito. Evite reforços agressivos; apenas exposição com proteção.',
  'Regime detector confirms bull trend post-halving. Funding rates remain sane. Ensemble treats drawdowns > 12% as accumulation.': 'O detetor de regime confirma tendência de alta após o halving. As funding rates mantêm-se saudáveis. O ensemble trata quedas superiores a 12% como acumulação.',
  'PAID': 'PAGO',
  'DUE 2d': 'EM ATRASO 2d',
  'Util {{meter}}%': 'Util. {{meter}}%',
  '€54 water · €38 gas': '€54 água · €38 gás',
  '€41 water · €28 gas': '€41 água · €28 gás',
  '€72 water · €44 gas': '€72 água · €44 gás',
  '€46 water · €31 gas': '€46 água · €31 gás',
  'BUY': 'COMPRA',
  'Side': 'Lado',
  'Qty': 'Qtd.',
  'Limit': 'Limite',
  'Route': 'Rota',
  'SMART': 'SMART',
  'London': 'Londres',
  'Zurich': 'Zurique',
  'Singapore': 'Singapura',
  'Tokyo': 'Tóquio',
  'R$ 812M / min': 'R$ 812M / min',
  '£6.1B / min': '£6,1B / min',
  'CHF 2.3B': 'CHF 2,3B',
  '€4.7B / min': '€4,7B / min',
  '$1.1B / min': '$1,1B / min',
  'S$ 420M': 'S$ 420M',
  'A$ 680M': 'A$ 680M',
  '+4.2% YoY · vs €1,197,600 LY': '+4,2% homólogo · vs €1.197.600 no ano anterior',
  '© 2026 Fortec Labs · Lisbon & Frankfurt': '© 2026 Fortec Labs · Lisboa e Frankfurt',
  'Fortec': 'Fortec',
  'Alpaca': 'Alpaca',
  'Interactive Brokers': 'Interactive Brokers',
  'Binance': 'Binance',
  'Coinbase': 'Coinbase',
  'Millennium BCP': 'Millennium BCP',
  'Revolut': 'Revolut',
  'N26': 'N26',
  'Score': 'Pontuação',
  '· 24h': '· 24h',
  'Mercês 6A': 'Mercês 6A',
  'Mercês 6B': 'Mercês 6B',
  'Cascais 3A': 'Cascais 3A',
  'Mercês 3B': 'Mercês 3B',
  'M. Carvalho': 'M. Carvalho',
  'J. Santos': 'J. Santos',
  'L. Fernandes': 'L. Fernandes',
  '25 sh': '25 ações',
  'IBKR ✓': 'IBKR ✓',
};

function normalizeLandingLocale(value) {
  if (!value) return LANDING_DEFAULT_LOCALE;
  const normalized = String(value).toLowerCase();
  if (normalized.startsWith('pt')) return 'pt';
  if (normalized.startsWith('en')) return 'en';
  return LANDING_DEFAULT_LOCALE;
}

function interpolateLanding(template, variables) {
  if (!variables) return template;
  return Object.entries(variables).reduce(
    (output, [key, value]) => output.replaceAll(`{{${key}}}`, String(value)),
    template,
  );
}

function translateLanding(locale, key, variables) {
  const template = locale === 'pt' ? (landingPtTranslations[key] || key) : key;
  return interpolateLanding(template, variables);
}

function isLandingTranslatableText(text) {
  if (!text || !LANDING_TEXT_PATTERN.test(text)) return false;
  return LANDING_NON_TRANSLATABLE_PATTERNS.every((pattern) => !pattern.test(text));
}

function translateLandingTextValue(value, translate) {
  const trimmed = value.trim();
  if (!trimmed || !isLandingTranslatableText(trimmed)) return value;

  const leadingWhitespace = value.match(/^\s*/)?.[0] || '';
  const trailingWhitespace = value.match(/\s*$/)?.[0] || '';
  return `${leadingWhitespace}${translate(trimmed)}${trailingWhitespace}`;
}

function translateLandingProps(props, translate) {
  let changed = false;
  const nextProps = { ...props };

  LANDING_TRANSLATABLE_PROPS.forEach((propName) => {
    const value = props[propName];
    if (typeof value !== 'string') return;
    const translated = translateLandingTextValue(value, translate);
    if (translated === value) return;
    nextProps[propName] = translated;
    changed = true;
  });

  return changed ? nextProps : props;
}

function translateLandingNode(node, translate) {
  if (typeof node === 'string') {
    return translateLandingTextValue(node, translate);
  }

  if (Array.isArray(node)) {
    return node.map((child, index) => {
      const translatedChild = translateLandingNode(child, translate);
      if (!React.isValidElement(translatedChild)) return translatedChild;
      return translatedChild.key == null
        ? React.cloneElement(translatedChild, { key: index })
        : translatedChild;
    });
  }

  if (!React.isValidElement(node)) {
    return node;
  }

  const props = node.props || {};
  if (props['data-i18n-skip']) {
    return node;
  }

  const translatedProps = translateLandingProps(props, translate);
  const translatedChildren =
    props.children === undefined ? props.children : translateLandingNode(props.children, translate);

  if (translatedProps === props && translatedChildren === props.children) {
    return node;
  }

  return React.cloneElement(node, translatedProps, translatedChildren);
}

const LandingI18nContext = React.createContext(null);

function getStoredLandingLocale() {
  const cookieLocale = document.cookie
    .split('; ')
    .find((item) => item.startsWith(`${LANDING_LOCALE_COOKIE_NAME}=`))
    ?.split('=')[1];

  return normalizeLandingLocale(
    window.localStorage.getItem(LANDING_LOCALE_STORAGE_KEY) ||
      cookieLocale ||
      window.navigator.language,
  );
}

function LandingI18nProvider({ children }) {
  const [locale, setLocaleState] = React.useState(() => getStoredLandingLocale());

  React.useEffect(() => {
    const stored = getStoredLandingLocale();
    if (stored !== locale) {
      setLocaleState(stored);
    }
  }, [locale]);

  React.useEffect(() => {
    document.documentElement.lang = locale === 'pt' ? 'pt-PT' : 'en';
    document.title = translateLanding(locale, 'Fortec — The fortress for modern wealth');
    window.localStorage.setItem(LANDING_LOCALE_STORAGE_KEY, locale);
    document.cookie = `${LANDING_LOCALE_COOKIE_NAME}=${locale}; path=/; max-age=${LANDING_LOCALE_COOKIE_MAX_AGE}; SameSite=Lax`;
  }, [locale]);

  const value = React.useMemo(
    () => ({
      locale,
      setLocale: setLocaleState,
      t(key, variables) {
        return translateLanding(locale, key, variables);
      },
      localeTag: locale === 'pt' ? 'pt-PT' : 'en-US',
    }),
    [locale],
  );

  return (
    <LandingI18nContext.Provider value={value}>
      {children}
    </LandingI18nContext.Provider>
  );
}

function useLandingI18n() {
  const value = React.useContext(LandingI18nContext);
  if (!value) {
    throw new Error('useLandingI18n must be used inside LandingI18nProvider');
  }
  return value;
}

function TranslatedLandingContent({ children }) {
  const { t } = useLandingI18n();
  const translatedChildren = React.useMemo(() => translateLandingNode(children, t), [children, t]);
  return <>{translatedChildren}</>;
}

Object.assign(window, {
  LandingI18nProvider,
  TranslatedLandingContent,
  useLandingI18n,
  landingTranslate: translateLanding,
});
