src/Services/I18nService.ps1

# I18nService — Catálogo de strings UI en español e inglés.
#
# Centraliza los strings que el usuario ve para que un solo lugar controle
# qué se muestra. Soporta los locales 'es' y 'en' (Settings.Language).
# Si una key no existe en el locale activo, fallback al 'es' (default histórico
# del proyecto). Si tampoco existe ahí, devuelve `[key]` para que el bug sea
# visible en la UI (preferible a string vacío silencioso).
#
# Uso:
# $svc = [I18nService]::new('es')
# $svc.T('hint.move') # → '↑↓' (mismo en ambos)
# $svc.T('hint.search') # → 'Buscar' o 'Search' según locale
#
# Convención de keys:
# - dot.notation jerárquica (hint.X, prefs.X, status.X, alias.X, tab.X, …)
# - el archivo lee los catálogos lazy via Get-RepoNavI18nCatalog (mantiene
# los hashes data afuera de la clase para no inflarla)

class I18nService {
    [string] $Locale = 'es'

    # Catálogo activo, cacheado en la primera llamada a EnsureCatalog. Se rebuilda
    # cuando cambia Locale.
    hidden [hashtable] $_catalog = $null

    I18nService() { }
    I18nService([string]$lc) {
        $this.SetLocale($lc)
    }

    # Param se llama $lc (no $locale) porque PS classes son case-insensitive en
    # scope: `$locale` chocaría con $this.Locale y rompería la asignación.
    [void] SetLocale([string]$lc) {
        $effective = if ([string]::IsNullOrWhiteSpace($lc)) { 'es' }
                     elseif ($lc -notin @('es', 'en')) { 'es' }
                     else { $lc }
        $this.Locale = $effective
        $this._catalog = $null   # invalidar cache
    }

    # Devuelve el string para una key. Fallback chain:
    # 1) catálogo del locale activo
    # 2) catálogo 'es' (default histórico)
    # 3) literal `[key]` para que el bug sea visible
    [string] T([string]$key) {
        if ([string]::IsNullOrWhiteSpace($key)) { return '' }
        $cat = $this._GetCatalog()
        if ($cat.ContainsKey($key)) { return [string]$cat[$key] }
        # Fallback al español default si la key falta en el locale activo.
        $fallback = Get-RepoNavI18nCatalog -Locale 'es'
        if ($fallback.ContainsKey($key)) { return [string]$fallback[$key] }
        return "[$key]"
    }

    # Versión con interpolación posicional. Útil para mensajes con valores
    # dinámicos. Ej: T('alias.confirmRemove', 'TEST') → "¿Eliminar alias TEST?".
    # Param se llama $values (no $args) porque $args es automatic en PS y
    # llega vacío cuando se usa como nombre formal en métodos de class.
    [string] T([string]$key, [object[]]$values) {
        $template = $this.T($key)
        if ($values -and $values.Count -gt 0) {
            try {
                return [string]::Format($template, $values)
            } catch {
                return $template
            }
        }
        return $template
    }

    hidden [hashtable] _GetCatalog() {
        if ($null -eq $this._catalog) {
            $this._catalog = Get-RepoNavI18nCatalog -Locale $this.Locale
        }
        return $this._catalog
    }
}