PSMacToolkitLib.psm1

<#

PSMacToolkitLib

Copyright (C) 2025 Vincent Anso

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

#>


function Get-LocalizedStrings 
{
    <#
    
    .SYNOPSIS
    Return the localized strings from a specified .loctable file in a given language, e.g. fr, fr_CA, Italian
    or
    Return the localized strings from a specified .strings file

    #>

    
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateScript({ Test-Path $_ })]
        [string]$LiteralPath,
        [string[]]$Localizations = @( (Get-Culture).TwoLetterISOLanguageName ),
        [ValidateScript({ $Localizations -contains $_ })]
        [string]$ExpandLocalization
    )

    $localizedStrings = @{}

    $extension = Get-Item -LiteralPath $LiteralPath | Select-Object -ExpandProperty Extension

    if (@(".strings", ".loctable") -notcontains $extension)
    {
        Write-Error "Unsupported file type ($extension). Expected .strings or .loctable file type."
        return $null
    }

    if ($extension -eq ".strings")
    {
        foreach($localization in $($Localizations | Get-Unique))
        {
            if ($LiteralPath.Contains("en.lproj"))
            {
                $LiteralPath = $LiteralPath -replace "en.lproj", "$localization.lproj"
            }
            elseif ($LiteralPath.Contains("English.lproj"))
            {
                $localizationEnglishName = $([Globalization.CultureInfo]::new($localization).EnglishName)
                
                $LiteralPath = $LiteralPath -replace "English.lproj", "$localizationEnglishName.lproj"
            }
           
            if (-Not (Test-Path -LiteralPath $LiteralPath) )
            {
                $LiteralPath = $LiteralPath -replace "$localization.lproj", "$localizationEnglishName.lproj"

                if (-Not (Test-Path -LiteralPath $LiteralPath) )
                {
                    $LiteralPath = $LiteralPath -replace "English.lproj", "$localizationEnglishName.lproj"
                }
            }

            $check = $(/usr/bin/plutil -lint $LiteralPath)

            if ($check -ne $LiteralPath + ": OK")
            {
                Write-Error $check
                return $null
            }

            $strings = $(/usr/bin/plutil -convert json $LiteralPath -o -) | ConvertFrom-Json -AsHashtable 
            
            $localizedStrings.Add($localization, $strings)
        }
    }
    else
    {
        $check = $(/usr/bin/plutil -lint $LiteralPath)

        if ($check -ne $LiteralPath + ": OK")
        {
            Write-Error $check
            return $null
        }

        $contents = $(/usr/bin/plutil -convert json $LiteralPath -o -) | ConvertFrom-Json -AsHashtable    
        
        ($contents) ? $contents.Remove("LocProvenance") : $(return $null)

        foreach($localization in $($Localizations | Get-Unique))
        {     
            $strings = $contents.$localization

            $localizationEnglishName = $([Globalization.CultureInfo]::new($localization).EnglishName)

            if ([string]::IsNullOrEmpty($strings))
            {
                $strings = $contents.$localizationEnglishName
            }

            if ([string]::IsNullOrEmpty($strings))
            {
                $strings = $contents
            }

            $localizedStrings.Add($localization, $strings)
        }

        if ( [string]::IsNullOrEmpty($strings) )
        {
            Write-Error "Available localizations : $($contents.Keys -join ",")"
        }
    }

    if ($ExpandLocalization)
    {
        if ($extension -eq ".strings")
        {
            ($localizedStrings.$ExpandLocalization) ? $localizedStrings.$ExpandLocalization : $(Write-Error "Localization `"$ExpandLocalization`" cannot be found.")
        }
        else
        {
            ($localizedStrings.$ExpandLocalization) ? $localizedStrings.$ExpandLocalization : $(Write-Error "Localization `"$ExpandLocalization`" cannot be found. Available localizations : $($contents.Keys -join ",")")
        }
    }
    else 
    {
        $localizedStrings
    }
}

class SPSettings
{
    static [string]$Language = "en"
    static [string]$FallbackLanguage = "English"
    static [string]$CustomLanguage = "en"
    static [switch]$Raw = $false
    static [string]$Format = "loctable"
    static [string]$DataType
    static [string[]]$DataTypes
    static [string[]]$AvailableLocalizations
    static [switch]$ResolveItemsName

    static SPSettings()
    {
        [Collections.ArrayList]$dataTypesList = [Collections.ArrayList]::new($(/usr/sbin/system_profiler -listDataTypes))
        $dataTypesList.RemoveAt(0)
        [SPSettings]::DataTypes = $dataTypesList

        $localizations = (Get-ChildItem /System/Library/SystemProfiler/SPOSReporter.spreporter/Contents/Resources/*.lproj | Select-Object -ExpandProperty Name).Split(".") | Where-Object { $_ -ne "lproj" }

        [SPSettings]::AvailableLocalizations = $localizations
    }

    static [string[]] ListDataTypes()
    {
        return [SPSettings]::DataTypes
    }

    static [string[]] ListAvailableLocalizations()
    {
        return [SPSettings]::AvailableLocalizations
    }
}

class SPLocalization
{
    hidden static [hashtable]$LocalisedStrings
    
    static SPLocalization()
    {    
        $spreporters += $(Get-ChildItem /System/Library/SystemProfiler/* | Select-Object -ExpandProperty Name).Split(".") | Where-Object { $_ -ne "spreporter" }

        $spreporters += "/System/Library/PrivateFrameworks/SPSupport.framework"

        $localizedStringsList = [System.Collections.Specialized.ListDictionary]::new()

        $suffix = "Contents/Resources/Localizable.loctable"

        $spsupportsuffix = "Versions/A/Resources/Localizable.loctable"

        if (-Not (Test-Path "/System/Library/PrivateFrameworks/SPSupport.framework/$spsupportsuffix") )
        {
            $spsupportsuffix = "Versions/A/Resources/en.lproj/Localizable.strings"
        
            [SPSettings]::Format = "strings"
        }

        foreach($spreporter in $spreporters)
        {
            if ($spreporter -like "*PrivateFrameworks*")
            {
                $filePath = "$spreporter/$spsupportsuffix"

                $dataType = "SPSupportDataType"
            }
            else 
            {  
                if ([SPSettings]::Format -eq "strings")
                {
                    if (-Not (Test-Path -LiteralPath "/System/Library/SystemProfiler/$spreporter.spreporter/$suffix") )
                    {
                        $suffix = "Contents/Resources/en.lproj/Localizable.strings"
                    
                        if (-Not (Test-Path -LiteralPath "/System/Library/SystemProfiler/$spreporter.spreporter/$suffix") )
                        {
                            $suffix = "Contents/Resources/English.lproj/Localizable.strings"
                        }
                    }
                }

                $filePath = "/System/Library/SystemProfiler/$spreporter.spreporter/$suffix"

                switch ($spreporter) {
                    "SPOSReporter" { 
                        $spreporter = "SPSoftwareReporter"
                    }
                    "SPPlatformReporter" { 
                        $spreporter = "SPHardwareReporter"
                    }
                    "SPCtkReporter" { 
                        $spreporter = "SPSmartCardsReporter"
                    }
                    "SPSyncReporter" { 
                        $spreporter = "SPSyncServicesReporter"
                    }
                    "SPFontReporter" { 
                        $spreporter = "SPFontsReporter"
                    }
                }

                $dataType = $spreporter.Replace("Reporter","DataType")
            }
         
            $strings = Get-LocalizedStrings -LiteralPath $filePath -Localizations $([SPSettings]::ListAvailableLocalizations())

            $localizedStringsList.Add($dataType, $strings)
        }

        [SPLocalization]::LocalisedStrings = $localizedStringsList
    }
    
    static [String]LocalizedString([string]$string, [string]$dataType, [string]$language)
    {          
        if ($string -eq "_name")
        {
            return "Name"
        }

        if ($string -eq "_items")
        {
            return "Items"
        }
        
        if ([string]::IsNullOrEmpty($dataType))
        {
            $dataType = [SPSettings]::DataType
        }

        if ([string]::IsNullOrEmpty($language))
        {
            $language = [SPSettings]::Language
        }
        
        $items = $null

        if ([SPLocalization]::LocalisedStrings[$dataType])
        {
            $items = [SPLocalization]::LocalisedStrings[$dataType][$language]
        }

        foreach ($item in $items)
        {
            if ($item.$string)
            {
                return $item.$string
            }
            else
            {
                foreach($key in $item.GetEnumerator())
                {
                    if ( $string.StartsWith($key.Key + " " ) )
                    {
                        return $string.Replace($($key.Key), $($key.Value))
                    }
                }
            }  
        }

        foreach ($item in [SPLocalization]::LocalisedStrings['SPSupportDataType'][$language])
        {
            if ($item.$string)
            {
                return $item.$string
            }
        }

        return $string
    }
 
    static [hashtable]Dump()
    {
        return [SPLocalization]::LocalisedStrings
    }
}