src/Langue.ps1
|
# --------------------------------------------------------------------------- # Langue — la sortie parle celle de son lecteur # --------------------------------------------------------------------------- # # LE PROBLEME # # La documentation est en anglais, la sortie des commandes en francais. Un # developpeur a Berlin qui installe ce module lit un README qu'il comprend, puis # recoit des refus et des diagnostics dans une langue qu'il ne parle peut-etre # pas. C'est l'incoherence la plus visible du projet, et celle qui fait # desinstaller a la premiere execution. # # TROIS REGLES # # 1. DEVCTX_LANG a le dernier mot. Un shell, un test, une CI, une capture # d'ecran pour la documentation : il faut pouvoir forcer. # 2. Sinon, la langue du systeme. Personne ne devrait avoir a configurer quoi # que ce soit pour etre compris. # 3. Sinon l'anglais. C'est la langue de repli d'un outil destine a circuler, # et celle de la documentation. # # ET UNE QUATRIEME, POUR LES CLES MANQUANTES # # Une traduction absente rend la cle entre crochets — « [ctx.noGo] » — jamais # une chaine vide. Un message vide se lit comme une commande qui n'a rien dit ; # une cle visible se lit comme une traduction a ecrire, et un test la trouve. # # CE FICHIER N'A AUCUNE DEPENDANCE, VOLONTAIREMENT # # Il est source par le module ET par les scripts autonomes (installateurs, # lanceurs), qui tournent sans lui. Tout ce dont il a besoin, il l'a. $script:LanguesDisponibles = @('en', 'fr') $script:LangueSecours = 'en' function Get-CtxLangue { <# PURE. Quelle langue, a partir de quoi. Les trois sources sont des PARAMETRES, pas des lectures directes : c'est ce qui rend la decision verifiable sans changer la culture de la machine qui execute les tests. Accepte 'fr-BE' comme 'fr' : une culture systeme porte presque toujours une region, et exiger le code court reviendrait a ne jamais reconnaitre personne. #> param( [AllowNull()][AllowEmptyString()][string]$Demandee = $env:DEVCTX_LANG, [AllowNull()][AllowEmptyString()][string]$Culture, [string[]]$Disponibles = $script:LanguesDisponibles ) foreach ($candidat in @($Demandee, $Culture)) { if (-not $candidat) { continue } $court = $candidat.Trim().ToLowerInvariant() -replace '[_-].*$', '' if ($court -in $Disponibles) { return $court } } $script:LangueSecours } function Get-CtxCultureSysteme { # Isole du reste pour que Get-CtxLangue reste pure et testable. try { [System.Globalization.CultureInfo]::CurrentUICulture.Name } catch { '' } } function Import-CtxTextes { <# Charge une table de textes depuis lang/<code>.psd1. Import-PowerShellDataFile et non Invoke-Expression : un fichier de donnees ne doit pas pouvoir executer de code, meme livre avec le module. #> param( [Parameter(Mandatory)][string]$Code, [string]$Dossier = (Join-Path (Split-Path $PSScriptRoot -Parent) 'lang') ) $fichier = [System.IO.Path]::Combine($Dossier, "$Code.psd1") if (-not (Test-Path -LiteralPath $fichier)) { return @{} } try { Import-PowerShellDataFile -LiteralPath $fichier -ErrorAction Stop } catch { # En anglais, et sans passer par T : c'est la traduction elle-meme qui a # echoue. Un message de panne qui depend du systeme en panne ne s'affiche pas. Write-Warning "DevContext: strings for '$Code' are unreadable: $($_.Exception.Message)" @{} } } function Set-CtxLangue { <# (Re)charge les textes pour une langue. Appelee au chargement du module, et par les tests qui verifient les deux langues dans le meme processus. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Aide privee, non exportee : charge une table en memoire du module, ne touche aucun etat systeme.')] param([string]$Code) if (-not $Code) { $Code = Get-CtxLangue -Culture (Get-CtxCultureSysteme) } $script:Langue = $Code $script:Textes = Import-CtxTextes -Code $Code # Le repli est charge meme quand c'est deja la langue active : cela evite un # cas particulier dans T, et une table de plus en memoire ne coute rien. $script:TextesSecours = if ($Code -eq $script:LangueSecours) { $script:Textes } else { Import-CtxTextes -Code $script:LangueSecours } $Code } function T { <# Le texte d'une cle, dans la langue active. Nom d'une lettre, et ce n'est pas de la paresse : il apparait a plus de deux cents endroits, et un nom long y transformerait chaque message en ligne illisible. Il reste INTERNE au module — jamais exporte, jamais visible depuis un terminal. Les arguments restants alimentent l'operateur -f, donc les textes portent {0}, {1}… Cela permet a une traduction de reordonner ses substitutions, ce qu'une concatenation interdirait. #> param( [Parameter(Mandatory, Position = 0)][string]$Cle, [Parameter(ValueFromRemainingArguments = $true)][object[]]$Arguments ) $texte = $null if ($script:Textes -and $script:Textes.ContainsKey($Cle)) { $texte = $script:Textes[$Cle] } elseif ($script:TextesSecours -and $script:TextesSecours.ContainsKey($Cle)) { $texte = $script:TextesSecours[$Cle] } # Cle inconnue : on la montre. Une chaine vide se lirait comme une commande # qui n'a rien dit ; « [ctx.noGo] » se lit comme un defaut, et se cherche. if ($null -eq $texte) { return "[$Cle]" } if ($Arguments -and $Arguments.Count) { return ($texte -f $Arguments) } $texte } |