private/core/Convert-KeyboardLayoutToLanguageCode.ps1

function Convert-KeyboardLayoutToLanguageCode {
    <#
    .SYNOPSIS
    Converts a Windows keyboard layout value to a language/culture code.
 
    .DESCRIPTION
    Resolves the culture tag (for example, en-US or fr-FR) from a keyboard
    layout hexadecimal string such as 00000409. If no keyboard layout is
    provided, the function attempts to detect the current layout from
    Win32_Keyboard. When conversion fails, a fallback language code is returned.
 
    .PARAMETER KeyboardLayout
    Keyboard layout hexadecimal value (KLID), for example 00000409 or
    00010409. The function uses the trailing 4 hex characters as the LCID.
 
    .PARAMETER FallbackLanguageCode
    Language code to return when keyboard layout detection or conversion fails.
    Default is en-US.
 
    .PARAMETER LowerCase
    Returns the language code in lowercase (for example en-us).
 
    .EXAMPLE
    Convert-KeyboardLayoutToLanguageCode
    Detects the active keyboard layout from Win32_Keyboard and returns the
    resolved language code.
 
    .EXAMPLE
    Convert-KeyboardLayoutToLanguageCode -KeyboardLayout '0000040C'
    Returns fr-FR.
 
    .EXAMPLE
    Convert-KeyboardLayoutToLanguageCode -KeyboardLayout '00000409' -LowerCase
    Returns en-us.
 
    .LINK
    https://github.com/OSDeploy/OSD/tree/master/docs
 
    .LINK
    https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/windows-language-pack-default-values
 
    .NOTES
    Author: David Segura - Recast Software
    2026-07-14 - Initial help block created
    2026-07-14 - Added keyboard layout to language code conversion with fallback behavior
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Layout', 'KLID')]
        [string]
        $KeyboardLayout,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $FallbackLanguageCode = 'en-US',

        [Parameter(Mandatory = $false)]
        [switch]
        $LowerCase
    )

    process {
        $allowedLanguageCodes = @(
            'ar-sa', 'bg-bg', 'cs-cz', 'da-dk', 'de-de', 'el-gr',
            'en-gb', 'en-us', 'es-es', 'es-mx', 'et-ee', 'fi-fi',
            'fr-ca', 'fr-fr', 'he-il', 'hr-hr', 'hu-hu', 'it-it',
            'ja-jp', 'ko-kr', 'lt-lt', 'lv-lv', 'nb-no', 'nl-nl',
            'pl-pl', 'pt-br', 'pt-pt', 'ro-ro', 'ru-ru', 'sk-sk',
            'sl-si', 'sr-latn-rs', 'sv-se', 'th-th', 'tr-tr',
            'uk-ua', 'zh-cn', 'zh-tw'
        )

        $resolvedFallbackLanguageCode = if ($allowedLanguageCodes -contains $FallbackLanguageCode.ToLowerInvariant()) {
            $FallbackLanguageCode
        }
        else {
            'en-US'
        }

        $layoutToResolve = $KeyboardLayout

        if ([string]::IsNullOrWhiteSpace($layoutToResolve)) {
            try {
                $layoutToResolve = [string](Get-CimInstance -ClassName Win32_Keyboard -ErrorAction Stop | Select-Object -ExpandProperty Layout -First 1)
                Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Detected keyboard layout: $layoutToResolve"
            }
            catch {
                Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Unable to detect keyboard layout from Win32_Keyboard. Returning fallback language code: $resolvedFallbackLanguageCode"
                if ($LowerCase) {
                    return $resolvedFallbackLanguageCode.ToLowerInvariant()
                }
                return $resolvedFallbackLanguageCode
            }
        }

        $normalizedLayout = ($layoutToResolve -replace '^0x', '').Trim()

        if ([string]::IsNullOrWhiteSpace($normalizedLayout) -or $normalizedLayout.Length -lt 4 -or $normalizedLayout -notmatch '^[0-9a-fA-F]+$') {
            Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Keyboard layout '$layoutToResolve' is not a valid hex layout. Returning fallback language code: $resolvedFallbackLanguageCode"
            if ($LowerCase) {
                return $resolvedFallbackLanguageCode.ToLowerInvariant()
            }
            return $resolvedFallbackLanguageCode
        }

        $languageIdHex = $normalizedLayout.Substring($normalizedLayout.Length - 4)

        try {
            $languageId = [Convert]::ToInt32($languageIdHex, 16)
            $cultureName = [System.Globalization.CultureInfo]::GetCultureInfo($languageId).Name

            if ([string]::IsNullOrWhiteSpace($cultureName)) {
                throw "No culture name resolved for LCID $languageIdHex"
            }

            if ($allowedLanguageCodes -notcontains $cultureName.ToLowerInvariant()) {
                Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Resolved language code '$cultureName' is not in the supported set. Returning fallback language code: $resolvedFallbackLanguageCode"
                if ($LowerCase) {
                    return $resolvedFallbackLanguageCode.ToLowerInvariant()
                }
                return $resolvedFallbackLanguageCode
            }

            if ($LowerCase) {
                return $cultureName.ToLowerInvariant()
            }

            return $cultureName
        }
        catch {
            Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Unable to convert keyboard layout '$layoutToResolve' (LCID: $languageIdHex). Returning fallback language code: $resolvedFallbackLanguageCode"
            if ($LowerCase) {
                return $resolvedFallbackLanguageCode.ToLowerInvariant()
            }
            return $resolvedFallbackLanguageCode
        }
    }
}