Public/Get-HDTStringTable.ps1

function Get-HDTStringTable {
    <#
        .SYNOPSIS
            The text a window shows, read from a file rather than from its
            markup.

        .DESCRIPTION
            EVERY LABEL, HINT, BUTTON AND TAB HEADER USED TO BE A LITERAL IN
            XAML. Changing a sentence meant editing a window; translating one
            meant forking it; and a string that appeared in two windows was two
            strings that drifted apart. This is the table they move into: one
            .psd1 per culture in the module's Strings folder, keyed by the
            control it belongs to.

            ONE BLOCK PER WINDOW, AND THE KEY INSIDE IT IS Control.Property.
            A translator works through a screen at a time and can see when one
            is finished, which a flat list of four hundred keys does not allow;
            and a window's strings can be handed to Set-HDTWindowText without
            carrying every other window's along with them.

              @{
                  BootImage = @{
                      'HDTBootImageImageNameLabel.Text' = 'Image name'
                  }
              }

            -Page RETURNS ONE BLOCK, and without it every block is merged into
            one table - which is what a caller filling several windows from one
            load wants.

            IT FALLS BACK RATHER THAN FAILS, TWICE. A culture nobody has
            translated loads en-us; a KEY nobody has translated takes the en-us
            string, so a half-finished translation is a console with some
            English in it rather than a console with holes in it. That is what
            makes it safe to ship a language before it is finished.

            en-us IS THE FLOOR AND ITS ABSENCE IS AN ERROR. Without it a missing
            key has nothing to fall back to, and the window would show blanks
            nobody could explain from a screenshot.

            IT IS A .psd1 AND NOT JSON OR YAML. Import-PowerShellDataFile parses
            it without executing it, it is the format PowerShell itself uses for
            module manifests and message tables, and an administrator editing
            one gets quoting rules they already know. YAML would need
            powershell-yaml, which is a module the console would then have to
            carry to draw a window.

        .PARAMETER Culture
            The culture to load, as a folder-style name: en-us, he-il, de-de.
            Defaults to the current thread's.

        .PARAMETER Path
            The Strings folder. Defaults to the module's own.

        .PARAMETER Page
            The block to return - BootImage, Wizard. Omitted, every block is
            merged: a control name is unique across this module, so the merge
            cannot collide.

        .PARAMETER PassThruCulture
            Return the culture that was actually loaded rather than the table -
            'en-us' when the asked-for one does not exist. For a caller that
            wants to log which it got.

        .INPUTS
            None. This command does not accept pipeline input.

        .OUTPUTS
            System.Collections.Hashtable - case-insensitive, key to string.
            With -PassThruCulture, System.String.

        .EXAMPLE
            $string = Get-HDTStringTable -Page BootImage
            Set-HDTWindowText -Root $window -String $string

        .EXAMPLE
            Get-HDTStringTable -Culture 'he-il'

            The Hebrew table, with every untranslated key filled from en-us.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Position = 0)]
        [AllowEmptyString()]
        [string] $Culture = '',

        [Parameter()]
        [AllowEmptyString()]
        [string] $Path = '',

        [Parameter()]
        [AllowEmptyString()]
        [string] $Page = '',

        [Parameter()]
        [switch] $PassThruCulture
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    $root = $Path
    if ([string]::IsNullOrWhiteSpace($root)) {
        $root = [System.IO.Path]::Combine($script:HDTModuleRoot, 'Strings')
    }

    $fallbackPath = [System.IO.Path]::Combine($root, 'en-us.psd1')

    if (-not (Test-Path -LiteralPath $fallbackPath -PathType Leaf)) {
        throw [System.IO.FileNotFoundException]::new(
            ("there is no en-us.psd1 in '{0}'. It is the table every other culture falls back to, so a console cannot be drawn without it." -f $root),
            $fallbackPath)
    }

    $wanted = $Culture
    if ([string]::IsNullOrWhiteSpace($wanted)) {
        $wanted = [System.Globalization.CultureInfo]::CurrentUICulture.Name
    }

    # THE FOLDER-STYLE NAME IS THE FILE NAME. en-US and en-us are the same
    # culture and a case-sensitive file system is not this module's problem.
    $wantedPath = [System.IO.Path]::Combine($root, ('{0}.psd1' -f $wanted.ToLowerInvariant()))

    $loaded = 'en-us'
    if ((Test-Path -LiteralPath $wantedPath -PathType Leaf) -and
        -not [string]::Equals($wanted, 'en-us', [System.StringComparison]::OrdinalIgnoreCase)) {

        $loaded = $wanted.ToLowerInvariant()
    }

    if ($PassThruCulture) { return $loaded }

    $table = [System.Collections.Hashtable]::new([System.StringComparer]::OrdinalIgnoreCase)
    $seen = $false

    # ENGLISH FIRST, THE TRANSLATION OVER THE TOP. That order is what makes a
    # half-finished translation usable: every key exists, and the ones somebody
    # has got to are the ones that changed.
    foreach ($file in @($fallbackPath, $wantedPath)) {

        if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { continue }

        $data = Import-PowerShellDataFile -LiteralPath $file

        foreach ($block in @($data.Keys | Sort-Object { [string]::Equals([string] $_, 'Common', [System.StringComparison]::OrdinalIgnoreCase) } -Descending)) {

            # A BLOCK IS A WINDOW, EXCEPT Common, WHICH IS EVERY WINDOW. Save,
            # Cancel, Browse: words that belong to no one screen and must be
            # spelled the same way on all of them. It is merged FIRST, so a page
            # naming the same control still wins.
            $isCommon = [string]::Equals([string] $block, 'Common', [System.StringComparison]::OrdinalIgnoreCase)

            if (-not [string]::IsNullOrWhiteSpace($Page) -and -not $isCommon -and
                -not [string]::Equals([string] $block, $Page, [System.StringComparison]::OrdinalIgnoreCase)) {
                continue
            }

            if (-not $isCommon) { $seen = $true }
            $strings = $data[$block]
            if ($null -eq $strings -or $strings -isnot [System.Collections.IDictionary]) { continue }

            foreach ($key in @($strings.Keys)) {
                $table[[string] $key] = [string] $strings[$key]
            }
        }
    }

    # A PAGE NOBODY HAS IS A TYPO, AND IT USED TO BE INVISIBLE. The markup
    # carries no text any more, so an empty table is not "nothing to translate"
    # - it is a window of blank labels, and the mistake is a page name nobody
    # can see from a screenshot.
    if (-not [string]::IsNullOrWhiteSpace($Page) -and -not $seen) {
        throw [System.Collections.Generic.KeyNotFoundException]::new(
            ("there is no '{0}' block in '{1}'. The blocks are: {2}." -f $Page, $fallbackPath,
                ((@((Import-PowerShellDataFile -LiteralPath $fallbackPath).Keys) | Sort-Object) -join ', ')))
    }

    return $table
}