Files
Omika/infra/deploy/New-OmikaLanguesManifeste.ps1
2026-10-23 18:46:50 +02:00

526 lines
23 KiB
PowerShell

# ============================================================
# Omika - Generation / verification du manifeste des packs de langue
# (manifest.json de Omika.Langues.Api)
# ============================================================
# Le manifeste decrit les packs ONNX telechargeables par le mobile. Ce n'est PAS
# un fichier de publication : il decrit les fichiers REELLEMENT presents sous
# Langues__RootPath. Il se genere donc la ou sont les modeles, pas dans le depot.
#
# Arborescence attendue : <racine>\<code>\<sens>\<fichier>, ou <sens> vaut
# versFrancais, depuisFrancais, ou commun (poids partages par les deux sens d'un
# modele familial ; le mobile ne les telecharge qu'une fois).
#
# Deux champs ne peuvent pas etre saisis a la main sans risque :
# - tailleOctets : taille reelle (progression du telechargement mobile) ;
# - sha256 : empreinte d'integrite, reverifiee par le mobile APRES
# telechargement. Une empreinte fausse ou absente ne se voit
# QUE sur le telephone : le pack est refuse et pack.json
# n'est jamais ecrit (aucune erreur ne remonte au serveur).
# Ce script les calcule depuis les fichiers. Aucune valeur n'est inventee.
#
# Usage :
# .\New-OmikaLanguesManifeste.ps1 # genere sous RootPath
# .\New-OmikaLanguesManifeste.ps1 -WhatIf # simulation, affiche
# .\New-OmikaLanguesManifeste.ps1 -RootPathLangues D:\Modeles
# .\New-OmikaLanguesManifeste.ps1 -Verifier # controle d'integrite
# .\New-OmikaLanguesManifeste.ps1 -Libelles @{ es = 'Espagnol (Espagne)' }
#
# -Verifier compare le manifeste EXISTANT aux fichiers du disque et sort en
# erreur (code 1) au moindre ecart. Aucun droit administrateur n'est requis :
# c'est le controle a passer apres chaque ajout ou remplacement de modele.
#
# La racine par defaut est lue dans Langues__RootPath (MACHINE), donc la meme
# source de verite que le service : le manifeste est ecrit la ou l'API le lit.
# ============================================================
[CmdletBinding(SupportsShouldProcess = $true)]
param(
# Dossier racine des modeles. Par defaut : variable MACHINE Langues__RootPath,
# puis C:\Omika\Data\Langues (recommandation du DEPLOIEMENT_API_LANGUES.md §6).
[string]$RootPathLangues,
# Fichier manifeste. Par defaut : <RootPathLangues>\manifest.json.
[string]$Sortie,
# Version ecrite dans chaque pack (le mobile la compare a celle installee).
[string]$Version = '1.0.0',
# Libelles affiches, par code de langue. Complete/ecrase la table par defaut.
[hashtable]$Libelles = @{},
# Autorise l'ecriture d'un manifeste SANS aucun pack (deconseille : le mobile
# affiche alors « Aucun modele de langue n'est disponible sur le serveur »).
[switch]$AutoriserVide,
# Controle le manifeste existant au lieu de le regenerer.
[switch]$Verifier
)
$ErrorActionPreference = 'Stop'
# Sens canoniques : le mobile compare ces valeurs de facon SENSIBLE A LA CASSE.
# « commun » porte les poids PARTAGES par les deux sens d'un modele familial
# (opus-mt-tc-big-itc-itc) : le telephone les telecharge une seule fois, et
# chaque sens ne contient que son modele.json (jeton de cible).
$SensCanoniques = @('versFrancais', 'depuisFrancais', 'commun')
# Noms de vocabulaire reconnus par l'application (OnnxTranslationService) :
# vocab.spm = vocabulaire commun, source.spm / target.spm = vocabulaire par
# langue d'un modele Marian/OPUS-MT. Tout autre nom .spm n'est qu'un repli.
$NomsVocabulaire = @('vocab.spm', 'source.spm', 'target.spm')
$LibellesParDefaut = @{
'ar' = 'Arabe'
'bg' = 'Bulgare'
'ca' = 'Catalan'
'cs' = 'Tchèque'
'da' = 'Danois'
'de' = 'Allemand'
'el' = 'Grec'
'en' = 'Anglais'
'es' = 'Espagnol'
'et' = 'Estonien'
'fi' = 'Finnois'
'gl' = 'Galicien'
'hr' = 'Croate'
'hu' = 'Hongrois'
'is' = 'Islandais'
'it' = 'Italien'
'lt' = 'Lituanien'
'lv' = 'Letton'
'mk' = 'Macédonien'
'mt' = 'Maltais'
'nl' = 'Néerlandais'
'no' = 'Norvégien'
'pl' = 'Polonais'
'pt' = 'Portugais'
'ro' = 'Roumain'
'ru' = 'Russe'
'sk' = 'Slovaque'
'sl' = 'Slovène'
'sv' = 'Suédois'
'tr' = 'Turc'
'uk' = 'Ukrainien'
'zh' = 'Chinois'
}
function Write-ManifesteInfo([string]$message) { Write-Host (' ' + $message) }
function Write-ManifesteOk([string]$message) { Write-Host ('[OK] ' + $message) -ForegroundColor Green }
function Write-ManifesteAlerte([string]$message) { Write-Host ('[ALERTE] ' + $message) -ForegroundColor Yellow }
function Write-ManifesteEchec([string]$message) { Write-Host ('[ECHEC] ' + $message) -ForegroundColor Red }
function Format-ManifesteTaille([long]$octets) {
if ($octets -ge 1073741824) { return ('{0:N2} Gio' -f ($octets / 1073741824)) }
if ($octets -ge 1048576) { return ('{0:N1} Mio' -f ($octets / 1048576)) }
if ($octets -ge 1024) { return ('{0:N0} Kio' -f ($octets / 1024)) }
return ('{0} octets' -f $octets)
}
# Code de langue : memes regles que LanguagePaths.EstCodeValide cote application
# (2 a 10 caracteres, minuscules ASCII, chiffres, tiret). Un code invalide fait
# IGNORER le pack SILENCIEUSEMENT par le mobile : on refuse de l'ecrire.
function Test-ManifesteCodeValide([string]$code) {
if ([string]::IsNullOrWhiteSpace($code)) { return $false }
$valeur = $code.Trim()
if ($valeur.Length -lt 2 -or $valeur.Length -gt 10) { return $false }
if ($valeur[0] -eq '-' -or $valeur[-1] -eq '-') { return $false }
foreach ($c in $valeur.ToCharArray()) {
$autorise = (($c -ge 'a' -and $c -le 'z') -or ($c -ge '0' -and $c -le '9') -or $c -eq '-')
if (-not $autorise) { return $false }
}
return $true
}
# Retourne le sens canonique, ou $null si le dossier n'est pas un sens connu.
function Resolve-ManifesteSens([string]$nomDossier) {
foreach ($canonique in $SensCanoniques) {
if ([string]::Equals($nomDossier, $canonique, [System.StringComparison]::OrdinalIgnoreCase)) {
return $canonique
}
}
return $null
}
function Resolve-ManifesteLibelle([string]$code, [hashtable]$surcharge) {
if ($surcharge -and $surcharge.ContainsKey($code)) { return [string]$surcharge[$code] }
if ($LibellesParDefaut.ContainsKey($code)) { return [string]$LibellesParDefaut[$code] }
return $code.ToUpperInvariant()
}
# SHA-256 hexadecimal minuscule : exactement ce que le mobile recalcule.
function Get-ManifesteEmpreinte([string]$chemin) {
return (Get-FileHash -LiteralPath $chemin -Algorithm SHA256).Hash.ToLowerInvariant()
}
# Echappe une chaine pour du JSON (guillemets, antislash, caracteres de controle).
function ConvertTo-ManifesteJsonChaine([string]$valeur) {
$sb = New-Object System.Text.StringBuilder
[void]$sb.Append('"')
foreach ($c in $valeur.ToCharArray()) {
$code = [int]$c
if ($c -eq '"') { [void]$sb.Append('\"') }
elseif ($c -eq '\') { [void]$sb.Append('\\') }
elseif ($code -lt 32) { [void]$sb.Append(('\u{0:x4}' -f $code)) }
else { [void]$sb.Append($c) }
}
[void]$sb.Append('"')
return $sb.ToString()
}
function New-ManifesteBlocFichier([string]$sens, [string]$nomFichier, [long]$taille, [string]$sha256) {
$lignes = @(
' {'
(' "sens": ' + (ConvertTo-ManifesteJsonChaine $sens) + ',')
(' "nomFichier": ' + (ConvertTo-ManifesteJsonChaine $nomFichier) + ',')
(' "tailleOctets": ' + $taille + ',')
(' "sha256": ' + (ConvertTo-ManifesteJsonChaine $sha256))
' }'
)
return ($lignes -join "`r`n")
}
function New-ManifesteBlocPack([string]$code, [string]$libelle, [string]$version, $blocsFichiers) {
$lignes = @(
' {'
(' "code": ' + (ConvertTo-ManifesteJsonChaine $code) + ',')
(' "libelle": ' + (ConvertTo-ManifesteJsonChaine $libelle) + ',')
(' "version": ' + (ConvertTo-ManifesteJsonChaine $version) + ',')
' "fichiers": ['
($blocsFichiers -join (",`r`n"))
' ]'
' }'
)
return ($lignes -join "`r`n")
}
function New-ManifesteJsonText($blocsPacks) {
# Tableau vide ecrit « [] » et non « [\n\n] » : lisible et sans ligne blanche.
$corps = if ($blocsPacks.Count -eq 0) {
@(' "packs": []')
}
else {
@(
' "packs": ['
($blocsPacks -join (",`r`n"))
' ]'
)
}
$lignes = @('{') + $corps + @('}')
return (($lignes -join "`r`n") + "`r`n")
}
# JSON ecrit en UTF-8 SANS BOM : un manifeste est un fichier de donnees lu par
# l'API et par des outils externes, jamais un script.
function Write-ManifesteFichier([string]$chemin, [string]$contenu) {
$encodage = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($chemin, $contenu, $encodage)
}
function Write-ManifesteBilan($echecs, $alertes) {
foreach ($alerte in $alertes) { Write-ManifesteAlerte $alerte }
foreach ($echec in $echecs) { Write-ManifesteEchec $echec }
Write-Host ''
if ($echecs.Count -gt 0) {
Write-Host ("=== RESULTAT : {0} ecart(s), {1} alerte(s) ===" -f $echecs.Count, $alertes.Count) -ForegroundColor Red
return $false
}
Write-Host ("=== RESULTAT : conforme ({0} alerte(s)) ===" -f $alertes.Count) -ForegroundColor Green
return $true
}
# --- Racine effective --------------------------------------------------------
if (-not $RootPathLangues) {
$RootPathLangues = [Environment]::GetEnvironmentVariable('Langues__RootPath', 'Machine')
}
if (-not $RootPathLangues) {
$RootPathLangues = [Environment]::GetEnvironmentVariable('Langues__RootPath', 'Process')
}
if (-not $RootPathLangues) {
$RootPathLangues = 'C:\Omika\Data\Langues'
}
$racine = $RootPathLangues.Trim().TrimEnd([char[]]@('\', '/'))
if (-not $Sortie) { $Sortie = Join-Path $racine 'manifest.json' }
Write-Host '=== Manifeste des packs de langue (Omika Langues API) ==='
Write-ManifesteInfo ("Racine des modeles : {0}" -f $racine)
Write-ManifesteInfo ("Manifeste : {0}" -f $Sortie)
if ($Verifier) { Write-ManifesteInfo 'Mode : VERIFICATION (aucune ecriture)' }
Write-Host ''
if (-not (Test-Path -LiteralPath $racine -PathType Container)) {
Write-ManifesteEchec ("Racine introuvable : {0}" -f $racine)
Write-ManifesteInfo 'Verifier Langues__RootPath (MACHINE), ou passer -RootPathLangues.'
exit 1
}
$echecs = New-Object 'System.Collections.Generic.List[string]'
$alertes = New-Object 'System.Collections.Generic.List[string]'
# ============================================================================
# MODE VERIFICATION
# ============================================================================
if ($Verifier) {
if (-not (Test-Path -LiteralPath $Sortie -PathType Leaf)) {
Write-ManifesteEchec ("Manifeste ABSENT : {0}" -f $Sortie)
Write-ManifesteInfo 'Sans ce fichier, GET /api/langues repond 404 (et le mobile n''a aucune langue).'
Write-ManifesteInfo 'Generer avec : .\New-OmikaLanguesManifeste.ps1'
exit 1
}
$manifeste = $null
try {
$manifeste = (Get-Content -LiteralPath $Sortie -Raw -Encoding UTF8 | ConvertFrom-Json)
}
catch {
Write-ManifesteEchec ("Manifeste illisible (JSON invalide) : {0}" -f $_.Exception.Message)
Write-ManifesteInfo 'Cote service : « Manifeste des langues illisible » puis 404.'
exit 1
}
$packs = @($manifeste.packs)
if ($packs.Count -eq 0) {
$echecs.Add('Le manifeste ne declare aucun pack : le mobile affichera « Aucun modele de langue n''est disponible sur le serveur ».')
}
$declare = New-Object 'System.Collections.Generic.List[string]'
$nbFichiers = 0
$octetsTotal = [long]0
foreach ($pack in $packs) {
$code = ([string]$pack.code).Trim().ToLowerInvariant()
if (-not (Test-ManifesteCodeValide $code)) {
$echecs.Add(("Pack « {0} » : code de langue invalide -> IGNORE SILENCIEUSEMENT par le mobile." -f $pack.code))
continue
}
if ([string]::IsNullOrWhiteSpace([string]$pack.libelle)) {
$alertes.Add(("Pack « {0} » : libelle vide (le code s'affichera a la place)." -f $code))
}
if ([string]::IsNullOrWhiteSpace([string]$pack.version)) {
$alertes.Add(("Pack « {0} » : version vide (comparaison d'installation impossible)." -f $code))
}
$fichiers = @($pack.fichiers)
if ($fichiers.Count -eq 0) {
$echecs.Add(("Pack « {0} » : aucun fichier declare." -f $code))
continue
}
foreach ($fichier in $fichiers) {
$sensManifeste = [string]$fichier.sens
$sens = Resolve-ManifesteSens $sensManifeste
if (-not $sens) {
$echecs.Add(("Pack « {0} » : sens inconnu « {1} » (attendu versFrancais, depuisFrancais ou commun)." -f $code, $sensManifeste))
continue
}
if ($sensManifeste -cne $sens) {
$echecs.Add(("Pack « {0} » : sens « {1} » a la mauvaise casse -> REFUSE par le mobile (comparaison sensible a la casse)." -f $code, $sensManifeste))
continue
}
$nomFichier = [string]$fichier.nomFichier
if ([string]::IsNullOrWhiteSpace($nomFichier)) {
$echecs.Add(("Pack « {0} » : nomFichier vide." -f $code))
continue
}
if ($nomFichier -ne [System.IO.Path]::GetFileName($nomFichier)) {
$echecs.Add(("Pack « {0} » : nomFichier « {1} » contient un chemin (interdit)." -f $code, $nomFichier))
continue
}
$chemin = Join-Path (Join-Path (Join-Path $racine $code) $sens) $nomFichier
$declare.Add($chemin.ToLowerInvariant())
$relatif = ('{0}/{1}/{2}' -f $code, $sens, $nomFichier)
# Un fichier declare est un fichier TELECHARGE par chaque telephone :
# signaler ceux qui ne servent a rien.
if ($nomFichier.EndsWith('.onnx', [System.StringComparison]::OrdinalIgnoreCase) `
-and $nomFichier -match 'past') {
$alertes.Add(("{0} : decodeur a cache (past_key_values) IGNORE par l'application, mais declare donc TELECHARGE pour rien." -f $relatif))
}
elseif ($nomFichier.EndsWith('.spm', [System.StringComparison]::OrdinalIgnoreCase) `
-and $NomsVocabulaire -notcontains $nomFichier) {
$alertes.Add(("{0} : nom de vocabulaire non reconnu (attendu vocab.spm, source.spm ou target.spm) : pris comme repli « premier .spm »." -f $relatif))
}
if (-not (Test-Path -LiteralPath $chemin -PathType Leaf)) {
$echecs.Add(("Fichier ABSENT sur le disque : {0} -> le mobile recevra 404." -f $relatif))
continue
}
$info = Get-Item -LiteralPath $chemin
$taille = [long]$fichier.tailleOctets
if ($taille -le 0) {
$alertes.Add(("{0} : tailleOctets non renseignee (0) -> pas de controle de taille ni de progression." -f $relatif))
}
elseif ($taille -ne $info.Length) {
$echecs.Add(("{0} : taille differente -> manifeste {1} octet(s), disque {2}." -f $relatif, $taille, $info.Length))
}
$sha = ([string]$fichier.sha256).Trim()
if ([string]::IsNullOrWhiteSpace($sha)) {
$echecs.Add(("{0} : empreinte SHA-256 absente -> le pack sera REFUSE par le mobile." -f $relatif))
continue
}
$reel = Get-ManifesteEmpreinte $chemin
if (-not [string]::Equals($sha, $reel, [System.StringComparison]::OrdinalIgnoreCase)) {
$echecs.Add(("{0} : empreinte differente ->`r`n manifeste {1}`r`n disque {2}" -f $relatif, $sha, $reel))
}
$nbFichiers++
$octetsTotal += $info.Length
}
}
# Fichiers presents mais non declares : jamais telecharges, poids mort.
foreach ($fichierDisque in @(Get-ChildItem -LiteralPath $racine -Recurse -File)) {
if ($fichierDisque.FullName.ToLowerInvariant() -eq $Sortie.ToLowerInvariant()) { continue }
# La sauvegarde du manifeste ecrite par ce script n'est evidemment pas un
# fichier de modele : la signaler serait un faux positif a chaque controle.
if ($fichierDisque.FullName.ToLowerInvariant() -eq ($Sortie + '.bak').ToLowerInvariant()) { continue }
$relatifDisque = $fichierDisque.FullName.Substring($racine.Length).TrimStart('\', '/')
# Dossiers techniques (ex. .hf-cache du script d'export, .tmp-*) : ce ne
# sont pas des modeles, et un cache peut contenir des milliers de fichiers.
if ($relatifDisque -match '(^|\\)\.[^\\]') { continue }
if (-not $declare.Contains($fichierDisque.FullName.ToLowerInvariant())) {
$alertes.Add(("Fichier non declare dans le manifeste (jamais telecharge) : {0}" -f $relatifDisque))
}
}
Write-ManifesteInfo ("{0} pack(s), {1} fichier(s) verifie(s), {2}" -f $packs.Count, $nbFichiers, (Format-ManifesteTaille $octetsTotal))
Write-Host ''
if (Write-ManifesteBilan $echecs $alertes) { exit 0 } else { exit 1 }
}
# ============================================================================
# MODE GENERATION
# ============================================================================
$dossiersPacks = @(Get-ChildItem -LiteralPath $racine -Directory |
Where-Object { -not $_.Name.StartsWith('.') } |
Sort-Object -Property Name)
if ($dossiersPacks.Count -eq 0) {
$message = ("Aucun sous-dossier de langue sous {0} (attendu : {0}\es\versFrancais\encoder.onnx)." -f $racine)
# -AutoriserVide couvre les DEUX causes de manifeste vide : racine sans
# dossier de langue, et dossiers sans aucun fichier exploitable.
if ($AutoriserVide) { $alertes.Add($message) } else { $echecs.Add($message) }
}
$blocsPacks = New-Object 'System.Collections.Generic.List[string]'
$recap = New-Object 'System.Collections.Generic.List[string]'
foreach ($dossierPack in $dossiersPacks) {
$nomDossier = $dossierPack.Name
$code = $nomDossier.Trim().ToLowerInvariant()
if (-not (Test-ManifesteCodeValide $code)) {
$alertes.Add(("Dossier ignore « {0} » : code de langue invalide (2 a 10 caracteres, minuscules a-z, chiffres, tiret)." -f $nomDossier))
continue
}
# -cne : comparaison SENSIBLE A LA CASSE. « -ne » considererait « ru » et
# « RU » comme egaux et ne signalerait rien.
if ($code -cne $nomDossier) {
$alertes.Add(("Dossier « {0} » : le manifeste ecrit « {1} ». Renommer le dossier si le volume peut etre sensible a la casse." -f $nomDossier, $code))
}
$libelle = Resolve-ManifesteLibelle $code $Libelles
$blocsFichiers = New-Object 'System.Collections.Generic.List[string]'
$nbFichiersPack = 0
$octetsPack = [long]0
foreach ($dossierSens in @(Get-ChildItem -LiteralPath $dossierPack.FullName -Directory | Sort-Object -Property Name)) {
$sens = Resolve-ManifesteSens $dossierSens.Name
if (-not $sens) {
$alertes.Add(("Dossier ignore {0}\{1} : sens inconnu (attendu versFrancais, depuisFrancais ou commun)." -f $nomDossier, $dossierSens.Name))
continue
}
if ($sens -cne $dossierSens.Name) {
$alertes.Add(("Dossier {0}\{1} : le manifeste ecrit « {2} » (casse canonique exigee par le mobile)." -f $nomDossier, $dossierSens.Name, $sens))
}
# L'API ne sert que {code}/{sens}/{fichier} : un sous-dossier n'est jamais atteignable.
foreach ($sousDossier in @(Get-ChildItem -LiteralPath $dossierSens.FullName -Directory)) {
$alertes.Add(("Dossier ignore {0}\{1}\{2} : l'API ne sert que {{code}}/{{sens}}/{{fichier}}." -f $nomDossier, $dossierSens.Name, $sousDossier.Name))
}
foreach ($fichier in @(Get-ChildItem -LiteralPath $dossierSens.FullName -File | Sort-Object -Property Name)) {
# Deux fichiers inutiles qu'il ne faut PAS livrer : declares ici, ils
# seraient telecharges par chaque telephone pour rien.
if ($fichier.Name.EndsWith('.onnx', [System.StringComparison]::OrdinalIgnoreCase) `
-and $fichier.Name -match 'past') {
$alertes.Add(("{0}\{1}\{2} (~{3}) : decodeur a cache (past_key_values) IGNORE par l'application, mais il serait TELECHARGE. Ne pas livrer ce fichier." -f $nomDossier, $dossierSens.Name, $fichier.Name, (Format-ManifesteTaille $fichier.Length)))
}
elseif ($fichier.Name.EndsWith('.spm', [System.StringComparison]::OrdinalIgnoreCase) `
-and $NomsVocabulaire -notcontains $fichier.Name) {
$alertes.Add(("{0}\{1}\{2} : nom de vocabulaire non reconnu (attendu vocab.spm, source.spm ou target.spm). L'application le prendrait comme repli « premier .spm » : verifier qu'il est bien le bon." -f $nomDossier, $dossierSens.Name, $fichier.Name))
}
$sha = Get-ManifesteEmpreinte $fichier.FullName
$blocsFichiers.Add((New-ManifesteBlocFichier -sens $sens -nomFichier $fichier.Name -taille $fichier.Length -sha256 $sha))
$nbFichiersPack++
$octetsPack += $fichier.Length
}
}
if ($nbFichiersPack -eq 0) {
$alertes.Add(("Pack « {0} » ignore : aucun fichier utilisable." -f $code))
continue
}
$blocsPacks.Add((New-ManifesteBlocPack -code $code -libelle $libelle -version $Version -blocsFichiers $blocsFichiers))
$recap.Add((' {0,-6} {1,-16} {2,3} fichier(s) {3,12}' -f $code, $libelle, $nbFichiersPack, (Format-ManifesteTaille $octetsPack)))
}
if ($blocsPacks.Count -eq 0 -and -not $AutoriserVide) {
$echecs.Add('Aucun pack exploitable : manifeste NON ecrit (un manifeste vide ferait afficher au mobile « Aucun modele de langue n''est disponible sur le serveur »).')
$echecs.Add('Corriger l''arborescence, ou forcer avec -AutoriserVide si c''est volontaire.')
}
if ($echecs.Count -gt 0) {
Write-ManifesteBilan $echecs $alertes | Out-Null
exit 1
}
$json = New-ManifesteJsonText $blocsPacks
foreach ($ligne in $recap) { Write-ManifesteInfo $ligne }
Write-Host ''
if (-not $PSCmdlet.ShouldProcess($Sortie, 'Ecriture du manifeste des packs de langue')) {
Write-ManifesteInfo 'Simulation (-WhatIf) : aucun fichier ecrit. Contenu qui aurait ete ecrit :'
Write-Host ''
Write-Host $json
Write-Host ''
Write-ManifesteBilan $echecs $alertes | Out-Null
exit 0
}
if (Test-Path -LiteralPath $Sortie -PathType Leaf) {
$sauvegarde = $Sortie + '.bak'
Copy-Item -LiteralPath $Sortie -Destination $sauvegarde -Force
Write-ManifesteInfo ("Manifeste precedent conserve : {0}" -f $sauvegarde)
}
Write-ManifesteFichier -chemin $Sortie -contenu $json
Write-ManifesteOk ("Manifeste ecrit : {0}" -f $Sortie)
Write-Host ''
foreach ($alerte in $alertes) { Write-ManifesteAlerte $alerte }
Write-Host ''
Write-ManifesteInfo 'Aucun demarrage/redemarrage de service n''est necessaire : l''API relit le'
Write-ManifesteInfo 'manifeste a CHAQUE appel de GET /api/langues.'
Write-ManifesteInfo 'Le mobile, lui, garde son index memorise : il faut un rafraichissement'
Write-ManifesteInfo '(liste des langues -> actualiser) pour voir la nouvelle liste.'
Write-Host ''
Write-ManifesteInfo 'Controles a enchainer :'
Write-ManifesteInfo (' 1. .\New-OmikaLanguesManifeste.ps1 -Verifier')
Write-ManifesteInfo ' 2. Invoke-RestMethod https://<URL_SRV_LANGUES>/api/langues | ConvertTo-Json -Depth 6'
exit 0