chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
function Test-IntentionalSourceEqualValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LanguageCode,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$SourceValue,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[AllowEmptyString()]
|
||||
[string]$TargetValue = ''
|
||||
)
|
||||
|
||||
if ($SourceValue -in @(
|
||||
'MSI',
|
||||
'MSIX',
|
||||
'OK',
|
||||
'UniGetUI',
|
||||
'UniGetUI - {0} {1}',
|
||||
'WinGet COM API',
|
||||
'your@email.com',
|
||||
'{0}: {1}',
|
||||
'{0}: {1}, {2}'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'it' -and $SourceValue -ceq 'No' -and $TargetValue -ceq 'No') {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'it' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'DEBUG BUILD'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'ca' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'1 - Errors',
|
||||
'Error',
|
||||
'Global',
|
||||
'Local',
|
||||
'Manifest',
|
||||
'Manifests',
|
||||
'No',
|
||||
'Notes:',
|
||||
'Text'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'pt_PT' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Global',
|
||||
'Local'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'hr' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Manifest',
|
||||
'Status',
|
||||
'URL'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'es' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Error',
|
||||
'Global',
|
||||
'Local',
|
||||
'No',
|
||||
'URL'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'es-MX' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Error',
|
||||
'Global',
|
||||
'Local',
|
||||
'No',
|
||||
'URL',
|
||||
'Url'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'fr' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Ascendant',
|
||||
'Descendant',
|
||||
'Global',
|
||||
'Local',
|
||||
'Machine | Global',
|
||||
'Navigation',
|
||||
'Portable',
|
||||
'Source',
|
||||
'Sources',
|
||||
'Verbose',
|
||||
'Version',
|
||||
'installation',
|
||||
'option',
|
||||
'version {0}',
|
||||
'{0} minutes'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'nl' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'1 week',
|
||||
'Filters',
|
||||
'Manifest',
|
||||
'Status',
|
||||
'Updates',
|
||||
'URL',
|
||||
'update',
|
||||
'website',
|
||||
'{0} status'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'sk' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Manifest',
|
||||
'Text'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'de' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Global',
|
||||
'Manifest',
|
||||
'Name',
|
||||
'Repository',
|
||||
'Start',
|
||||
'Status',
|
||||
'Text',
|
||||
'Updates',
|
||||
'URL',
|
||||
'Verbose',
|
||||
'Version',
|
||||
'Version:',
|
||||
'optional',
|
||||
'{package} Installation',
|
||||
'{package} Update'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'sv' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Global',
|
||||
'Manifest',
|
||||
'Start',
|
||||
'Status',
|
||||
'Text',
|
||||
'Version',
|
||||
'Version:',
|
||||
'installation',
|
||||
'version {0}',
|
||||
'{0} installation',
|
||||
'{0} status',
|
||||
'{pm} version:'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'id' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Global',
|
||||
'Grid',
|
||||
'Manifest',
|
||||
'Status',
|
||||
'URL',
|
||||
'Verbose',
|
||||
'{0} status'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'fil' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Android Subsystem',
|
||||
'Default',
|
||||
'Error',
|
||||
'Global',
|
||||
'Grid',
|
||||
'Machine | Global',
|
||||
'Manifest',
|
||||
'OK',
|
||||
'Ok',
|
||||
'Package',
|
||||
'Password',
|
||||
'Portable',
|
||||
'Portable mode',
|
||||
'PreRelease',
|
||||
'Repository',
|
||||
'Source',
|
||||
'Source:',
|
||||
'Telemetry',
|
||||
'Text',
|
||||
'URL',
|
||||
'UniGetUI',
|
||||
'UniGetUI - {0} {1}',
|
||||
'User',
|
||||
'Username',
|
||||
'Verbose',
|
||||
'website',
|
||||
'library'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'tl' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Machine | Global'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$script:ProjectRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
|
||||
$script:LanguageRemap = [ordered]@{
|
||||
'pt-BR' = 'pt_BR'
|
||||
'pt-PT' = 'pt_PT'
|
||||
'nn-NO' = 'nn'
|
||||
'uk' = 'ua'
|
||||
'zh-Hans' = 'zh_CN'
|
||||
'zh-Hant' = 'zh_TW'
|
||||
}
|
||||
|
||||
$script:LanguageFlagsRemap = [ordered]@{
|
||||
'af' = 'za'
|
||||
'ar' = 'sa'
|
||||
'bs' = 'ba'
|
||||
'ca' = 'ad'
|
||||
'cs' = 'cz'
|
||||
'da' = 'dk'
|
||||
'el' = 'gr'
|
||||
'en' = 'gb'
|
||||
'eo' = 'https://upload.wikimedia.org/wikipedia/commons/f/f5/Flag_of_Esperanto.svg'
|
||||
'es-MX' = 'mx'
|
||||
'et' = 'ee'
|
||||
'fa' = 'ir'
|
||||
'fil' = 'ph'
|
||||
'gl' = 'es'
|
||||
'he' = 'il'
|
||||
'hi' = 'in'
|
||||
'ja' = 'jp'
|
||||
'ka' = 'ge'
|
||||
'ko' = 'kr'
|
||||
'ku' = 'iq'
|
||||
'mr' = 'in'
|
||||
'nb' = 'no'
|
||||
'nn' = 'no'
|
||||
'pt_BR' = 'br'
|
||||
'pt_PT' = 'pt'
|
||||
'si' = 'lk'
|
||||
'sr' = 'rs'
|
||||
'sv' = 'se'
|
||||
'sl' = 'si'
|
||||
'ta' = 'in'
|
||||
'vi' = 'vn'
|
||||
'zh_CN' = 'cn'
|
||||
'zh_TW' = 'tw'
|
||||
'zh' = 'cn'
|
||||
'bn' = 'bd'
|
||||
'tg' = 'ph'
|
||||
'sq' = 'al'
|
||||
'kn' = 'in'
|
||||
'sa' = 'in'
|
||||
'gu' = 'in'
|
||||
'ur' = 'pk'
|
||||
'be' = 'by'
|
||||
}
|
||||
|
||||
function Get-ProjectRoot {
|
||||
return $script:ProjectRoot
|
||||
}
|
||||
|
||||
function Get-ContributorsListPath {
|
||||
return Join-Path (Get-ProjectRoot) 'src\UniGetUI.Core.Data\Assets\Data\Contributors.list'
|
||||
}
|
||||
|
||||
function Get-TranslatorsJsonPath {
|
||||
return Join-Path (Get-ProjectRoot) 'src\Languages\Data\Translators.json'
|
||||
}
|
||||
|
||||
function Get-TranslatedPercentagesJsonPath {
|
||||
return Join-Path (Get-ProjectRoot) 'src\Languages\Data\TranslatedPercentages.json'
|
||||
}
|
||||
|
||||
function Get-LanguagesReferenceJsonPath {
|
||||
return Join-Path (Get-ProjectRoot) 'src\Languages\Data\LanguagesReference.json'
|
||||
}
|
||||
|
||||
function Get-LanguagesDirectoryPath {
|
||||
return Join-Path (Get-ProjectRoot) 'src\Languages'
|
||||
}
|
||||
|
||||
function Read-JsonDictionary {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
if (-not (Test-Path -Path $Path -PathType Leaf)) {
|
||||
return [ordered]@{}
|
||||
}
|
||||
|
||||
$content = [System.IO.File]::ReadAllText($Path)
|
||||
if ([string]::IsNullOrWhiteSpace($content)) {
|
||||
return [ordered]@{}
|
||||
}
|
||||
|
||||
$parsed = $content | ConvertFrom-Json -AsHashtable
|
||||
if ($null -eq $parsed) {
|
||||
return [ordered]@{}
|
||||
}
|
||||
|
||||
if (-not ($parsed -is [System.Collections.IDictionary])) {
|
||||
throw "JSON root must be an object: $Path"
|
||||
}
|
||||
|
||||
return $parsed
|
||||
}
|
||||
|
||||
function Get-ContributorsList {
|
||||
$path = Get-ContributorsListPath
|
||||
if (-not (Test-Path -Path $path -PathType Leaf)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
return @(
|
||||
Get-Content -Path $path -Encoding UTF8 |
|
||||
ForEach-Object { $_.Trim() } |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
||||
)
|
||||
}
|
||||
|
||||
function Get-LanguageCredits {
|
||||
return Read-JsonDictionary -Path (Get-TranslatorsJsonPath)
|
||||
}
|
||||
|
||||
function Get-TranslatedPercentages {
|
||||
return Read-JsonDictionary -Path (Get-TranslatedPercentagesJsonPath)
|
||||
}
|
||||
|
||||
function Get-LanguageReference {
|
||||
return Read-JsonDictionary -Path (Get-LanguagesReferenceJsonPath)
|
||||
}
|
||||
|
||||
function Get-LanguageRemap {
|
||||
return [ordered]@{} + $script:LanguageRemap
|
||||
}
|
||||
|
||||
function Get-LanguageFlagsRemap {
|
||||
return [ordered]@{} + $script:LanguageFlagsRemap
|
||||
}
|
||||
|
||||
function Get-TranslatorsFromCredits {
|
||||
param(
|
||||
[AllowNull()]
|
||||
[string]$Credits
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Credits)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
$contributors = Get-ContributorsList
|
||||
$translatorLookup = @{}
|
||||
foreach ($translator in ($Credits -split ',')) {
|
||||
$trimmed = $translator.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($trimmed)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$wasPrefixed = $trimmed.StartsWith('@', [System.StringComparison]::Ordinal)
|
||||
if ($wasPrefixed) {
|
||||
$trimmed = $trimmed.Substring(1)
|
||||
}
|
||||
|
||||
$link = ''
|
||||
if ($wasPrefixed -or ($contributors -contains $trimmed)) {
|
||||
$link = "https://github.com/$trimmed"
|
||||
}
|
||||
|
||||
$translatorLookup[$trimmed] = [pscustomobject]@{
|
||||
name = $trimmed
|
||||
link = $link
|
||||
}
|
||||
}
|
||||
|
||||
return @(
|
||||
$translatorLookup.Keys |
|
||||
Sort-Object { $_.ToLowerInvariant() } |
|
||||
ForEach-Object { $translatorLookup[$_] }
|
||||
)
|
||||
}
|
||||
|
||||
function ConvertTo-TranslatorMarkdown {
|
||||
param(
|
||||
[AllowNull()]
|
||||
[object]$Translators
|
||||
)
|
||||
|
||||
if ($null -eq $Translators) {
|
||||
return ''
|
||||
}
|
||||
|
||||
$translatorItems = @()
|
||||
if ($Translators -is [string]) {
|
||||
$translatorItems = Get-TranslatorsFromCredits -Credits $Translators
|
||||
}
|
||||
else {
|
||||
$translatorItems = @($Translators)
|
||||
}
|
||||
|
||||
$formatted = foreach ($translator in $translatorItems) {
|
||||
$name = [string]$translator.name
|
||||
$link = if ($null -eq $translator.link) { '' } else { [string]$translator.link }
|
||||
if ([string]::IsNullOrWhiteSpace($link)) {
|
||||
$name
|
||||
}
|
||||
else {
|
||||
"[$name]($link)"
|
||||
}
|
||||
}
|
||||
|
||||
return ($formatted -join ', ')
|
||||
}
|
||||
|
||||
function Get-LanguageFilePathMap {
|
||||
param(
|
||||
[switch]$AbsolutePaths
|
||||
)
|
||||
|
||||
$languageReference = Get-LanguageReference
|
||||
$languagesDirectory = Get-LanguagesDirectoryPath
|
||||
$result = [ordered]@{}
|
||||
|
||||
foreach ($entry in $languageReference.GetEnumerator()) {
|
||||
$code = [string]$entry.Key
|
||||
if ($code -eq 'default') {
|
||||
continue
|
||||
}
|
||||
|
||||
$fileName = "lang_$code.json"
|
||||
$result[$code] = if ($AbsolutePaths.IsPresent) { Join-Path $languagesDirectory $fileName } else { $fileName }
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function Get-MarkdownSupportLangs {
|
||||
return Get-MarkdownTranslationsTable
|
||||
}
|
||||
|
||||
function Get-MarkdownTranslationsTable {
|
||||
param(
|
||||
[switch]$IncludeZeroPercent,
|
||||
|
||||
[switch]$IncludeLanguageCode,
|
||||
|
||||
[switch]$IncludeFileColumn,
|
||||
|
||||
[bool]$IncludeTranslatedColumn = $true,
|
||||
|
||||
[bool]$IncludeCreditsColumn = $true,
|
||||
|
||||
[string]$MissingCreditsText = ''
|
||||
)
|
||||
|
||||
$languageReference = Get-LanguageReference
|
||||
$translationPercentages = Get-TranslatedPercentages
|
||||
$languageCredits = Get-LanguageCredits
|
||||
$flagRemap = Get-LanguageFlagsRemap
|
||||
$languagesDirectory = Get-LanguagesDirectoryPath
|
||||
|
||||
$lines = New-Object System.Collections.Generic.List[string]
|
||||
$headers = @('Language')
|
||||
$alignments = @(':--')
|
||||
|
||||
if ($IncludeLanguageCode.IsPresent) {
|
||||
$headers += 'Code'
|
||||
$alignments += ':--'
|
||||
}
|
||||
|
||||
if ($IncludeTranslatedColumn) {
|
||||
$headers += 'Translated'
|
||||
$alignments += ':--'
|
||||
}
|
||||
|
||||
if ($IncludeFileColumn.IsPresent) {
|
||||
$headers += 'File'
|
||||
$alignments += ':--'
|
||||
}
|
||||
|
||||
if ($IncludeCreditsColumn) {
|
||||
$headers += 'Contributor(s)'
|
||||
$alignments += '---'
|
||||
}
|
||||
|
||||
$lines.Add('| ' + ($headers -join ' | ') + ' |')
|
||||
$lines.Add('| ' + ($alignments -join ' | ') + ' |')
|
||||
|
||||
foreach ($entry in $languageReference.GetEnumerator()) {
|
||||
$languageCode = [string]$entry.Key
|
||||
if ($languageCode -eq 'default') {
|
||||
continue
|
||||
}
|
||||
|
||||
$languageFilePath = Join-Path $languagesDirectory ("lang_$languageCode.json")
|
||||
if (-not (Test-Path -Path $languageFilePath -PathType Leaf)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$percentage = if ($translationPercentages.Contains($languageCode)) { [string]$translationPercentages[$languageCode] } else { '100%' }
|
||||
if ($percentage -eq '0%' -and -not $IncludeZeroPercent.IsPresent) {
|
||||
continue
|
||||
}
|
||||
|
||||
$languageName = [string]$entry.Value
|
||||
$flag = if ($flagRemap.Contains($languageCode)) { [string]$flagRemap[$languageCode] } else { $languageCode }
|
||||
$credits = ''
|
||||
if ($IncludeCreditsColumn) {
|
||||
$credits = if ($languageCredits.Contains($languageCode)) {
|
||||
ConvertTo-TranslatorMarkdown -Translators $languageCredits[$languageCode]
|
||||
}
|
||||
else {
|
||||
''
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($credits) -and -not [string]::IsNullOrWhiteSpace($MissingCreditsText)) {
|
||||
$credits = $MissingCreditsText
|
||||
}
|
||||
}
|
||||
|
||||
$flagImageSource = if ([string]$flag -match '^[a-z]+://') { [string]$flag } else { "https://flagcdn.com/$flag.svg" }
|
||||
|
||||
$row = New-Object System.Collections.Generic.List[string]
|
||||
$row.Add("<img src='$flagImageSource' width=20> $languageName")
|
||||
|
||||
if ($IncludeLanguageCode.IsPresent) {
|
||||
$row.Add(('`{0}`' -f $languageCode))
|
||||
}
|
||||
|
||||
if ($IncludeTranslatedColumn) {
|
||||
$row.Add($percentage)
|
||||
}
|
||||
|
||||
if ($IncludeFileColumn.IsPresent) {
|
||||
$relativeLanguageFilePath = (Join-Path 'src/Languages' ("lang_{0}.json" -f $languageCode)) -replace '\\', '/'
|
||||
$row.Add("[lang_$languageCode.json]($relativeLanguageFilePath)")
|
||||
}
|
||||
|
||||
if ($IncludeCreditsColumn) {
|
||||
$row.Add($credits)
|
||||
}
|
||||
|
||||
$lines.Add('| ' + ($row -join ' | ') + ' |')
|
||||
}
|
||||
|
||||
$lines.Add('')
|
||||
return ($lines -join [Environment]::NewLine)
|
||||
}
|
||||
|
||||
function Get-TranslationDocumentationMarkdown {
|
||||
$coverageTable = Get-MarkdownTranslationsTable -IncludeZeroPercent -IncludeLanguageCode -IncludeFileColumn -IncludeCreditsColumn:$false
|
||||
$contributorsTable = Get-MarkdownTranslationsTable -IncludeZeroPercent -IncludeLanguageCode -IncludeTranslatedColumn:$false
|
||||
|
||||
$sections = @(
|
||||
'# Translations',
|
||||
'',
|
||||
'UniGetUI includes translations for the languages listed below.',
|
||||
'',
|
||||
'This page lists the supported languages, each locale file, current completion status, and the credited contributors for each translation.',
|
||||
'',
|
||||
'If you would like to help improve a translation or report an issue, please open an issue or submit a pull request.',
|
||||
'',
|
||||
'Translation discussion and coordination also happens in [GitHub discussion #4510](https://github.com/Devolutions/UniGetUI/discussions/4510).',
|
||||
'',
|
||||
'## Language Coverage',
|
||||
'',
|
||||
$coverageTable.TrimEnd(),
|
||||
'',
|
||||
'## Contributors',
|
||||
'',
|
||||
'We are grateful to everyone who contributes translations to UniGetUI. Contributor credits are sourced from [Translators.json](src/Languages/Data/Translators.json). If you would like to be added to or removed from the list for a particular language, please open a pull request.',
|
||||
'',
|
||||
$contributorsTable.TrimEnd(),
|
||||
'',
|
||||
'## Maintaining Translation Data',
|
||||
'',
|
||||
'The tables in this document are generated from the checked-in translation metadata. Completion is computed against the active English keys in `lang_en.json`; keys below the legacy boundary marker are excluded from the percentage.',
|
||||
'',
|
||||
'To refresh translated percentages, contributor metadata, and this document after locale changes, run:',
|
||||
'',
|
||||
'```powershell',
|
||||
'pwsh ./scripts/translation/Sync-TranslationMetadata.ps1 -AllLanguages -UpdateTranslationDoc',
|
||||
'```',
|
||||
'',
|
||||
'To inspect the current status without modifying files, run:',
|
||||
'',
|
||||
'```powershell',
|
||||
'pwsh ./scripts/translation/Get-TranslationStatus.ps1 -OutputFormat Markdown -OnlyIncomplete',
|
||||
'```',
|
||||
''
|
||||
)
|
||||
|
||||
return (($sections -join [Environment]::NewLine) + [Environment]::NewLine)
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function @(
|
||||
'Get-ProjectRoot',
|
||||
'Get-ContributorsListPath',
|
||||
'Get-TranslatorsJsonPath',
|
||||
'Get-TranslatedPercentagesJsonPath',
|
||||
'Get-LanguagesReferenceJsonPath',
|
||||
'Get-LanguagesDirectoryPath',
|
||||
'Get-ContributorsList',
|
||||
'Get-LanguageCredits',
|
||||
'Get-TranslatedPercentages',
|
||||
'Get-LanguageReference',
|
||||
'Get-LanguageRemap',
|
||||
'Get-LanguageFlagsRemap',
|
||||
'Get-TranslatorsFromCredits',
|
||||
'ConvertTo-TranslatorMarkdown',
|
||||
'Get-MarkdownSupportLangs',
|
||||
'Get-MarkdownTranslationsTable',
|
||||
'Get-TranslationDocumentationMarkdown',
|
||||
'Get-LanguageFilePathMap'
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$AsJson,
|
||||
[switch]$AbsolutePaths
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
Import-Module (Join-Path $PSScriptRoot 'LanguageData.psm1') -Force
|
||||
|
||||
$languageFileMap = Get-LanguageFilePathMap -AbsolutePaths:$AbsolutePaths.IsPresent
|
||||
|
||||
if ($AsJson.IsPresent) {
|
||||
$languageFileMap | ConvertTo-Json -Depth 5
|
||||
return
|
||||
}
|
||||
|
||||
$languageFileMap
|
||||
@@ -0,0 +1,497 @@
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$script:TranslationLegacyBoundaryKey = '__LEGACY_TRANSLATION_KEYS_BELOW__'
|
||||
$script:TranslationLegacyBoundaryValue = 'Legacy translation keys below are kept for backward compatibility with older UniGetUI builds. Do not translate or remove yet.'
|
||||
|
||||
function Assert-Command {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
if (-not (Get-Command -Name $Name -ErrorAction SilentlyContinue)) {
|
||||
throw "Required command not found on PATH: $Name"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-FullPath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
return [System.IO.Path]::GetFullPath($Path)
|
||||
}
|
||||
|
||||
function New-Utf8File {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Content
|
||||
)
|
||||
|
||||
$directory = Split-Path -Path $Path -Parent
|
||||
if (-not [string]::IsNullOrWhiteSpace($directory)) {
|
||||
New-Item -Path $directory -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$encoding = [System.Text.UTF8Encoding]::new($false)
|
||||
[System.IO.File]::WriteAllText($Path, $Content, $encoding)
|
||||
}
|
||||
|
||||
function New-OrderedStringMap {
|
||||
return [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::Ordinal)
|
||||
}
|
||||
|
||||
function Assert-NoDuplicateJsonKeys {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Content,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$pattern = [regex]'(?m)^\s*"((?:\\.|[^"])*)"\s*:'
|
||||
$seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||
$duplicates = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
foreach ($match in $pattern.Matches($Content)) {
|
||||
$key = [regex]::Unescape($match.Groups[1].Value)
|
||||
if (-not $seen.Add($key) -and -not $duplicates.Contains($key)) {
|
||||
$duplicates.Add($key)
|
||||
}
|
||||
}
|
||||
|
||||
if ($duplicates.Count -gt 0) {
|
||||
throw "JSON file contains duplicate key(s): $($duplicates -join ', '). Path: $Path"
|
||||
}
|
||||
}
|
||||
|
||||
function Read-OrderedJsonMap {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
if (-not (Test-Path -Path $Path -PathType Leaf)) {
|
||||
return (New-OrderedStringMap)
|
||||
}
|
||||
|
||||
$content = [System.IO.File]::ReadAllText((Get-FullPath -Path $Path))
|
||||
return Convert-JsonContentToOrderedMap -Content $content -Path $Path -DetectDuplicates
|
||||
}
|
||||
|
||||
function Read-OrderedJsonMapPermissive {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
if (-not (Test-Path -Path $Path -PathType Leaf)) {
|
||||
return (New-OrderedStringMap)
|
||||
}
|
||||
|
||||
$content = [System.IO.File]::ReadAllText((Get-FullPath -Path $Path))
|
||||
return Convert-JsonContentToOrderedMap -Content $content -Path $Path
|
||||
}
|
||||
|
||||
function Convert-JsonContentToOrderedMap {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Content,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
|
||||
[switch]$DetectDuplicates
|
||||
)
|
||||
|
||||
$result = New-OrderedStringMap
|
||||
if ([string]::IsNullOrWhiteSpace($Content)) {
|
||||
return $result
|
||||
}
|
||||
|
||||
if ($DetectDuplicates) {
|
||||
Assert-NoDuplicateJsonKeys -Content $Content -Path $Path
|
||||
}
|
||||
|
||||
$document = [System.Text.Json.JsonDocument]::Parse($Content)
|
||||
try {
|
||||
if ($document.RootElement.ValueKind -eq [System.Text.Json.JsonValueKind]::Null) {
|
||||
return $result
|
||||
}
|
||||
|
||||
if ($document.RootElement.ValueKind -ne [System.Text.Json.JsonValueKind]::Object) {
|
||||
throw "JSON file must contain a flat object: $Path"
|
||||
}
|
||||
|
||||
foreach ($property in $document.RootElement.EnumerateObject()) {
|
||||
if ($property.Value.ValueKind -eq [System.Text.Json.JsonValueKind]::Object -or $property.Value.ValueKind -eq [System.Text.Json.JsonValueKind]::Array) {
|
||||
throw "JSON file must contain a flat object with string values only: $Path"
|
||||
}
|
||||
|
||||
if ($property.Value.ValueKind -eq [System.Text.Json.JsonValueKind]::String) {
|
||||
$result[$property.Name] = $property.Value.GetString()
|
||||
continue
|
||||
}
|
||||
|
||||
if ($property.Value.ValueKind -eq [System.Text.Json.JsonValueKind]::Null) {
|
||||
$result[$property.Name] = ''
|
||||
continue
|
||||
}
|
||||
|
||||
$result[$property.Name] = $property.Value.ToString()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$document.Dispose()
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function ConvertTo-JsonStringLiteral {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
return ($Value | ConvertTo-Json -Compress)
|
||||
}
|
||||
|
||||
function Write-OrderedJsonMap {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$Map
|
||||
)
|
||||
|
||||
$lines = New-Object System.Collections.Generic.List[string]
|
||||
$lines.Add('{')
|
||||
|
||||
$index = 0
|
||||
$count = $Map.Count
|
||||
foreach ($entry in $Map.GetEnumerator()) {
|
||||
$index += 1
|
||||
$line = ' {0}: {1}' -f (ConvertTo-JsonStringLiteral -Value ([string]$entry.Key)), (ConvertTo-JsonStringLiteral -Value ([string]$entry.Value))
|
||||
if ($index -lt $count) {
|
||||
$line += ','
|
||||
}
|
||||
|
||||
$lines.Add($line)
|
||||
}
|
||||
|
||||
$lines.Add('}')
|
||||
New-Utf8File -Path $Path -Content (($lines -join "`r`n") + "`r`n")
|
||||
}
|
||||
|
||||
function Get-TranslationLegacyBoundaryKey {
|
||||
return $script:TranslationLegacyBoundaryKey
|
||||
}
|
||||
|
||||
function Get-TranslationLegacyBoundaryValue {
|
||||
return $script:TranslationLegacyBoundaryValue
|
||||
}
|
||||
|
||||
function Split-TranslationMapAtBoundary {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$Map
|
||||
)
|
||||
|
||||
$boundaryKey = Get-TranslationLegacyBoundaryKey
|
||||
$activeMap = New-OrderedStringMap
|
||||
$legacyMap = New-OrderedStringMap
|
||||
$hasBoundary = $false
|
||||
$boundaryReached = $false
|
||||
|
||||
foreach ($entry in $Map.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
$value = [string]$entry.Value
|
||||
|
||||
if ($key -ceq $boundaryKey) {
|
||||
$hasBoundary = $true
|
||||
$boundaryReached = $true
|
||||
continue
|
||||
}
|
||||
|
||||
if ($boundaryReached) {
|
||||
$legacyMap[$key] = $value
|
||||
}
|
||||
else {
|
||||
$activeMap[$key] = $value
|
||||
}
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
HasBoundary = $hasBoundary
|
||||
ActiveMap = $activeMap
|
||||
LegacyMap = $legacyMap
|
||||
}
|
||||
}
|
||||
|
||||
function Join-TranslationMapWithBoundary {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$ActiveMap,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$LegacyMap,
|
||||
|
||||
[switch]$IncludeBoundary
|
||||
)
|
||||
|
||||
$result = New-OrderedStringMap
|
||||
foreach ($entry in $ActiveMap.GetEnumerator()) {
|
||||
$result[[string]$entry.Key] = [string]$entry.Value
|
||||
}
|
||||
|
||||
if ($IncludeBoundary) {
|
||||
$result[(Get-TranslationLegacyBoundaryKey)] = Get-TranslationLegacyBoundaryValue
|
||||
}
|
||||
|
||||
foreach ($entry in $LegacyMap.GetEnumerator()) {
|
||||
$result[[string]$entry.Key] = [string]$entry.Value
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function Test-OrderedStringMapsEqual {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$Left,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$Right
|
||||
)
|
||||
|
||||
if ($Left.Count -ne $Right.Count) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$leftEntries = @($Left.GetEnumerator())
|
||||
$rightEntries = @($Right.GetEnumerator())
|
||||
for ($index = 0; $index -lt $leftEntries.Count; $index += 1) {
|
||||
if ([string]$leftEntries[$index].Key -cne [string]$rightEntries[$index].Key) {
|
||||
return $false
|
||||
}
|
||||
|
||||
if ([string]$leftEntries[$index].Value -cne [string]$rightEntries[$index].Value) {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
function Invoke-CirupJson {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$Arguments
|
||||
)
|
||||
|
||||
Assert-Command -Name 'cirup'
|
||||
|
||||
$output = & cirup @Arguments --output-format json
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "cirup command failed: cirup $($Arguments -join ' ')"
|
||||
}
|
||||
|
||||
$jsonText = [string]::Join([Environment]::NewLine, @($output)).Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($jsonText)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
$parsed = $jsonText | ConvertFrom-Json -AsHashtable
|
||||
return @($parsed)
|
||||
}
|
||||
|
||||
function Get-RepositoryRoot {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$WorkingDirectory
|
||||
)
|
||||
|
||||
$repoRoot = & git -C $WorkingDirectory rev-parse --show-toplevel
|
||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($repoRoot)) {
|
||||
throw "Unable to resolve git repository root from '$WorkingDirectory'."
|
||||
}
|
||||
|
||||
return $repoRoot.Trim()
|
||||
}
|
||||
|
||||
function Get-RepoRelativePath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RepositoryRoot,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath
|
||||
)
|
||||
|
||||
$repoRootFull = Get-FullPath -Path $RepositoryRoot
|
||||
$filePathFull = Get-FullPath -Path $FilePath
|
||||
$repoRootWithSeparator = $repoRootFull.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) + [System.IO.Path]::DirectorySeparatorChar
|
||||
|
||||
if (-not $filePathFull.StartsWith($repoRootWithSeparator, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "File '$filePathFull' is not located under repository root '$repoRootFull'."
|
||||
}
|
||||
|
||||
return [System.IO.Path]::GetRelativePath($repoRootFull, $filePathFull).Replace('\', '/')
|
||||
}
|
||||
|
||||
function Get-SafeLabel {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
$safe = $Value -replace '[^A-Za-z0-9._-]+', '-'
|
||||
$safe = $safe.Trim('-')
|
||||
if ([string]::IsNullOrWhiteSpace($safe)) {
|
||||
return 'base'
|
||||
}
|
||||
|
||||
return $safe
|
||||
}
|
||||
|
||||
function Get-PatchBaseName {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$NeutralJsonPath
|
||||
)
|
||||
|
||||
$stem = [System.IO.Path]::GetFileNameWithoutExtension($NeutralJsonPath)
|
||||
if ($stem -match '^(.*)_en$' -and -not [string]::IsNullOrWhiteSpace($matches[1])) {
|
||||
return $matches[1]
|
||||
}
|
||||
|
||||
return $stem
|
||||
}
|
||||
|
||||
function Get-LanguagesReferencePath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$NeutralJsonPath
|
||||
)
|
||||
|
||||
$languagesDirectory = Split-Path -Path (Get-FullPath -Path $NeutralJsonPath) -Parent
|
||||
return Join-Path $languagesDirectory 'Data\LanguagesReference.json'
|
||||
}
|
||||
|
||||
function Assert-LanguageCodeKnown {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LanguagesReferencePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LanguageCode
|
||||
)
|
||||
|
||||
$referenceMap = Read-OrderedJsonMap -Path $LanguagesReferencePath
|
||||
if (-not $referenceMap.Contains($LanguageCode)) {
|
||||
throw "Unknown language code '$LanguageCode'. Expected one of the language codes from $LanguagesReferencePath"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-NeedsTranslation {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Key,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$SourceMap,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$TargetMap
|
||||
)
|
||||
|
||||
if (-not $TargetMap.Contains($Key)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
$targetValue = [string]$TargetMap[$Key]
|
||||
if ([string]::IsNullOrWhiteSpace($targetValue)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
return $targetValue -ceq [string]$SourceMap[$Key]
|
||||
}
|
||||
|
||||
function Get-PlaceholderTokens {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
$tokens = foreach ($match in [regex]::Matches($Value, '\{[A-Za-z0-9_]+(?:,[^}:]+)?(?::[^}]+)?\}')) {
|
||||
$match.Value
|
||||
}
|
||||
|
||||
return @($tokens | Sort-Object)
|
||||
}
|
||||
|
||||
function Get-HtmlTokens {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
$tokens = foreach ($match in [regex]::Matches($Value, '</?[^>]+?>')) {
|
||||
$match.Value
|
||||
}
|
||||
|
||||
return @($tokens | Sort-Object)
|
||||
}
|
||||
|
||||
function Get-NewlineCount {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
return ([regex]::Matches($Value, "`r`n|`n")).Count
|
||||
}
|
||||
|
||||
function Assert-TranslationStructure {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$SourceValue,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$TranslatedValue,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Key
|
||||
)
|
||||
|
||||
$sourcePlaceholderSignature = (Get-PlaceholderTokens -Value $SourceValue) -join "`n"
|
||||
$translatedPlaceholderSignature = (Get-PlaceholderTokens -Value $TranslatedValue) -join "`n"
|
||||
if ($sourcePlaceholderSignature -ne $translatedPlaceholderSignature) {
|
||||
throw "Placeholder mismatch for key '$Key'."
|
||||
}
|
||||
|
||||
$sourceHtmlSignature = (Get-HtmlTokens -Value $SourceValue) -join "`n"
|
||||
$translatedHtmlSignature = (Get-HtmlTokens -Value $TranslatedValue) -join "`n"
|
||||
if ($sourceHtmlSignature -ne $translatedHtmlSignature) {
|
||||
throw "HTML fragment mismatch for key '$Key'."
|
||||
}
|
||||
|
||||
if ((Get-NewlineCount -Value $SourceValue) -ne (Get-NewlineCount -Value $TranslatedValue)) {
|
||||
throw "Line-break mismatch for key '$Key'."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
. (Join-Path $PSScriptRoot 'TranslationJsonTools.ps1')
|
||||
|
||||
$script:SupportedSourceExtensions = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||
[void]$script:SupportedSourceExtensions.Add('.cs')
|
||||
[void]$script:SupportedSourceExtensions.Add('.xaml')
|
||||
[void]$script:SupportedSourceExtensions.Add('.axaml')
|
||||
|
||||
$script:ExcludedDirectoryNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($name in @('.git', '.vs', 'bin', 'obj', 'generated', 'node_modules', 'packages')) {
|
||||
[void]$script:ExcludedDirectoryNames.Add($name)
|
||||
}
|
||||
|
||||
function Resolve-TranslationSyncRepositoryRoot {
|
||||
param(
|
||||
[string]$RepositoryRoot
|
||||
)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($RepositoryRoot)) {
|
||||
return Get-FullPath -Path $RepositoryRoot
|
||||
}
|
||||
|
||||
return [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
|
||||
}
|
||||
|
||||
function Resolve-EnglishLanguageFilePath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ResolvedRepositoryRoot,
|
||||
|
||||
[string]$EnglishFilePath
|
||||
)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($EnglishFilePath)) {
|
||||
return Get-FullPath -Path $EnglishFilePath
|
||||
}
|
||||
|
||||
return Join-Path $ResolvedRepositoryRoot 'src\Languages\lang_en.json'
|
||||
}
|
||||
|
||||
function Test-TranslationSourceFileIncluded {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.IO.FileInfo]$File,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ResolvedRepositoryRoot
|
||||
)
|
||||
|
||||
if (-not $script:SupportedSourceExtensions.Contains($File.Extension)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$relativePath = [System.IO.Path]::GetRelativePath($ResolvedRepositoryRoot, $File.FullName).Replace('\', '/')
|
||||
foreach ($segment in $relativePath.Split('/')) {
|
||||
if ($script:ExcludedDirectoryNames.Contains($segment)) {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
if ($relativePath -like 'src/Languages/lang_*.json') {
|
||||
return $false
|
||||
}
|
||||
|
||||
return $relativePath.StartsWith('src/', [System.StringComparison]::OrdinalIgnoreCase)
|
||||
}
|
||||
|
||||
function Get-TranslationSourceFiles {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ResolvedRepositoryRoot
|
||||
)
|
||||
|
||||
return @(
|
||||
Get-ChildItem -Path $ResolvedRepositoryRoot -File -Recurse -ErrorAction SilentlyContinue |
|
||||
Where-Object { Test-TranslationSourceFileIncluded -File $_ -ResolvedRepositoryRoot $ResolvedRepositoryRoot } |
|
||||
Sort-Object FullName
|
||||
)
|
||||
}
|
||||
|
||||
function Get-LineNumberFromIndex {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Text,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$Index
|
||||
)
|
||||
|
||||
if ($Index -le 0) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return ([regex]::Matches($Text.Substring(0, $Index), "`r`n|`n")).Count + 1
|
||||
}
|
||||
|
||||
function Convert-CSharpStringLiteralValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Literal
|
||||
)
|
||||
|
||||
if ($Literal.StartsWith('@"', [System.StringComparison]::Ordinal)) {
|
||||
return $Literal.Substring(2, $Literal.Length - 3).Replace('""', '"')
|
||||
}
|
||||
|
||||
return [regex]::Unescape($Literal.Substring(1, $Literal.Length - 2))
|
||||
}
|
||||
|
||||
function Add-TranslationSourceKey {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Key,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SourcePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$LineNumber,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IList]$KeyOrder,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$SourcesByKey
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Key)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (-not $SourcesByKey.Contains($Key)) {
|
||||
$SourcesByKey[$Key] = New-Object System.Collections.Generic.List[string]
|
||||
$KeyOrder.Add($Key)
|
||||
}
|
||||
|
||||
$location = '{0}:{1}' -f $SourcePath.Replace('\', '/'), $LineNumber
|
||||
$locations = [System.Collections.Generic.List[string]]$SourcesByKey[$Key]
|
||||
if (-not $locations.Contains($location)) {
|
||||
$locations.Add($location)
|
||||
}
|
||||
}
|
||||
|
||||
function Add-TranslationSourceWarning {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Type,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SourcePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$LineNumber,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Message,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IList]$Warnings
|
||||
)
|
||||
|
||||
$Warnings.Add([pscustomobject]@{
|
||||
Type = $Type
|
||||
Path = $SourcePath.Replace('\', '/')
|
||||
Line = $LineNumber
|
||||
Message = $Message
|
||||
})
|
||||
}
|
||||
|
||||
function Get-CSharpTranslationMatches {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Content,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IList]$KeyOrder,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$SourcesByKey,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IList]$Warnings
|
||||
)
|
||||
|
||||
$literalPattern = [regex]'CoreTools\s*\.\s*(?:Translate|AutoTranslated)\(\s*(?<literal>@"(?:[^"]|"")*"|"(?:\\.|[^"\\])*")'
|
||||
foreach ($match in $literalPattern.Matches($Content)) {
|
||||
$literal = [string]$match.Groups['literal'].Value
|
||||
$lineNumber = Get-LineNumberFromIndex -Text $Content -Index $match.Index
|
||||
Add-TranslationSourceKey -Key (Convert-CSharpStringLiteralValue -Literal $literal) -SourcePath $FilePath -LineNumber $lineNumber -KeyOrder $KeyOrder -SourcesByKey $SourcesByKey
|
||||
}
|
||||
|
||||
$interpolatedPattern = [regex]'CoreTools\s*\.\s*Translate\(\s*\$@?"'
|
||||
foreach ($match in $interpolatedPattern.Matches($Content)) {
|
||||
$lineNumber = Get-LineNumberFromIndex -Text $Content -Index $match.Index
|
||||
Add-TranslationSourceWarning -Type 'InterpolatedTranslateCall' -SourcePath $FilePath -LineNumber $lineNumber -Message 'Interpolated CoreTools.Translate call is not synchronized automatically.' -Warnings $Warnings
|
||||
}
|
||||
}
|
||||
|
||||
function Get-TranslatedTextBlockMatches {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Content,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IList]$KeyOrder,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$SourcesByKey
|
||||
)
|
||||
|
||||
$translatedTextBlockPattern = [regex]'<(?<tag>[A-Za-z0-9_:.\-]+TranslatedTextBlock)\b[^>]*\bText="(?<text>[^"]*)"'
|
||||
foreach ($match in $translatedTextBlockPattern.Matches($Content)) {
|
||||
$lineNumber = Get-LineNumberFromIndex -Text $Content -Index $match.Index
|
||||
$text = [System.Net.WebUtility]::HtmlDecode([string]$match.Groups['text'].Value)
|
||||
Add-TranslationSourceKey -Key $text -SourcePath $FilePath -LineNumber $lineNumber -KeyOrder $KeyOrder -SourcesByKey $SourcesByKey
|
||||
}
|
||||
}
|
||||
|
||||
function Get-AvaloniaTranslateMarkupMatches {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Content,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IList]$KeyOrder,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$SourcesByKey,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IList]$Warnings
|
||||
)
|
||||
|
||||
$markupPattern = [regex]'\{t:Translate(?<body>[^}]*)\}'
|
||||
foreach ($match in $markupPattern.Matches($Content)) {
|
||||
$body = [string]$match.Groups['body'].Value
|
||||
$trimmedBody = $body.Trim()
|
||||
$lineNumber = Get-LineNumberFromIndex -Text $Content -Index $match.Index
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($trimmedBody)) {
|
||||
Add-TranslationSourceWarning -Type 'EmptyMarkupTranslate' -SourcePath $FilePath -LineNumber $lineNumber -Message 'Empty {t:Translate} markup extension was ignored.' -Warnings $Warnings
|
||||
continue
|
||||
}
|
||||
|
||||
$namedMatch = [regex]::Match($trimmedBody, '^Text\s*=\s*(?:''(?<single>[^'']*)''|"(?<double>[^"]*)")$')
|
||||
if ($namedMatch.Success) {
|
||||
$value = if ($namedMatch.Groups['single'].Success) { $namedMatch.Groups['single'].Value } else { $namedMatch.Groups['double'].Value }
|
||||
Add-TranslationSourceKey -Key ([System.Net.WebUtility]::HtmlDecode($value)) -SourcePath $FilePath -LineNumber $lineNumber -KeyOrder $KeyOrder -SourcesByKey $SourcesByKey
|
||||
continue
|
||||
}
|
||||
|
||||
Add-TranslationSourceKey -Key ([System.Net.WebUtility]::HtmlDecode($trimmedBody)) -SourcePath $FilePath -LineNumber $lineNumber -KeyOrder $KeyOrder -SourcesByKey $SourcesByKey
|
||||
}
|
||||
}
|
||||
|
||||
function Get-TranslationSourceSnapshot {
|
||||
param(
|
||||
[string]$RepositoryRoot
|
||||
)
|
||||
|
||||
$resolvedRepositoryRoot = Resolve-TranslationSyncRepositoryRoot -RepositoryRoot $RepositoryRoot
|
||||
$sourceFiles = Get-TranslationSourceFiles -ResolvedRepositoryRoot $resolvedRepositoryRoot
|
||||
$keyOrder = New-Object System.Collections.Generic.List[string]
|
||||
$sourcesByKey = [ordered]@{}
|
||||
$warnings = New-Object System.Collections.Generic.List[object]
|
||||
|
||||
foreach ($file in $sourceFiles) {
|
||||
$relativePath = [System.IO.Path]::GetRelativePath($resolvedRepositoryRoot, $file.FullName).Replace('\', '/')
|
||||
$content = [System.IO.File]::ReadAllText($file.FullName)
|
||||
|
||||
switch ($file.Extension.ToLowerInvariant()) {
|
||||
'.cs' {
|
||||
Get-CSharpTranslationMatches -FilePath $relativePath -Content $content -KeyOrder $keyOrder -SourcesByKey $sourcesByKey -Warnings $warnings
|
||||
}
|
||||
'.xaml' {
|
||||
Get-TranslatedTextBlockMatches -FilePath $relativePath -Content $content -KeyOrder $keyOrder -SourcesByKey $sourcesByKey
|
||||
}
|
||||
'.axaml' {
|
||||
Get-TranslatedTextBlockMatches -FilePath $relativePath -Content $content -KeyOrder $keyOrder -SourcesByKey $sourcesByKey
|
||||
Get-AvaloniaTranslateMarkupMatches -FilePath $relativePath -Content $content -KeyOrder $keyOrder -SourcesByKey $sourcesByKey -Warnings $warnings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$orderedKeyMap = New-OrderedStringMap
|
||||
foreach ($key in $keyOrder) {
|
||||
$orderedKeyMap[$key] = $key
|
||||
}
|
||||
|
||||
$sourceFilePaths = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($sourceFile in $sourceFiles) {
|
||||
$sourceFilePaths.Add($sourceFile.FullName)
|
||||
}
|
||||
|
||||
$warningItems = New-Object System.Collections.Generic.List[object]
|
||||
foreach ($warning in $warnings) {
|
||||
$warningItems.Add($warning)
|
||||
}
|
||||
|
||||
$sourceFileArray = $sourceFilePaths.ToArray()
|
||||
$keyOrderArray = $keyOrder.ToArray()
|
||||
$warningArray = $warningItems.ToArray()
|
||||
|
||||
$snapshot = New-Object -TypeName psobject
|
||||
$snapshot | Add-Member -NotePropertyName RepositoryRoot -NotePropertyValue $resolvedRepositoryRoot
|
||||
$snapshot | Add-Member -NotePropertyName SourceFiles -NotePropertyValue $sourceFileArray
|
||||
$snapshot | Add-Member -NotePropertyName KeyOrder -NotePropertyValue $keyOrderArray
|
||||
$snapshot | Add-Member -NotePropertyName Keys -NotePropertyValue $orderedKeyMap
|
||||
$snapshot | Add-Member -NotePropertyName SourcesByKey -NotePropertyValue $sourcesByKey
|
||||
$snapshot | Add-Member -NotePropertyName Warnings -NotePropertyValue $warningArray
|
||||
|
||||
return $snapshot
|
||||
}
|
||||
Reference in New Issue
Block a user