Modules/businessdev.ALbuild.Containers/Private/Install-BcContainerLanguageApp.ps1

function Install-BcContainerLanguageApp {
    <#
    .SYNOPSIS
        Installs Microsoft's language app for a culture into a container's default tenant.
 
    .DESCRIPTION
        Without its language app a culture is not available in the client, even when DefaultLanguage names
        it: the generic image publishes the language apps but leaves them unsynced and uninstalled.
 
        Uses the published app when there is one. Otherwise publishes it from the artifact inside the
        container: the compiled country folders (C:\Applications.<country>), then C:\Extensions, then the
        source apps under C:\Applications. Then syncs and installs it, and verifies that it is installed.
        Idempotent. Throws, listing the language apps that do exist, when no app matches the culture.
 
    .PARAMETER Name
        Container name.
 
    .PARAMETER Language
        Culture name, e.g. 'de-DE'.
 
    .PARAMETER ServerInstance
        BC server instance inside the container. Default 'BC'.
 
    .PARAMETER DockerExecutable
        The Docker executable to use (default 'docker').
 
    .OUTPUTS
        PSCustomObject with AppName, Version, Action (Installed / AlreadyInstalled) and Source.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('ContainerName')] [string] $Name,
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Language,
        [string] $ServerInstance = 'BC',
        [string] $DockerExecutable = 'docker'
    )

    $appName = Get-BcLanguageAppName -Language $Language
    if (-not $PSCmdlet.ShouldProcess($Name, "Install language app '$appName'")) { return }
    Write-ALbuildLog "Installing language app '$appName' for '$Language' into '$Name'..."

    $script = {
        $isLanguageApp = { param($app) $app.Publisher -eq 'Microsoft' -and $app.Name -eq $AppName }
        $getPublished = {
            @(Get-NAVAppInfo -ServerInstance $ServerInstance -Tenant default -TenantSpecificProperties |
                    Where-Object { & $isLanguageApp $_ } | Sort-Object { [version]"$($_.Version)" } -Descending)
        }

        $source = 'published'
        $published = & $getPublished
        if ($published.Count -eq 0) {
            # Not published: take it from the artifact. Compiled country folders first, the source app last.
            $roots = New-Object System.Collections.Generic.List[string]
            foreach ($d in (Get-ChildItem -Path 'C:\' -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^Applications\..+' } | Sort-Object Name)) {
                $roots.Add($d.FullName)
            }
            $roots.Add('C:\Extensions'); $roots.Add('C:\Applications')
            $file = $null
            foreach ($root in $roots) {
                if (-not (Test-Path -LiteralPath $root)) { continue }
                $file = Get-ChildItem -LiteralPath $root -Filter "Microsoft_$AppName*.app" -Recurse -File -ErrorAction SilentlyContinue |
                    Where-Object { $_.Name -notlike '*.runtime.app' } | Select-Object -First 1
                if ($file) { break }
            }
            if ($file) {
                Write-Output "Publishing '$AppName' from $($file.FullName)"
                Publish-NAVApp -ServerInstance $ServerInstance -Path $file.FullName -SkipVerification -Scope Global -ErrorAction Stop
                $source = $file.FullName
                $published = & $getPublished
            }
        }

        if ($published.Count -eq 0) {
            $available = @(Get-NAVAppInfo -ServerInstance $ServerInstance | Where-Object { $_.Publisher -eq 'Microsoft' -and $_.Name -like '* language (*)' } |
                    ForEach-Object { "$($_.Name)" } | Sort-Object -Unique)
            $list = if ($available.Count -gt 0) { $available -join ', ' } else { '(none)' }
            throw "No language app '$AppName' for '$Language' is published in the container or ships with its artifact. Available language apps: $list"
        }

        $app = $published[0]
        $action = 'AlreadyInstalled'
        if ("$($app.SyncState)" -ne 'Synced') {
            Write-Output "Syncing '$AppName' $($app.Version) (was $($app.SyncState))"
            Sync-NAVApp -ServerInstance $ServerInstance -Name $app.Name -Publisher $app.Publisher -Version $app.Version -ErrorAction Stop
        }
        if (-not $app.IsInstalled) {
            Install-NAVApp -ServerInstance $ServerInstance -Name $app.Name -Publisher $app.Publisher -Version $app.Version -ErrorAction Stop
            $action = 'Installed'
        }

        $check = Get-NAVAppInfo -ServerInstance $ServerInstance -Name $app.Name -Publisher $app.Publisher -Version $app.Version -Tenant default -TenantSpecificProperties
        if (-not $check -or -not $check.IsInstalled) {
            throw "Language app '$AppName' $($app.Version) is still not installed after Install-NAVApp (sync state: $($check.SyncState))."
        }
        Write-Output "ALBUILD-LANGAPP|$($app.Name)|$($app.Version)|$action|$source"
    }

    $output = @(Invoke-BcContainerCommand -ContainerName $Name -ScriptBlock $script -DockerExecutable $DockerExecutable -Variables @{
            AppName        = $appName
            Language       = $Language
            ServerInstance = $ServerInstance
        })
    $lines = @($output | ForEach-Object { "$_" -split "`r?`n" } | Where-Object { $_ })
    foreach ($line in ($lines | Where-Object { $_ -notlike 'ALBUILD-LANGAPP|*' })) { Write-ALbuildLog " $line" }

    $marker = $lines | Where-Object { $_ -like 'ALBUILD-LANGAPP|*' } | Select-Object -Last 1
    if (-not $marker) {
        throw "Installing language app '$appName' into '$Name' returned no result. Output: $($lines -join ' | ')"
    }
    $parts = $marker -split '\|'
    $result = [PSCustomObject]@{ AppName = $parts[1]; Version = $parts[2]; Action = $parts[3]; Source = $parts[4] }
    $verb = if ($result.Action -eq 'Installed') { 'Installed' } else { 'Already installed:' }
    Write-ALbuildLog -Level Success "$verb language app '$($result.AppName)' $($result.Version) in '$Name' (source: $($result.Source))."
    return $result
}