From 6feebba8787976d689f554aa502240c9fa620a5d Mon Sep 17 00:00:00 2001 From: Sashank Date: Fri, 2 Oct 2026 00:55:34 +0530 Subject: [PATCH] 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 --- .github/CONTRIBUTING.md | 14 +- Config/Languages/en-US/Apps.json | 566 ++++++++++++++++++ Scripts/FileIO/Import-AppDetailsFromJson.ps1 | 54 +- Scripts/FileIO/Import-LanguageFile.ps1 | 31 +- Scripts/GUI/MainWindow-AppSelection.ps1 | 16 +- Tests/Import-LanguageFile.Tests.ps1 | 10 +- Tests/MainWindow-AppSelection.Tests.ps1 | 26 + Tests/Settings-And-Apps-FileIO.Tests.ps1 | 18 +- .../TestData/LanguageLoading/en-US/Apps.json | 6 + .../TestData/LanguageLoading/es-ES/Apps.json | 1 + Win11Debloat.ps1 | 1 + 11 files changed, 705 insertions(+), 38 deletions(-) create mode 100644 Config/Languages/en-US/Apps.json create mode 100644 Tests/TestData/LanguageLoading/en-US/Apps.json create mode 100644 Tests/TestData/LanguageLoading/es-ES/Apps.json diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 47224f1..1b08083 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -196,7 +196,7 @@ Avoid these common mistakes when contributing: 8. **Hardcoded Paths**: When writing PowerShell logic, use `$PSScriptRoot` and script variables instead of hardcoded paths. This ensures the script works regardless of where it's installed. -9. **Missing Translation Entry**: A new Feature, Category, or UI Group needs a matching entry in `Config/Languages/en-US/` (see [Localizing UI Text](#localizing-ui-text)). Without one, the GUI silently falls back to displaying the raw `FeatureId`/`CategoryId`/`GroupId` instead of your `Label`/`ToolTip` text. +9. **Missing Translation Entry**: A new Feature, Category, UI Group, or App needs a matching entry in `Config/Languages/en-US/` (see [Localizing UI Text](#localizing-ui-text)). Without one, the GUI silently falls back to displaying the raw `FeatureId`/`CategoryId`/`GroupId`/`AppId` instead of your `Label`/`ToolTip`/`FriendlyName`/`Description` text. ## Implementing New Features @@ -233,7 +233,8 @@ To add a new app that can be removed via Win11Debloat: - `FriendlyName`: Display name shown in the GUI. - `AppId`: The package name from `Get-AppxPackage` or the `Id` from `winget list`, depending on removal method. Use an array when one app - requires multiple identifiers. + requires multiple identifiers. The GUI looks up the translation entry by + the first ID in the array, key `Apps.json` by that one. - `Description`: Brief description of the app shown in the GUI. - `SelectedByDefault`: Set to `true` only for apps that are largely considered bloatware, otherwise set to `false`. - `Recommendation`: Indicates how strongly the app is recommended for removal. One of: @@ -244,7 +245,9 @@ To add a new app that can be removed via Win11Debloat: - `Appx` — remove as a standard Appx package via `Remove-AppxPackage` (most apps) - `WinGet` — remove via WinGet (`winget uninstall`). Use for non-Appx apps such as Microsoft Copilot. -3. **Follow the Guidelines**: +3. **Add the English Translation Entry**: Add a matching entry for your `AppId` to `Config/Languages/en-US/Apps.json` (see [Localizing UI Text](#localizing-ui-text)) with `FriendlyName` and `Description` matching what you set above, so the GUI shows them instead of the raw `AppId`. + +4. **Follow the Guidelines**: - Use clear, user-friendly names for `FriendlyName` - Provide a concise description explaining what the app does @@ -451,11 +454,12 @@ Add a matching entry for your `GroupId` to `Config/Languages/en-US/Features.json ### Localizing UI Text -GUI text lives in `Config/Languages/en-US/`, not hardcoded in `Schemas/*.xaml` or `Scripts/GUI/*.ps1`. Adding or changing a Feature, Category, or UI Group means updating the matching English entry, or the GUI falls back to showing the raw ID. +GUI text lives in `Config/Languages/en-US/`, not hardcoded in `Schemas/*.xaml` or `Scripts/GUI/*.ps1`. Adding or changing a Feature, Category, UI Group, or App means updating the matching English entry, or the GUI falls back to showing the raw ID. - `Config/Languages/en-US/Features.json`, under `"Features"`, keyed by `FeatureId`. Add `Label`, `ToolTip`, `ApplyText`, `UndoLabel`, and `ApplyUndoText` to match whatever fields you set in `Config/Features.json`. - `Config/Languages/en-US/Features.json`, under `"UiGroups"`, keyed by `GroupId`. Add `Label`, `ToolTip`, and a `Values` map of `FeatureId: "Option label"` matching the group's `Values` array. - `Config/Languages/en-US/Categories.json`, keyed by `CategoryId`. Add a `Label` for every new category. +- `Config/Languages/en-US/Apps.json`, keyed by `AppId`. Add `FriendlyName` and `Description` for every new app. - `Config/Languages/en-US/Chrome.json`, a flat key/value map for static GUI chrome: buttons, dialog titles, tooltips not tied to a specific Feature/Category/UiGroup. Add a key here only for new static text, and reference it from XAML with a `%LANG:YourKey%` marker or from PowerShell with `Get-Translation -Key 'YourKey'`. Check for an existing `Chrome.json` key with the same English text before adding a new one. Two keys holding identical strings is duplication waiting to drift. @@ -468,7 +472,7 @@ A new language is its own pull request, not something bundled with unrelated cha 1. **Create the language folder**: copy `Config/Languages/en-US/` to `Config/Languages//`, using the .NET culture code the GUI should match (`nl-NL`, `es-ES`). This is also the folder matched against the user's Windows display language: exact match first, then a language-only prefix match (`nl-BE` falls back to `nl-NL` if that's the only Dutch folder present), then `en-US` if nothing matches. -2. **Translate the three files** (`Chrome.json`, `Features.json`, `Categories.json`), keeping every key name exactly as it is in `en-US`. Only the values change. Leave a key untranslated and the GUI shows the `en-US` text for it instead of a blank or a crash, so a translation can land incrementally. +2. **Translate the four files** (`Chrome.json`, `Features.json`, `Categories.json`, `Apps.json`), keeping every key name exactly as it is in `en-US`. Only the values change. Leave a key untranslated and the GUI shows the `en-US` text for it instead of a blank or a crash, so a translation can land incrementally. 3. **Add the language's plural rule**. Strings using the `_one`/`_other` suffix convention (see `AppsSelectedCount_one` in `en-US/Chrome.json`) need a matching `case` in `Get-PluralCategory` (`Scripts/FileIO/Import-LanguageFile.ps1`), keyed by language prefix (`'nl'`). Several languages share English's rule, singular at exactly 1, plural otherwise, and can reuse it as-is. Others need more categories: CLDR defines `zero`/`one`/`two`/`few`/`many`/`other`. Match your key suffixes to whatever categories your language's rule actually uses. diff --git a/Config/Languages/en-US/Apps.json b/Config/Languages/en-US/Apps.json new file mode 100644 index 0000000..425f7ae --- /dev/null +++ b/Config/Languages/en-US/Apps.json @@ -0,0 +1,566 @@ +{ + "Clipchamp.Clipchamp": { + "FriendlyName": "Clipchamp", + "Description": "Video editor from Microsoft" + }, + "Microsoft.3DBuilder": { + "FriendlyName": "3D Builder", + "Description": "Basic 3D modeling software" + }, + "Microsoft.549981C3F5F10": { + "FriendlyName": "Cortana", + "Description": "Microsoft Cortana voice assistant (Discontinued)" + }, + "Microsoft.BingFinance": { + "FriendlyName": "Bing Finance", + "Description": "Finance news and tracking via Bing (Discontinued)" + }, + "Microsoft.BingFoodAndDrink": { + "FriendlyName": "Bing Food And Drink", + "Description": "Recipes and food news via Bing (Discontinued)" + }, + "Microsoft.BingHealthAndFitness": { + "FriendlyName": "Bing Health And Fitness", + "Description": "Health and fitness tracking/news via Bing (Discontinued)" + }, + "Microsoft.BingNews": { + "FriendlyName": "Bing News", + "Description": "News aggregator via Bing (Replaced by Microsoft News/Start)" + }, + "Microsoft.BingSports": { + "FriendlyName": "Bing Sports", + "Description": "Sports news and scores via Bing (Discontinued)" + }, + "Microsoft.BingTranslator": { + "FriendlyName": "Bing Translator", + "Description": "Translation service via Bing" + }, + "Microsoft.BingTravel": { + "FriendlyName": "Bing Travel", + "Description": "Travel planning and news via Bing (Discontinued)" + }, + "Microsoft.BingWeather": { + "FriendlyName": "Bing Weather", + "Description": "Weather forecast via Bing" + }, + "XP9CXNGPPJ97XX": { + "FriendlyName": "Microsoft Copilot", + "Description": "AI assistant integrated into Windows" + }, + "Microsoft.Windows.AIHub": { + "FriendlyName": "Copilot+ AI Hub", + "Description": "Copilot+ AI Hub app (Windows 11 24H2+)" + }, + "Microsoft.PCManager": { + "FriendlyName": "Microsoft PC Manager", + "Description": "Microsoft PC Manager system cleanup and optimization tool (often preinstalled)" + }, + "Microsoft.Getstarted": { + "FriendlyName": "Get Started", + "Description": "Tips and introductory guide for Windows (Cannot be uninstalled in Windows 11)" + }, + "Microsoft.Messaging": { + "FriendlyName": "Messaging", + "Description": "Messaging app, often integrates with Skype (Largely discontinued)" + }, + "Microsoft.Microsoft3DViewer": { + "FriendlyName": "3D Viewer", + "Description": "Viewer for 3D models" + }, + "Microsoft.MicrosoftJournal": { + "FriendlyName": "Microsoft Journal", + "Description": "Digital note-taking app optimized for pen input" + }, + "Microsoft.MicrosoftOfficeHub": { + "FriendlyName": "Office Hub", + "Description": "Hub to access Microsoft Office apps and documents (Precursor to Microsoft 365 app)" + }, + "Microsoft.MicrosoftPowerBIForWindows": { + "FriendlyName": "Power BI", + "Description": "Business analytics service client" + }, + "Microsoft.MicrosoftSolitaireCollection": { + "FriendlyName": "Solitaire Collection", + "Description": "Collection of solitaire card games" + }, + "Microsoft.MicrosoftStickyNotes": { + "FriendlyName": "Sticky Notes", + "Description": "Digital sticky notes app (Discontinued & replaced by OneNote)" + }, + "Microsoft.MixedReality.Portal": { + "FriendlyName": "Mixed Reality Portal", + "Description": "Portal for Windows Mixed Reality headsets" + }, + "Microsoft.NetworkSpeedTest": { + "FriendlyName": "Network Speed Test", + "Description": "Internet connection speed test utility" + }, + "Microsoft.News": { + "FriendlyName": "Microsoft News", + "Description": "News aggregator (Replaced Bing News, now part of Microsoft Start)" + }, + "Microsoft.Office.OneNote": { + "FriendlyName": "OneNote", + "Description": "Digital note-taking app (Universal Windows Platform version)" + }, + "Microsoft.Office.Sway": { + "FriendlyName": "Sway", + "Description": "Presentation and storytelling app" + }, + "Microsoft.OneConnect": { + "FriendlyName": "One Connect", + "Description": "Mobile Operator management app (Replaced by Mobile Plans)" + }, + "Microsoft.Print3D": { + "FriendlyName": "Print 3D", + "Description": "3D printing preparation software" + }, + "Microsoft.PowerAutomateDesktop": { + "FriendlyName": "Power Automate", + "Description": "Desktop automation tool (RPA)" + }, + "Microsoft.SkypeApp": { + "FriendlyName": "Skype (UWP)", + "Description": "Skype communication app, Universal Windows Platform version (Discontinued)" + }, + "Microsoft.Todos": { + "FriendlyName": "Microsoft To Do", + "Description": "To-do list and task management app" + }, + "Microsoft.Windows.DevHome": { + "FriendlyName": "Dev Home", + "Description": "Developer dashboard and tool configuration utility (Discontinued)" + }, + "Microsoft.WindowsAlarms": { + "FriendlyName": "Alarms & Clock", + "Description": "Alarms & Clock app" + }, + "Microsoft.WindowsFeedbackHub": { + "FriendlyName": "Feedback Hub", + "Description": "App for providing feedback to Microsoft on Windows" + }, + "Microsoft.WindowsMaps": { + "FriendlyName": "Windows Maps", + "Description": "Mapping and navigation app" + }, + "Microsoft.WindowsSoundRecorder": { + "FriendlyName": "Sound Recorder", + "Description": "Basic audio recording app" + }, + "Microsoft.XboxApp": { + "FriendlyName": "Xbox Console Companion", + "Description": "Old Xbox Console Companion App (Discontinued)" + }, + "Microsoft.ZuneVideo": { + "FriendlyName": "Movies & TV", + "Description": "Movies & TV app for renting/buying/playing video content (Rebranded as \"Films & TV\")" + }, + "MicrosoftCorporationII.MicrosoftFamily": { + "FriendlyName": "Family Safety", + "Description": "Family Safety App for managing family accounts and settings" + }, + "MicrosoftCorporationII.QuickAssist": { + "FriendlyName": "Quick Assist", + "Description": "Remote assistance tool" + }, + "MicrosoftTeams": { + "FriendlyName": "Microsoft Teams (Old)", + "Description": "Old Microsoft Teams personal (MS Store version)" + }, + "MSTeams": { + "FriendlyName": "Microsoft Teams (New)", + "Description": "New Microsoft Teams app (Work/School or Personal)" + }, + "ACGMediaPlayer": { + "FriendlyName": "ACG Media Player", + "Description": "Media player app" + }, + "ActiproSoftwareLLC": { + "FriendlyName": "Actipro Software", + "Description": "Potentially UI controls or software components, often bundled by OEMs" + }, + "AdobeSystemsIncorporated.AdobePhotoshopExpress": { + "FriendlyName": "Adobe Photoshop Express", + "Description": "Basic photo editing app from Adobe" + }, + "Amazon.com.Amazon": { + "FriendlyName": "Amazon", + "Description": "Amazon shopping app" + }, + "AmazonVideo.PrimeVideo": { + "FriendlyName": "Prime Video", + "Description": "Amazon Prime Video streaming service app" + }, + "Asphalt8Airborne": { + "FriendlyName": "Asphalt 8", + "Description": "Racing game" + }, + "AutodeskSketchBook": { + "FriendlyName": "Autodesk SketchBook", + "Description": "Digital drawing and sketching app" + }, + "CaesarsSlotsFreeCasino": { + "FriendlyName": "Caesars Slots", + "Description": "Casino slot machine game" + }, + "COOKINGFEVER": { + "FriendlyName": "Cooking Fever", + "Description": "Restaurant simulation game" + }, + "CyberLinkMediaSuiteEssentials": { + "FriendlyName": "CyberLink Media Suite", + "Description": "Multimedia software suite (often preinstalled by OEMs)" + }, + "DisneyMagicKingdoms": { + "FriendlyName": "Disney Magic Kingdoms", + "Description": "Disney theme park building game" + }, + "Disney.37853FC22B2CE": { + "FriendlyName": "Disney+", + "Description": "Disney+ streaming service app" + }, + "DrawboardPDF": { + "FriendlyName": "Drawboard PDF", + "Description": "PDF viewing and annotation app, often focused on pen input" + }, + "Duolingo-LearnLanguagesforFree": { + "FriendlyName": "Duolingo", + "Description": "Language learning app" + }, + "EclipseManager": { + "FriendlyName": "Eclipse Manager", + "Description": "Often related to specific OEM software or utilities (e.g. for managing screen settings)" + }, + "FACEBOOK.FACEBOOK": { + "FriendlyName": "Facebook", + "Description": "Facebook social media app" + }, + "FarmVille2CountryEscape": { + "FriendlyName": "FarmVille 2", + "Description": "Farming simulation game" + }, + "Flipboard": { + "FriendlyName": "Flipboard", + "Description": "News and social network aggregator styled as a magazine" + }, + "HiddenCity": { + "FriendlyName": "Hidden City", + "Description": "Hidden object puzzle adventure game" + }, + "HULULLC.HULUPLUS": { + "FriendlyName": "Hulu", + "Description": "Hulu streaming service app" + }, + "iHeartRadio": { + "FriendlyName": "iHeartRadio", + "Description": "Internet radio streaming app" + }, + "Facebook.Instagram": { + "FriendlyName": "Instagram", + "Description": "Instagram social media app" + }, + "king.com.BubbleWitch3Saga": { + "FriendlyName": "Bubble Witch 3", + "Description": "Puzzle game from King" + }, + "king.com.CandyCrushSaga": { + "FriendlyName": "Candy Crush Saga", + "Description": "Puzzle game from King" + }, + "king.com.CandyCrushSodaSaga": { + "FriendlyName": "Candy Crush Soda", + "Description": "Puzzle game from King" + }, + "LinkedInforWindows": { + "FriendlyName": "LinkedIn", + "Description": "LinkedIn professional networking app" + }, + "MarchofEmpires": { + "FriendlyName": "March of Empires", + "Description": "Strategy game" + }, + "4DF9E0F8.Netflix": { + "FriendlyName": "Netflix", + "Description": "Netflix streaming service app" + }, + "NYTCrossword": { + "FriendlyName": "NYT Crossword", + "Description": "New York Times crossword puzzle app" + }, + "OneCalendar": { + "FriendlyName": "One Calendar", + "Description": "Calendar aggregation app" + }, + "PandoraMediaInc": { + "FriendlyName": "Pandora", + "Description": "Pandora music streaming app" + }, + "PhototasticCollage": { + "FriendlyName": "Phototastic Collage", + "Description": "Photo collage creation app" + }, + "PicsArt-PhotoStudio": { + "FriendlyName": "PicsArt", + "Description": "Photo editing and creative app" + }, + "PolarrPhotoEditorAcademicEdition": { + "FriendlyName": "Polarr Photo Editor", + "Description": "Photo editing app (Academic Edition)" + }, + "flaregamesGmbH.RoyalRevolt": { + "FriendlyName": "Royal Revolt", + "Description": "Tower defense / strategy game" + }, + "Sidia.LiveWallpaper": { + "FriendlyName": "Live Wallpaper", + "Description": "Live wallpaper app" + }, + "SlingTV": { + "FriendlyName": "Sling TV", + "Description": "Live TV streaming service app" + }, + "SpotifyAB.SpotifyMusic": { + "FriendlyName": "Spotify", + "Description": "Spotify music streaming app" + }, + "BytedancePte.Ltd.TikTok": { + "FriendlyName": "TikTok", + "Description": "TikTok short-form video app" + }, + "TuneInRadio": { + "FriendlyName": "TuneIn Radio", + "Description": "Internet radio streaming app" + }, + "WinZipUniversal": { + "FriendlyName": "WinZip", + "Description": "File compression and extraction utility (Universal Windows Platform version)" + }, + "Microsoft.BingSearch": { + "FriendlyName": "Bing Search", + "Description": "Web Search from Microsoft Bing (Integrates into Windows Search)" + }, + "Microsoft.Edge": { + "FriendlyName": "Microsoft Edge", + "Description": "Windows' default browser, WARNING: Removing this app also removes the only browser from Windows Sandbox and could affect other apps" + }, + "Microsoft.GamingApp": { + "FriendlyName": "Xbox Gaming App", + "Description": "Modern Xbox Gaming App, required for installing some PC games" + }, + "Microsoft.GetHelp": { + "FriendlyName": "Get Help", + "Description": "Required for some Windows 11 Troubleshooters and support interactions" + }, + "Microsoft.M365Companions": { + "FriendlyName": "Microsoft 365 Companions", + "Description": "Microsoft 365 (Business) Calendar, Files and People mini-apps, these apps may be reinstalled if enabled by your Microsoft 365 admin" + }, + "Microsoft.MSPaint": { + "FriendlyName": "Paint 3D", + "Description": "Paint 3D (Modern paint application with 3D features)" + }, + "Microsoft.OneDrive": { + "FriendlyName": "OneDrive", + "Description": "OneDrive consumer cloud storage client" + }, + "Microsoft.OutlookForWindows": { + "FriendlyName": "Outlook for Windows", + "Description": "New Outlook for Windows mail client" + }, + "Microsoft.Paint": { + "FriendlyName": "Paint", + "Description": "Classic Paint (Traditional 2D paint application)" + }, + "Microsoft.People": { + "FriendlyName": "People", + "Description": "Required for & included with Mail & Calendar (discontinued)" + }, + "Microsoft.RemoteDesktop": { + "FriendlyName": "Remote Desktop", + "Description": "Remote Desktop client app" + }, + "Microsoft.ScreenSketch": { + "FriendlyName": "Snipping Tool", + "Description": "Snipping Tool (Screenshot and annotation tool)" + }, + "Microsoft.StartExperiencesApp": { + "FriendlyName": "Widgets Experience", + "Description": "This app powers Windows Widgets My Feed" + }, + "Microsoft.Whiteboard": { + "FriendlyName": "Whiteboard", + "Description": "Digital collaborative whiteboard app" + }, + "Microsoft.Windows.Photos": { + "FriendlyName": "Photos", + "Description": "Default photo viewing and basic editing app" + }, + "Microsoft.WindowsCalculator": { + "FriendlyName": "Calculator", + "Description": "Calculator app" + }, + "Microsoft.WindowsCamera": { + "FriendlyName": "Camera", + "Description": "Camera app for using built-in or connected cameras" + }, + "Microsoft.windowscommunicationsapps": { + "FriendlyName": "Mail & Calendar", + "Description": "Mail & Calendar app suite (Discontinued)" + }, + "Microsoft.WindowsNotepad": { + "FriendlyName": "Notepad", + "Description": "Notepad text editor app" + }, + "Microsoft.WindowsStore": { + "FriendlyName": "Microsoft Store", + "Description": "Microsoft Store, WARNING: This app cannot be reinstalled easily if removed!" + }, + "Microsoft.WindowsTerminal": { + "FriendlyName": "Windows Terminal", + "Description": "Default terminal app in windows 11 (Command Prompt, PowerShell, WSL), WARNING: Do not remove if you launched Win11Debloat from Windows Terminal, as this will cause the script to fail." + }, + "Microsoft.Xbox.TCUI": { + "FriendlyName": "Xbox TCUI Framework", + "Description": "UI framework, seems to be required for Microsoft Store, photos and certain games" + }, + "Microsoft.XboxGameOverlay": { + "FriendlyName": "Xbox Game Overlay", + "Description": "Game overlay, required/useful for some games (Part of Xbox Game Bar)" + }, + "Microsoft.XboxGamingOverlay": { + "FriendlyName": "Xbox Gaming Overlay", + "Description": "Game overlay, required/useful for some games (Part of Xbox Game Bar)" + }, + "Microsoft.XboxIdentityProvider": { + "FriendlyName": "Xbox Identity Provider", + "Description": "Xbox sign-in framework, required for some games and Xbox services" + }, + "Microsoft.XboxSpeechToTextOverlay": { + "FriendlyName": "Xbox Speech To Text", + "Description": "Accessibility feature required for some games, WARNING: This app cannot be reinstalled easily!" + }, + "Microsoft.YourPhone": { + "FriendlyName": "Phone Link", + "Description": "Phone link (Connects Android/iOS phone to PC)" + }, + "Microsoft.ZuneMusic": { + "FriendlyName": "Media Player", + "Description": "Modern Media Player (Replaced Groove Music, plays local audio/video)" + }, + "MicrosoftWindows.CrossDevice": { + "FriendlyName": "Cross Device Experience", + "Description": "Phone integration within File Explorer, Camera and more (Part of Phone Link features)" + }, + "MicrosoftWindows.Client.WebExperience": { + "FriendlyName": "Windows Web Experience Pack", + "Description": "Helps deliver and update certain features, like Widgets, through the Microsoft Store" + }, + "Microsoft.WidgetsPlatformRuntime": { + "FriendlyName": "Widgets Platform Runtime", + "Description": "Runtime required for Windows Widgets to function" + }, + "LGElectronics.LGMonitorApp": { + "FriendlyName": "LG Monitor App", + "Description": "LG OEM software for controlling your LG monitor, Windows Update may silently reinstall this app if you have an LG monitor connected" + }, + "AD2F1837.HPAIExperienceCenter": { + "FriendlyName": "HP AI Experience Center", + "Description": "HP OEM software, AI-enhanced features and support" + }, + "AD2F1837.HPConnectedMusic": { + "FriendlyName": "HP Connected Music", + "Description": "HP OEM software for music (Potentially discontinued)" + }, + "AD2F1837.HPConnectedPhotopoweredbySnapfish": { + "FriendlyName": "HP Connected Photo", + "Description": "HP OEM software for photos, integrated with Snapfish (Potentially discontinued)" + }, + "AD2F1837.HPDesktopSupportUtilities": { + "FriendlyName": "HP Desktop Support Utilities", + "Description": "HP OEM software providing desktop support tools" + }, + "AD2F1837.HPEasyClean": { + "FriendlyName": "HP Easy Clean", + "Description": "HP OEM software for system cleaning or optimization" + }, + "AD2F1837.HPFileViewer": { + "FriendlyName": "HP File Viewer", + "Description": "HP OEM software for viewing specific file types" + }, + "AD2F1837.HPJumpStarts": { + "FriendlyName": "HP JumpStarts", + "Description": "HP OEM software for tutorials, app discovery, or quick access to HP features" + }, + "AD2F1837.HPPCHardwareDiagnosticsWindows": { + "FriendlyName": "HP PC Hardware Diagnostics", + "Description": "HP OEM software for PC hardware diagnostics" + }, + "AD2F1837.HPPowerManager": { + "FriendlyName": "HP Power Manager", + "Description": "HP OEM software for managing power settings and battery" + }, + "AD2F1837.HPPrinterControl": { + "FriendlyName": "HP Printer Control", + "Description": "HP OEM software for managing HP printers" + }, + "AD2F1837.HPPrivacySettings": { + "FriendlyName": "HP Privacy Settings", + "Description": "HP OEM software for managing privacy settings" + }, + "AD2F1837.HPQuickDrop": { + "FriendlyName": "HP QuickDrop", + "Description": "HP OEM software for quick file transfer between devices" + }, + "AD2F1837.HPQuickTouch": { + "FriendlyName": "HP QuickTouch", + "Description": "HP OEM software, possibly for touch-specific shortcuts or controls" + }, + "AD2F1837.HPRegistration": { + "FriendlyName": "HP Registration", + "Description": "HP OEM software for product registration" + }, + "AD2F1837.HPSupportAssistant": { + "FriendlyName": "HP Support Assistant", + "Description": "HP OEM software for support, updates, and troubleshooting" + }, + "AD2F1837.HPSureShieldAI": { + "FriendlyName": "HP Sure Shield AI", + "Description": "HP OEM security software, likely AI-based threat protection" + }, + "AD2F1837.HPSystemInformation": { + "FriendlyName": "HP System Information", + "Description": "HP OEM software for displaying system information" + }, + "AD2F1837.HPWelcome": { + "FriendlyName": "HP Welcome", + "Description": "HP OEM software providing a welcome experience or initial setup help" + }, + "AD2F1837.HPWorkWell": { + "FriendlyName": "HP WorkWell", + "Description": "HP OEM software focused on well-being, possibly with break reminders or ergonomic tips" + }, + "AD2F1837.myHP": { + "FriendlyName": "myHP", + "Description": "HP OEM central hub app for device info, support, and services" + }, + "E046963F.LenovoCompanion": { + "FriendlyName": "Lenovo Vantage", + "Description": "Lenovo OEM hub app for device settings, updates, and support" + }, + "LenovoCompanyLimited.LenovoVantageService": { + "FriendlyName": "Lenovo Vantage Service", + "Description": "Background service component for Lenovo Vantage" + }, + "DellInc.DellSupportAssistforPCs": { + "FriendlyName": "Dell SupportAssist", + "Description": "Dell OEM support, diagnostic, and update tool" + }, + "DellInc.DellDigitalDelivery": { + "FriendlyName": "Dell Digital Delivery Services", + "Description": "Dell OEM software for delivering pre-purchased software" + }, + "DellInc.DellMobileConnect": { + "FriendlyName": "Dell Mobile Connect", + "Description": "Dell OEM app for linking Android/iOS phone to PC (superseded by Phone Link)" + } +} diff --git a/Scripts/FileIO/Import-AppDetailsFromJson.ps1 b/Scripts/FileIO/Import-AppDetailsFromJson.ps1 index 6218554..c7fad69 100644 --- a/Scripts/FileIO/Import-AppDetailsFromJson.ps1 +++ b/Scripts/FileIO/Import-AppDetailsFromJson.ps1 @@ -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 +} diff --git a/Scripts/FileIO/Import-LanguageFile.ps1 b/Scripts/FileIO/Import-LanguageFile.ps1 index cf17f33..092d733 100644 --- a/Scripts/FileIO/Import-LanguageFile.ps1 +++ b/Scripts/FileIO/Import-LanguageFile.ps1 @@ -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 } diff --git a/Scripts/GUI/MainWindow-AppSelection.ps1 b/Scripts/GUI/MainWindow-AppSelection.ps1 index de90123..08909f9 100644 --- a/Scripts/GUI/MainWindow-AppSelection.ps1 +++ b/Scripts/GUI/MainWindow-AppSelection.ps1 @@ -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) diff --git a/Tests/Import-LanguageFile.Tests.ps1 b/Tests/Import-LanguageFile.Tests.ps1 index 3e6cd20..c31291c 100644 --- a/Tests/Import-LanguageFile.Tests.ps1 +++ b/Tests/Import-LanguageFile.Tests.ps1 @@ -16,13 +16,14 @@ Describe 'Resolve-LanguageFolder' { } Describe 'Import-LanguageFile' { - It 'loads all three JSON files for the resolved language' { + It 'loads all four JSON files for the resolved language' { $lang = Import-LanguageFile -LanguageCode 'en-US' $lang.LanguageCode | Should -Be 'en-US' $lang.Chrome.TitleBarClose | Should -Be 'Close' $lang.Features.DisableTelemetry.Label | Should -Be 'Disable telemetry' $lang.Categories.PrivacySuggestedContent.Label | Should -Be 'Privacy & Suggested Content' + $lang.Apps.'TestApp.One'.FriendlyName | Should -Be 'Test App' } It 'attaches an en-US Fallback when a non-en-US language is loaded' { @@ -124,6 +125,11 @@ Describe 'Get-Translation' { Get-Translation -Key 'PrivacySuggestedContent' -Field 'Label' -Lang $script:EnLang | Should -Be 'Privacy & Suggested Content' } + It 'looks up a Field on an Apps entry' { + Get-Translation -Key 'TestApp.One' -Field 'FriendlyName' -Section 'Apps' -Lang $script:EnLang | Should -Be 'Test App' + Get-Translation -Key 'TestApp.One' -Field 'Description' -Section 'Apps' -Lang $script:EnLang | Should -Be 'A test app.' + } + It 'looks up a Field on a UiGroups entry' { Get-Translation -Key 'SearchIcon' -Field 'Label' -Lang $script:EnLang | Should -Be 'Taskbar search style' } @@ -199,6 +205,8 @@ Describe 'Test-LanguageKeyCoverage' { $coverage.MissingKeys | Should -Contain 'Features.DisableTelemetry.ToolTip' $coverage.MissingKeys | Should -Contain 'UiGroups.SearchIcon.Label' $coverage.MissingKeys | Should -Contain 'Categories.PrivacySuggestedContent.Label' + $coverage.MissingKeys | Should -Contain 'Apps.TestApp.One.FriendlyName' + $coverage.MissingKeys | Should -Contain 'Apps.TestApp.One.Description' $coverage.ExtraKeys | Should -BeNullOrEmpty } diff --git a/Tests/MainWindow-AppSelection.Tests.ps1 b/Tests/MainWindow-AppSelection.Tests.ps1 index fbe01b8..53ee373 100644 --- a/Tests/MainWindow-AppSelection.Tests.ps1 +++ b/Tests/MainWindow-AppSelection.Tests.ps1 @@ -1,7 +1,10 @@ BeforeAll { Add-Type -AssemblyName PresentationFramework + function Invoke-DoEvents {} . (Join-Path $PSScriptRoot '..\Scripts\FileIO\Import-JsonFile.ps1') . (Join-Path $PSScriptRoot '..\Scripts\FileIO\Import-LanguageFile.ps1') + . (Join-Path $PSScriptRoot '..\Scripts\FileIO\Import-AppDetailsFromJson.ps1') + . (Join-Path $PSScriptRoot '..\Scripts\Threading\Invoke-NonBlocking.ps1') . (Join-Path $PSScriptRoot '..\Scripts\GUI\MainWindow-AppSelection.ps1') . (Join-Path $PSScriptRoot '..\Scripts\GUI\MainWindow-Deployment.ps1') . (Join-Path $PSScriptRoot '..\Scripts\GUI\MainWindow-Navigation.ps1') @@ -9,6 +12,10 @@ BeforeAll { . (Join-Path $PSScriptRoot '..\Scripts\GUI\Set-WindowThemeResources.ps1') $script:LanguagesPath = Join-Path $PSScriptRoot '..\Config\Languages' $script:Lang = Import-LanguageFile -LanguageCode 'en-US' + $script:LoadAppsDetailsScriptPath = Join-Path $PSScriptRoot '..\Scripts\FileIO\Import-AppDetailsFromJson.ps1' + $script:TestAppInWingetListScriptPath = Join-Path $PSScriptRoot '..\Scripts\AppRemoval\Test-AppInWingetList.ps1' + $script:ImportLanguageFileScriptPath = Join-Path $PSScriptRoot '..\Scripts\FileIO\Import-LanguageFile.ps1' + $script:AppsListFilePath = Join-Path $PSScriptRoot '..\Config\Apps.json' function New-TestWindow { $window = New-Object System.Windows.Window @@ -19,6 +26,25 @@ BeforeAll { } } +Describe 'Invoke-AppDetailsFromJsonAsync' { + BeforeEach { $script:GuiWindow = [PSCustomObject]@{} } + + It 'resolves translated FriendlyName and Description inside the background runspace' { + $result = @(Invoke-AppDetailsFromJsonAsync -OnlyInstalled:$false) + + $result.Count | Should -Be 141 + $clipchamp = $result | Where-Object { $_.AppId -contains 'Clipchamp.Clipchamp' } + $clipchamp.FriendlyName | Should -Be 'Clipchamp' + $clipchamp.Description | Should -Be 'Video editor from Microsoft' + } + + It 'does not return null entries when OnlyInstalled filters the runspace results' { + $result = @(Invoke-AppDetailsFromJsonAsync -OnlyInstalled:$true -InstalledList @()) + + @($result | Where-Object { -not $_ }).Count | Should -Be 0 + } +} + Describe 'ConvertTo-NormalizedCheckboxState' { It 'normalizes indeterminate tri-state checkboxes to checked' { $checkBox = New-Object System.Windows.Controls.CheckBox diff --git a/Tests/Settings-And-Apps-FileIO.Tests.ps1 b/Tests/Settings-And-Apps-FileIO.Tests.ps1 index d89765d..54ba1e9 100644 --- a/Tests/Settings-And-Apps-FileIO.Tests.ps1 +++ b/Tests/Settings-And-Apps-FileIO.Tests.ps1 @@ -3,6 +3,7 @@ BeforeAll { function Test-AppInWingetList { param($appId, $InstalledList) $false } . (Join-Path $PSScriptRoot '..\Scripts\FileIO\Import-JsonFile.ps1') + . (Join-Path $PSScriptRoot '..\Scripts\FileIO\Import-LanguageFile.ps1') . (Join-Path $PSScriptRoot '..\Scripts\Helpers\Add-Parameter.ps1') . (Join-Path $PSScriptRoot '..\Scripts\FileIO\Save-ToFile.ps1') . (Join-Path $PSScriptRoot '..\Scripts\FileIO\Import-Settings.ps1') @@ -12,6 +13,8 @@ BeforeAll { . (Join-Path $PSScriptRoot '..\Scripts\FileIO\Import-AppPresetsFromJson.ps1') . (Join-Path $PSScriptRoot '..\Scripts\FileIO\Get-ValidatedAppList.ps1') $script:JsonFixturePath = Join-Path $PSScriptRoot 'TestData\JsonFileLoading' + $script:LanguagesPath = Join-Path $PSScriptRoot '..\Config\Languages' + $script:Lang = Import-LanguageFile -LanguageCode 'en-US' } Describe 'Save-ToFile' { @@ -150,7 +153,8 @@ Describe 'Import-AppDetailsFromJson' { $apps = @(Import-AppDetailsFromJson -InitialCheckedFromJson) $apps | Should -HaveCount 2 - $apps[0].DisplayName | Should -Be 'One (One.App, Alias.App)' + # 'One.App' has no en-US Apps.json entry, so Get-Translation falls back to the raw key. + $apps[0].DisplayName | Should -Be 'One.App (One.App, Alias.App)' $apps[0].IsChecked | Should -BeTrue $apps[0].RemovalMethod | Should -Be 'WinGet' $apps[1].FriendlyName | Should -Be 'Two.App' @@ -159,6 +163,18 @@ Describe 'Import-AppDetailsFromJson' { @($apps[1].AppId) | Should -HaveCount 1 } + It 'resolves FriendlyName and Description from Config/Languages/en-US/Apps.json for a real AppId' { + '{"Apps":[{"AppId":"Clipchamp.Clipchamp","FriendlyName":"stale JSON name, should not appear","Description":"stale JSON description, should not appear"}]}' | + Set-Content -LiteralPath $script:AppsListFilePath -Encoding UTF8 + + $apps = @(Import-AppDetailsFromJson) + + $apps | Should -HaveCount 1 + $apps[0].FriendlyName | Should -Be 'Clipchamp' + $apps[0].Description | Should -Be 'Video editor from Microsoft' + $apps[0].DisplayName | Should -Be 'Clipchamp (Clipchamp.Clipchamp)' + } + It 'skips missing, blank, and non-string app IDs without failing the complete catalog' { '{"Apps":[{"AppId":null},{"FriendlyName":"Missing"},{"AppId":42},{"AppId":[" Valid.App ",null,{},""]}]}' | Set-Content -LiteralPath $script:AppsListFilePath -Encoding UTF8 diff --git a/Tests/TestData/LanguageLoading/en-US/Apps.json b/Tests/TestData/LanguageLoading/en-US/Apps.json new file mode 100644 index 0000000..7b1101e --- /dev/null +++ b/Tests/TestData/LanguageLoading/en-US/Apps.json @@ -0,0 +1,6 @@ +{ + "TestApp.One": { + "FriendlyName": "Test App", + "Description": "A test app." + } +} diff --git a/Tests/TestData/LanguageLoading/es-ES/Apps.json b/Tests/TestData/LanguageLoading/es-ES/Apps.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/Tests/TestData/LanguageLoading/es-ES/Apps.json @@ -0,0 +1 @@ +{} diff --git a/Win11Debloat.ps1 b/Win11Debloat.ps1 index 04b459c..7b11da8 100644 --- a/Win11Debloat.ps1 +++ b/Win11Debloat.ps1 @@ -200,6 +200,7 @@ $script:ImportExportConfigSchema = Join-Path $schemasPath 'ImportExportConfigWin $script:RestoreBackupWindowSchema = Join-Path $schemasPath 'RestoreBackupWindow.xaml' $script:LoadAppsDetailsScriptPath = Join-Path (Join-Path $scriptsPath 'FileIO') 'Import-AppDetailsFromJson.ps1' $script:TestAppInWingetListScriptPath = Join-Path (Join-Path $scriptsPath 'AppRemoval') 'Test-AppInWingetList.ps1' +$script:ImportLanguageFileScriptPath = Join-Path (Join-Path $scriptsPath 'FileIO') 'Import-LanguageFile.ps1' $script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Language', 'Silent', 'Sysprep', 'User', 'SkipExplorerRestart', 'SkipRegistryBackup', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'Config', 'CLI', 'AppRemovalTarget'