Add localization for app names and descriptions (#769)

* Translate app names and descriptions in Config/Apps.json

* Document Apps section and first-ID translation key for multi-ID apps

* Fix app list breaking when "Only show installed" runs translation in an isolated runspace
This commit is contained in:
Sashank
2026-10-01 21:25:34 +02:00
committed by GitHub
parent 32024662f3
commit 6feebba878
11 changed files with 705 additions and 38 deletions
+51 -3
View File
@@ -70,8 +70,8 @@ function Import-AppDetailsFromJson {
# Use first AppId for fallback names, join all for display
$primaryAppId = $appIdArray[0]
$appIdDisplay = $appIdArray -join ', '
$friendlyName = if ($appData.FriendlyName) { $appData.FriendlyName } else { $primaryAppId }
$displayName = if ($appData.FriendlyName) { "$($appData.FriendlyName) ($appIdDisplay)" } else { $appIdDisplay }
$friendlyName = Get-Translation -Key $primaryAppId -Field 'FriendlyName' -Section 'Apps'
$displayName = "$friendlyName ($appIdDisplay)"
$isChecked = if ($InitialCheckedFromJson) { $appData.SelectedByDefault } else { $false }
$apps += [PSCustomObject]@{
@@ -80,7 +80,7 @@ function Import-AppDetailsFromJson {
FriendlyName = $friendlyName
DisplayName = $displayName
IsChecked = $isChecked
Description = $appData.Description
Description = Get-Translation -Key $primaryAppId -Field 'Description' -Section 'Apps'
SelectedByDefault = $appData.SelectedByDefault
Recommendation = $appData.Recommendation
RemovalMethod = if ($appData.RemovalMethod -and $appData.RemovalMethod -eq 'WinGet') { 'WinGet' } else { 'Appx' }
@@ -89,3 +89,51 @@ function Import-AppDetailsFromJson {
return $apps
}
<#
.SYNOPSIS
Runs Import-AppDetailsFromJson in a background runspace via Invoke-NonBlocking.
.DESCRIPTION
The runspace inherits neither dot-sourced functions nor script-scoped variables, so
this dot-sources the app loader, its winget helper, and the language loader inside the
scriptblock, and passes $Lang through to be reassigned to $script:Lang there.
.PARAMETER OnlyInstalled
Filters the results to applications detected through Appx or the supplied
winget installation list.
.PARAMETER InstalledList
A pre-fetched winget installation list used when filtering installed apps.
.PARAMETER InitialCheckedFromJson
Sets each returned app's IsChecked value from its SelectedByDefault setting.
.PARAMETER Lang
The loaded language object ($script:Lang) to make available inside the runspace.
#>
function Invoke-AppDetailsFromJsonAsync {
param (
[string]$LoaderScriptPath = $script:LoadAppsDetailsScriptPath,
[string]$HelperScriptPath = $script:TestAppInWingetListScriptPath,
[string]$LanguageFileScriptPath = $script:ImportLanguageFileScriptPath,
[string]$AppsFilePath = $script:AppsListFilePath,
[object[]]$InstalledList = $null,
[switch]$OnlyInstalled,
[switch]$InitialCheckedFromJson,
[object]$Lang = $script:Lang
)
$onlyInstalledValue = [bool]$OnlyInstalled
$initialCheckedFromJsonValue = [bool]$InitialCheckedFromJson
return Invoke-NonBlocking -ScriptBlock {
param($loaderScript, $helperScript, $languageFileScript, $appsListFilePath, $installedList, $onlyInstalled, $initialCheckedFromJson, $Lang)
$script:AppsListFilePath = $appsListFilePath
$script:Lang = $Lang
. $helperScript
. $languageFileScript
. $loaderScript
Import-AppDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$initialCheckedFromJson
} -ArgumentList $LoaderScriptPath, $HelperScriptPath, $LanguageFileScriptPath, $AppsFilePath, $InstalledList, $onlyInstalledValue, $initialCheckedFromJsonValue, $Lang
}
+17 -14
View File
@@ -25,7 +25,7 @@ function Resolve-LanguageFolder {
<#
.SYNOPSIS
Loads Chrome/Features/Categories JSON for a language folder, or returns $null on failure.
Loads Chrome/Features/Categories/Apps JSON for a language folder, or returns $null on failure.
#>
function Import-LanguageContent {
param(
@@ -38,8 +38,9 @@ function Import-LanguageContent {
$chrome = Import-JsonFile -filePath (Join-Path $folderPath 'Chrome.json')
$features = Import-JsonFile -filePath (Join-Path $folderPath 'Features.json')
$categories = Import-JsonFile -filePath (Join-Path $folderPath 'Categories.json')
$apps = Import-JsonFile -filePath (Join-Path $folderPath 'Apps.json')
if (-not $chrome -or -not $features -or -not $categories) {
if (-not $chrome -or -not $features -or -not $categories -or -not $apps) {
return $null
}
@@ -49,6 +50,7 @@ function Import-LanguageContent {
Features = $features.Features
UiGroups = $features.UiGroups
Categories = $categories
Apps = $apps
}
}
@@ -128,7 +130,7 @@ function Get-LanguageFallbackChain {
<#
.SYNOPSIS
Finds the named section object (Chrome, Features, UiGroups, or Categories) that owns a key.
Finds the named section object (Chrome, Features, UiGroups, Categories, or Apps) that owns a key.
.DESCRIPTION
A FeatureId, GroupId, and CategoryId aren't guaranteed to be disjoint (Config/Features.json
@@ -142,7 +144,7 @@ function Find-TranslationSection {
[object]$Lang,
[Parameter(Mandatory)]
[string]$Key,
[ValidateSet('', 'Chrome', 'Features', 'UiGroups', 'Categories')]
[ValidateSet('', 'Chrome', 'Features', 'UiGroups', 'Categories', 'Apps')]
[string]$Section = ''
)
@@ -154,7 +156,7 @@ function Find-TranslationSection {
return $null
}
foreach ($sectionName in 'Chrome', 'Features', 'UiGroups', 'Categories') {
foreach ($sectionName in 'Chrome', 'Features', 'UiGroups', 'Categories', 'Apps') {
$sectionObject = $Lang.$sectionName
if ($sectionObject -and $sectionObject.PSObject.Properties[$Key]) {
return $sectionObject
@@ -169,14 +171,15 @@ function Find-TranslationSection {
Looks up a translated value from the active language, falling back to en-US, then to the key itself.
.DESCRIPTION
Chrome.json keys are flat strings, so -Field is omitted for those. Features/UiGroups/Categories
keys (FeatureId/GroupId/CategoryId) resolve to an object, so -Field picks the property on it
(Label, ToolTip, ApplyText, UndoLabel, ApplyUndoText). One generic lookup covers every section
instead of a separate function per section.
Chrome.json keys are flat strings, so -Field is omitted for those. Features/UiGroups/Categories/Apps
keys (FeatureId/GroupId/CategoryId/AppId) resolve to an object, so -Field picks the property on it
(Label, ToolTip, ApplyText, UndoLabel, ApplyUndoText for Features/UiGroups/Categories;
FriendlyName, Description for Apps). One generic lookup covers every section instead of a
separate function per section.
A FeatureId and a GroupId aren't guaranteed to be distinct strings, so pass -Section
('Features', 'UiGroups', or 'Categories') whenever the caller knows which one it means,
rather than relying on Find-TranslationSection's search order to guess correctly.
('Features', 'UiGroups', 'Categories', or 'Apps') whenever the caller knows which one it
means, rather than relying on Find-TranslationSection's search order to guess correctly.
When -Count is supplied, tries the plural-suffixed key ("$Key`_$category") before the bare key,
so callers don't need to add a plural variant for every string, only the ones that need one.
@@ -191,7 +194,7 @@ function Get-Translation {
[object]$Lang = $script:Lang,
[Nullable[int]]$Count = $null,
[object[]]$FormatArgs = $null,
[ValidateSet('', 'Chrome', 'Features', 'UiGroups', 'Categories')]
[ValidateSet('', 'Chrome', 'Features', 'UiGroups', 'Categories', 'Apps')]
[string]$Section = ''
)
@@ -282,7 +285,7 @@ function Get-GroupValueTranslation {
Flattens a loaded language object into a sorted set of dotted key paths.
.DESCRIPTION
Chrome.json keys are already flat and are used as-is. Features/UiGroups/Categories
Chrome.json keys are already flat and are used as-is. Features/UiGroups/Categories/Apps
keys are one level deeper (EntryId -> {Field: value}), so each field becomes its own
"Section.EntryId.Field" path (e.g. "Features.DisableTelemetry.Label"). UiGroups' nested
Values map becomes "UiGroups.GroupId.Values.FeatureId". Used by Test-LanguageKeyCoverage
@@ -301,7 +304,7 @@ function Get-LanguageKeyPaths {
$paths.Add($chromeKey)
}
foreach ($sectionName in 'Features', 'Categories') {
foreach ($sectionName in 'Features', 'Categories', 'Apps') {
$section = $Lang.$sectionName
if (-not $section) { continue }
+2 -14
View File
@@ -391,27 +391,15 @@ function Add-AppsToMainWindow {
$script:MainWindowLastSelectedCheckbox = $null
$loaderScriptPath = $script:LoadAppsDetailsScriptPath
$helperScriptPath = $script:TestAppInWingetListScriptPath
$appsFilePath = $script:AppsListFilePath
$onlyInstalled = [bool]$OnlyInstalledAppsBox.IsChecked
# Use preloaded data if available; otherwise load in background job
# Use preloaded data if available; otherwise load in a background job to keep the UI responsive.
if (-not $onlyInstalled -and $script:PreloadedAppData) {
$rawAppData = $script:PreloadedAppData
$script:PreloadedAppData = $null
}
else {
# Load apps details in a background job to keep the UI responsive.
# The helper is dot-sourced inside the job because the runspace
# does not inherit the parent scope's dot-sourced functions.
$rawAppData = Invoke-NonBlocking -ScriptBlock {
param($loaderScript, $helperScript, $appsListFilePath, $installedList, $onlyInstalled)
$script:AppsListFilePath = $appsListFilePath
. $helperScript
. $loaderScript
Import-AppDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$false
} -ArgumentList $loaderScriptPath, $helperScriptPath, $appsFilePath, $ListOfApps, $onlyInstalled
$rawAppData = Invoke-AppDetailsFromJsonAsync -OnlyInstalled:$onlyInstalled -InstalledList $ListOfApps
}
$appsToAdd = @($rawAppData | Where-Object { $_ -and ($_.AppId -or $_.FriendlyName) } | Sort-Object -Property FriendlyName)