diff --git a/README.md b/README.md index e3f13795..82169a0e 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Windows. Test your ISO in a VM before installing it.** ## More information [Usage, compatibility and build instructions](docs/usage.md) · +[Interface translations](docs/translations.md) · [Attribution and distribution notes](NOTICE.md) The original PowerShell builders remain in this repository. diff --git a/docs/translations.md b/docs/translations.md new file mode 100644 index 00000000..2237fe49 --- /dev/null +++ b/docs/translations.md @@ -0,0 +1,36 @@ +# Interface translations + +The WinUI app resolves its interface text through `Strings\\Resources.resw`. +English is the source language; every other language must contain the same keys. + +| Language | Tag | File | +| --- | --- | --- | +| English | `en-US` | `src/Tiny11Builder.App/Strings/en-US/Resources.resw` | +| Traditional Chinese | `zh-Hant` | `src/Tiny11Builder.App/Strings/zh-Hant/Resources.resw` | +| Simplified Chinese | `zh-Hans` | `src/Tiny11Builder.App/Strings/zh-Hans/Resources.resw` | + +Choose a language under **Language** in the sidebar. The choice is saved in +`%LOCALAPPDATA%\Tiny11Builder\settings.json` and applied after a restart, because +XAML resolves its text when a page is loaded. + +## Add a language + +1. Copy `Strings\en-US\Resources.resw` to `Strings\\Resources.resw`, using a + BCP-47 tag such as `ja-JP` or `de-DE`. +2. Translate every value. Keep the `name` attributes and the `{0}` placeholders + unchanged; a placeholder may move within a sentence. +3. Add the tag and its native name to `LanguageCatalog.All` in + `src/Tiny11Builder.App/LanguageCatalog.cs`. +4. Run `dotnet test tests/Tiny11Builder.Core.Tests`. `LocalizationTests` checks + that every language has exactly the English keys, no empty values, and the + same placeholders, and that every `{loc:Tr Key=…}` used by XAML exists. + +Keys are flat and use `__` between words (`Build__Result__Saved`). A key must not +be a prefix of another key: MRT treats dots as resource-map separators, so flat +keys keep a resource from also becoming a scope. + +## What stays English + +Build-engine operation names, stage status words, the raw build recipe and the +build log are technical diagnostics. They are not translated by these files; the +interactive interface, removal catalog and user-facing notices are. diff --git a/docs/usage.md b/docs/usage.md index 7676c46a..df65d21b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -341,6 +341,17 @@ as an unsupported operation. Removing a service deletes its offline registry registration; shared `svchost.exe` and other shared binaries are never blindly deleted. Test aggressive images in an isolated VM and keep known-good media. +## Interface languages + +The app ships with English, Traditional Chinese (`zh-Hant`) and Simplified +Chinese (`zh-Hans`). Choose a language under **Language** in the sidebar; the +choice is saved and applied after a restart. The interface, removal catalog and +user-facing notices are translated. Build-engine operation names, stage status +words, the raw recipe and the build log remain English technical text. + +See [translations.md](translations.md) for the resource layout and the steps to +add another language. + ## Build and run Requirements: Windows 10 2004 or newer as the host, .NET 8 SDK and a Windows SDK diff --git a/src/Tiny11Builder.App/App.xaml.cs b/src/Tiny11Builder.App/App.xaml.cs index 11af9e1c..9f8212f2 100644 --- a/src/Tiny11Builder.App/App.xaml.cs +++ b/src/Tiny11Builder.App/App.xaml.cs @@ -1,11 +1,22 @@ using Microsoft.UI.Xaml; +using Microsoft.Windows.Globalization; namespace Tiny11Builder.App; public partial class App : Application { private Window? window; - public App() => InitializeComponent(); + + public App() + { + // The override must be in place before any XAML is loaded: markup-extension text is resolved + // when a page is parsed, not when the window is shown. + var language = LanguageCatalog.Normalize(AppSettings.Load().Language); + if (language.Length > 0) ApplicationLanguages.PrimaryLanguageOverride = language; + InitializeComponent(); + ResourceProvider.Install(); + } + protected override void OnLaunched(LaunchActivatedEventArgs args) { window = new MainWindow(); diff --git a/src/Tiny11Builder.App/AppSettings.cs b/src/Tiny11Builder.App/AppSettings.cs new file mode 100644 index 00000000..4e628fc5 --- /dev/null +++ b/src/Tiny11Builder.App/AppSettings.cs @@ -0,0 +1,30 @@ +using System.Text.Json; + +namespace Tiny11Builder.App; + +// The only user setting the app owns. It lives beside the downloaded tools, never in the build +// workspace, so a recovered or discarded job cannot change the interface language. +internal sealed class AppSettings +{ + private static readonly string FilePath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Tiny11Builder", "settings.json"); + + public string Language { get; set; } = ""; + + public static AppSettings Load() + { + try { return JsonSerializer.Deserialize(File.ReadAllText(FilePath)) ?? new(); } + catch (Exception error) when (error is IOException or UnauthorizedAccessException or JsonException) { return new(); } + } + + public bool TrySave() + { + try + { + Directory.CreateDirectory(Path.GetDirectoryName(FilePath)!); + File.WriteAllText(FilePath, JsonSerializer.Serialize(this)); + return true; + } + catch (Exception error) when (error is IOException or UnauthorizedAccessException) { return false; } + } +} diff --git a/src/Tiny11Builder.App/Choices.cs b/src/Tiny11Builder.App/Choices.cs index 3dfe64ee..557da55c 100644 --- a/src/Tiny11Builder.App/Choices.cs +++ b/src/Tiny11Builder.App/Choices.cs @@ -24,9 +24,16 @@ public sealed class OptionChoice(CleanupOption option) : ObservableChoice { private bool selected = option.Balanced; public CleanupOption Option { get; } = option; - public string Title => Option.Title; - public string Description => Option.Description; - public string Category => Option.Category; + public string Title => Loc.Get($"Catalog__{Option.Id}__Title"); + public string Description => Loc.Get($"Catalog__{Option.Id}__Description"); + public string Category => Option.Category switch + { + "Bundled apps" => Loc.Get("Catalog__Category__Apps"), + "Optional features" => Loc.Get("Catalog__Category__Features"), + "Windows settings" => Loc.Get("Catalog__Category__Settings"), + "Advanced / original builder" => Loc.Get("Catalog__Category__Advanced"), + _ => Option.Category + }; public bool Selected { get => selected; set => Set(ref selected, value); } } @@ -37,10 +44,10 @@ public sealed class InventoryChoice(ImageInventoryItem item) : ObservableChoice public ImageInventoryItem Item { get; } = item; public string Title => Item.Identity; public string Detail => $"{Item.Kind} · {Item.Detail}"; - public string Risk => Item.Protected ? "Protected / potentially boot-critical" : "Application"; - public string ModeLabel => $"Action for {Title}"; + public string Risk => Loc.Get(Item.Protected ? "Components__Inventory__Protected" : "Components__Inventory__Application"); + public string ModeLabel => Loc.Format("Components__Inventory__ActionFor", Title); public bool HasModes => Item.Kind is ImageItemKind.Service or ImageItemKind.Feature; - public string[] Modes { get; } = ["Disable", "Remove"]; + public string[] Modes { get; } = [Loc.Get("Components__Inventory__Disable"), Loc.Get("Components__Inventory__Remove")]; public bool Selected { get => selected; set => Set(ref selected, value); } public int ModeIndex { get => modeIndex; set => Set(ref modeIndex, value); } public ExpertAction Action => new(Item.Kind, Item.Identity, ModeIndex == 0 ? ExpertActionMode.Disable : ExpertActionMode.Remove); @@ -52,8 +59,8 @@ public sealed class BuildStageBoard { public BuildStageChoice[] Stages { get; } = [ - new("Prepare media", "\uE8B7"), new("Export and mount", "\uE7C5"), new("Remove components", "\uE74D"), - new("Commit image", "\uE74E"), new("Compress image", "\uE8B9"), new("Create ISO", "\uE958"), new("Verify output", "\uE73E") + new(Loc.Get("Build__Board__Prepare"), "\uE8B7"), new(Loc.Get("Build__Board__Export"), "\uE7C5"), new(Loc.Get("Build__Board__Remove"), "\uE74D"), + new(Loc.Get("Build__Board__Commit"), "\uE74E"), new(Loc.Get("Build__Board__Compress"), "\uE8B9"), new(Loc.Get("Build__Board__Iso"), "\uE958"), new(Loc.Get("Build__Board__Verify"), "\uE73E") ]; // Returns the phase the event belongs to, or -1 when it is not a displayed operation. diff --git a/src/Tiny11Builder.App/LanguageCatalog.cs b/src/Tiny11Builder.App/LanguageCatalog.cs new file mode 100644 index 00000000..1172ad1c --- /dev/null +++ b/src/Tiny11Builder.App/LanguageCatalog.cs @@ -0,0 +1,39 @@ +namespace Tiny11Builder.App; + +internal sealed record AppLanguage(string Tag, string NativeName) +{ + // The system choice has no native name of its own; it is the one label that follows the UI language. + public string DisplayName => Tag.Length == 0 ? Loc.Get("Language__System") : NativeName; +} + +internal static class LanguageCatalog +{ + // Tags match the Strings\\Resources.resw folders. English is the fallback and must stay first. + public static IReadOnlyList All { get; } = + [ + new("", ""), + new("en-US", "English"), + new("zh-Hant", "繁體中文"), + new("zh-Hans", "简体中文"), + ]; + + public static string Normalize(string? tag) + { + if (string.IsNullOrWhiteSpace(tag)) return ""; + var exact = All.FirstOrDefault(language => language.Tag.Equals(tag, StringComparison.OrdinalIgnoreCase)); + if (exact is not null) return exact.Tag; + return tag.ToLowerInvariant() switch + { + "zh-tw" or "zh-hk" or "zh-mo" or "zh-hant" => "zh-Hant", + "zh-cn" or "zh-sg" or "zh-hans" => "zh-Hans", + "en" => "en-US", + _ => "" + }; + } + + public static AppLanguage For(string? tag) + { + var normalized = Normalize(tag); + return All.First(language => language.Tag == normalized); + } +} diff --git a/src/Tiny11Builder.App/Localization.cs b/src/Tiny11Builder.App/Localization.cs new file mode 100644 index 00000000..d65146c3 --- /dev/null +++ b/src/Tiny11Builder.App/Localization.cs @@ -0,0 +1,19 @@ +using System.Globalization; + +namespace Tiny11Builder.App; + +// UI text is resolved through this one seam. The app installs the Windows resource lookup at startup; +// the tests install the English resources, so the same code runs in both and no English copy is +// duplicated in C#. +internal static class Loc +{ + private static Func? lookup; + + public static void Use(Func resolver) => lookup = resolver; + + public static string Get(string key) => + lookup?.Invoke(key) is { Length: > 0 } text ? text : key; + + public static string Format(string key, params object?[] values) => + string.Format(CultureInfo.CurrentCulture, Get(key), values); +} diff --git a/src/Tiny11Builder.App/MainWindow.xaml b/src/Tiny11Builder.App/MainWindow.xaml index 05f2c765..5745a455 100644 --- a/src/Tiny11Builder.App/MainWindow.xaml +++ b/src/Tiny11Builder.App/MainWindow.xaml @@ -2,6 +2,7 @@ x:Class="Tiny11Builder.App.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:loc="using:Tiny11Builder.App" Title="Tiny11 Builder"> @@ -9,42 +10,44 @@ - + - - - - - - - + + + + + + - - - + + + - @@ -93,15 +96,15 @@ - - - + @@ -110,47 +113,47 @@ @@ -158,102 +161,102 @@ - - + - + - - - + + - - - + + - + - - - + + - - - - - - - - + + - - + - - - + + + @@ -272,26 +275,26 @@ - - + - @@ -566,31 +569,31 @@ + Visibility="Collapsed" AutomationProperties.Name="{loc:Tr Key=Common__Working}"/> - - - - + + + - - - + + + - + - - + + - + diff --git a/src/Tiny11Builder.App/MainWindow.xaml.cs b/src/Tiny11Builder.App/MainWindow.xaml.cs index 86ace557..4467c352 100644 --- a/src/Tiny11Builder.App/MainWindow.xaml.cs +++ b/src/Tiny11Builder.App/MainWindow.xaml.cs @@ -35,7 +35,7 @@ public sealed partial class MainWindow : Window private bool completed; private bool coreAcknowledged; private bool modalOpen; - private enum NoticeAction { None, OpenLogs, CopyDetails, OpenTools, OpenOfficialDownload } + private enum NoticeAction { None, OpenLogs, CopyDetails, OpenTools, OpenOfficialDownload, Restart } private string officialDownloadPage = OfficialImageDownloader.DownloadPage; // The run RunOperation is executing and the cancel prompt open for it; both are null when nothing is happening. private OperationRun? active; @@ -67,6 +67,7 @@ private enum NoticeAction { None, OpenLogs, CopyDetails, OpenTools, OpenOfficial public MainWindow() { InitializeComponent(); + Title = Loc.Get("App__Title"); ExtendsContentIntoTitleBar = true; SetTitleBar(TitleArea); AppWindow.Resize(new Windows.Graphics.SizeInt32(1200, 900)); @@ -74,11 +75,13 @@ public MainWindow() { if (!busy) return; args.Cancel = true; - ShowNotice("An operation is still running.", cancelRequested ? "It is being cancelled. Wait for it to stop before closing." : - $"Choose {CancelPrompts.ButtonLabel} and wait for the image operation to finish before closing.", InfoBarSeverity.Warning); + ShowNotice(Loc.Get("Notice__CloseDuringBuild__Title"), cancelRequested ? Loc.Get("Notice__CloseDuringBuild__Cancelling") : + Loc.Format("Notice__CloseDuringBuild__Message", Loc.Get("Cancel__ButtonLabel")), InfoBarSeverity.Warning); }; WorkspaceBox.Text = @"C:\tiny11"; - SupportPurpose.Text = ProjectSupport.Purpose; + SupportPurpose.Text = Loc.Get("Support__Purpose"); + LanguageChoice.ItemsSource = LanguageCatalog.All; + LanguageChoice.SelectedItem = LanguageCatalog.For(AppSettings.Load().Language); OutputBox.Text = Path.Combine(WorkspaceBox.Text, $"tiny11-{DateTime.Now:yyyyMMdd-HHmmss}.iso"); BuildStages.ItemsSource = board.Stages; progressTimer.Tick += (_, _) => { FlushBuildProgress(); DrainDownload(); }; @@ -87,7 +90,7 @@ public MainWindow() Root.ActualThemeChanged += (_, _) => ApplyContrast(); Activated += (_, _) => ApplyContrast(); Closed += (_, _) => progressTimer.Stop(); - LatestDownloadDescription.Text = $"Official Microsoft ISO · {HostArchitecture.ToUpperInvariant()} · {CultureInfo.CurrentUICulture.NativeName}"; + LatestDownloadDescription.Text = Loc.Format("Source__Download__AutoDescription", HostArchitecture.ToUpperInvariant(), CultureInfo.CurrentUICulture.NativeName); foreach (var choice in options) choice.PropertyChanged += (_, _) => UpdateCounts(); initialized = true; ApplyContrast(); @@ -101,11 +104,11 @@ public MainWindow() try { var recipe = JsonSerializer.Deserialize(File.ReadAllText(args[restoreIndex + 1]), JsonDefaults.Options) - ?? throw new InvalidDataException("The saved session is empty."); + ?? throw new InvalidDataException(Loc.Get("Error__SavedSessionEmpty")); RestoreRecipe(recipe); - ShowNotice("Session restored.", "Inspect the image again before continuing. No build starts automatically."); + ShowNotice(Loc.Get("Notice__SessionRestored__Title"), Loc.Get("Notice__SessionRestored__Message")); } - catch (Exception error) { ShowNotice("Session could not be restored.", ErrorText.Reason(error), InfoBarSeverity.Error); } + catch (Exception error) { ShowNotice(Loc.Get("Notice__SessionRestoreFailed__Title"), ErrorText.Reason(error), InfoBarSeverity.Error); } } UpdateCounts(); } @@ -116,6 +119,32 @@ public MainWindow() (SelectedMode == BuildMode.Custom && CustomCoreCheck.IsOn); private static string HostArchitecture => RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64" : "x64"; + // Engine stages are stable English identifiers used for phase mapping; only their display name is localized. + private static string StageName(string stage) => stage switch + { + "Prepare" => Loc.Get("Build__StageNames__Prepare"), + "Copy media" => Loc.Get("Build__StageNames__CopyMedia"), + "Export edition" => Loc.Get("Build__StageNames__ExportEdition"), + "Mount" => Loc.Get("Build__StageNames__Mount"), + "Inventory" => Loc.Get("Build__StageNames__Inventory"), + "Custom servicing" => Loc.Get("Build__StageNames__CustomServicing"), + "Remove apps" => Loc.Get("Build__StageNames__RemoveApps"), + "Optional features" => Loc.Get("Build__StageNames__OptionalFeatures"), + ".NET 3.5" => Loc.Get("Build__StageNames__NetFx"), + "Windows settings" => Loc.Get("Build__StageNames__WindowsSettings"), + "Component cleanup" => Loc.Get("Build__StageNames__ComponentCleanup"), + "Advanced payloads" => Loc.Get("Build__StageNames__AdvancedPayloads"), + "Core pruning" => Loc.Get("Build__StageNames__CorePruning"), + "Redundant data" => Loc.Get("Build__StageNames__RedundantData"), + "Commit" => Loc.Get("Build__StageNames__Commit"), + "Setup image" => Loc.Get("Build__StageNames__SetupImage"), + "Compress" => Loc.Get("Build__StageNames__Compress"), + "Create ISO" => Loc.Get("Build__StageNames__CreateIso"), + "Verify output" => Loc.Get("Build__StageNames__VerifyOutput"), + "Check saved progress" => Loc.Get("Build__StageNames__ResumeCheck"), + _ => stage + }; + private void OnContentSizeChanged(object sender, SizeChangedEventArgs args) => UpdateWelcomeLayout(); private void OnContentViewChanged(object sender, ScrollViewerViewChangedEventArgs args) @@ -202,10 +231,12 @@ private void UpdateNavigation() NextButton.IsEnabled = !busy && canAdvance; // After a build stops, the primary action says what it will do: start over, or continue the resume. var retry = step == 5 && !completed && stoppedRun is not null; - ActionLabel.Text = completed ? "Build another" : retry ? (stoppedRun == OperationKind.Resume ? "Resume again" : "Build again") : step switch - { - 0 => "Start", 1 => "Next", 2 => "Next", 3 => "Review", 4 => "Continue", _ => "Build" - }; + ActionLabel.Text = completed ? Loc.Get("Action__BuildAnother") : + retry ? Loc.Get(stoppedRun == OperationKind.Resume ? "Action__ResumeAgain" : "Action__BuildAgain") : step switch + { + 0 => Loc.Get("Action__Start"), 1 => Loc.Get("Action__Next"), 2 => Loc.Get("Action__Next"), + 3 => Loc.Get("Action__Review"), 4 => Loc.Get("Action__Continue"), _ => Loc.Get("Action__Build") + }; ActionLabel.FontSize = 32; Grid.SetColumn(NextButton, 2); NextButton.HorizontalAlignment = HorizontalAlignment.Right; @@ -215,6 +246,7 @@ private void UpdateNavigation() SupportButton.IsEnabled = !busy; ResumeButton.IsEnabled = !busy; ThemeChoice.IsEnabled = !busy && !HighContrast; + LanguageChoice.IsEnabled = !busy; // Advisory only: the checkpoint file's presence decides whether to offer Resume; the engine verifies it. ResumeRunButton.Visibility = !busy && step == 5 && !completed && stoppedRun == OperationKind.Build && stoppedHasCheckpoint ? Visibility.Visible : Visibility.Collapsed; @@ -253,7 +285,7 @@ private void OnModeCardClick(object sender, RoutedEventArgs args) if (busy || modalOpen) return; if (sender is not Button { Tag: string tag } || !Enum.TryParse(tag, out var mode) || !Enum.IsDefined(mode)) { - ShowNotice("Build mode could not be selected.", "The selected card does not identify a valid build mode.", InfoBarSeverity.Error); + ShowNotice(Loc.Get("Notice__BuildModeInvalid__Title"), Loc.Get("Notice__BuildModeInvalid__Message"), InfoBarSeverity.Error); return; } ModeChoice.SelectedIndex = (int)mode; @@ -286,17 +318,14 @@ private async void OnNext(object sender, RoutedEventArgs args) } if (step == 2 && UsesCore && !coreAcknowledged) { - coreAcknowledged = await Confirm("Core is not serviceable", - "No Windows Update\nNo Microsoft Defender\nNo recovery environment\nNo added languages or features\n\n" + - "Use only in an isolated test VM.\nThe image may not boot.", - "I agree"); - CoreConsentStatus.Text = coreAcknowledged ? "Core limitations accepted for this build." : "Core limitations have not been accepted."; + coreAcknowledged = await Confirm(Loc.Get("Dialog__Core__Title"), Loc.Get("Dialog__Core__Body"), Loc.Get("Dialog__Core__Accept")); + CoreConsentStatus.Text = Loc.Get(coreAcknowledged ? "Components__CoreConsent__Accepted" : "Components__CoreConsent__NotAccepted"); if (!coreAcknowledged) return; } await RunOperation(OperationKind.Check, async token => { - Working("Checking your choices…"); - if (step >= 1 && image is null) throw new InvalidOperationException("Inspect the selected Windows image first."); + Working(Loc.Get("Working__CheckingChoices")); + if (step >= 1 && image is null) throw new InvalidOperationException(Loc.Get("Error__InspectFirst")); image?.Validate(); if (step >= 2) CreateRecipe().ValidateOptions(); if (step >= 3) @@ -453,8 +482,8 @@ private void ShowStopped(OperationKind kind, OperationOutcome outcome) // First, so that whatever below fails to draw, the page is not left spinning. StopBuildActivity(); board.Stop(outcome.Cancelled); - var noun = kind == OperationKind.Resume ? "Resume" : "Build"; - BuildHeading.Text = outcome.Cancelled ? $"{noun} cancelled" : $"{noun} stopped"; + var noun = Loc.Get(kind == OperationKind.Resume ? "Build__Noun__Resume" : "Build__Noun__Build"); + BuildHeading.Text = outcome.Cancelled ? Loc.Format("Build__CancelledHeading", noun) : Loc.Format("Build__Stopped", noun); CurrentMessage.Text = outcome.FullMessage; HistoryCount.Text = StageSummary.Describe(board); } @@ -480,8 +509,11 @@ private void ShowNotice(string title, string message, InfoBarSeverity severity = NoticeButton.Visibility = action == NoticeAction.None ? Visibility.Collapsed : Visibility.Visible; var label = action switch { - NoticeAction.OpenLogs => "Open logs", NoticeAction.OpenTools => "Get image tools", - NoticeAction.OpenOfficialDownload => "Open Microsoft download page", _ => "Copy details" + NoticeAction.OpenLogs => Loc.Get("Notice__OpenLogs"), + NoticeAction.OpenTools => Loc.Get("Notice__OpenTools"), + NoticeAction.OpenOfficialDownload => Loc.Get("Notice__OpenOfficialDownload"), + NoticeAction.Restart => Loc.Get("Notice__Restart"), + _ => Loc.Get("Notice__CopyDetails") }; NoticeButton.Content = label; Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(NoticeButton, label); @@ -490,6 +522,7 @@ private void ShowNotice(string title, string message, InfoBarSeverity severity = private void OnNoticeAction(object sender, RoutedEventArgs args) { + if (noticeAction == NoticeAction.Restart) { RestartForLanguage(); return; } if (noticeAction == NoticeAction.OpenLogs) { OnOpenLogs(sender, args); return; } if (noticeAction is NoticeAction.OpenTools or NoticeAction.OpenOfficialDownload) { @@ -497,19 +530,19 @@ private void OnNoticeAction(object sender, RoutedEventArgs args) try { if (page is not (ServicingToolsRequiredException.DownloadPage or OfficialImageDownloader.DownloadPage or OfficialImageDownloader.ArmDownloadPage)) - throw new InvalidDataException("The suggested download page is not an approved Microsoft page."); + throw new InvalidDataException(Loc.Get("Error__NotApprovedPage")); using var browser = Process.Start(new ProcessStartInfo(page) { UseShellExecute = true }); - if (browser is null) throw new IOException("The browser could not open Microsoft's image-tools page."); + if (browser is null) throw new IOException(Loc.Get("Error__BrowserMicrosoftPage")); } catch (Exception error) when (error is IOException or InvalidDataException or System.ComponentModel.Win32Exception or InvalidOperationException) { - ShowNotice("Could not open the download page.", page + "\n" + + ShowNotice(Loc.Get("Notice__CouldNotOpenDownload"), page + "\n" + ErrorText.Reason(error), InfoBarSeverity.Error, NoticeAction.CopyDetails, error.ToString()); } return; } if (noticeAction != NoticeAction.CopyDetails || noticeDetails.Length == 0) return; - NoticeButton.Content = CopyToClipboard(noticeDetails) ? "Copied" : "Could not copy"; + NoticeButton.Content = Loc.Get(CopyToClipboard(noticeDetails) ? "Notice__Copied" : "Notice__CouldNotCopy"); } // A clipboard that is locked or unavailable is reported to the caller, never thrown into the page or a dialog. @@ -574,7 +607,7 @@ private async Task Confirm(string title, string content, string action) var dialog = new ContentDialog { XamlRoot = Root.XamlRoot, RequestedTheme = DialogTheme(), - Title = title, Content = content, PrimaryButtonText = action, CloseButtonText = "Cancel", + Title = title, Content = content, PrimaryButtonText = action, CloseButtonText = Loc.Get("Dialog__Close"), DefaultButton = ContentDialogButton.Close }; return await dialog.ShowAsync() == ContentDialogResult.Primary; @@ -600,7 +633,7 @@ private async void OnSupport(object sender, RoutedEventArgs args) } catch (Exception error) { - ShowNotice("Support options could not be opened.", ErrorText.Reason(error), + ShowNotice(Loc.Get("Notice__SupportOpenFailed__Title"), ErrorText.Reason(error), InfoBarSeverity.Error, NoticeAction.CopyDetails, error.ToString()); } finally @@ -616,15 +649,15 @@ private async void OnSupportDestination(object sender, RoutedEventArgs args) { if (sender is not FrameworkElement { Tag: string tag } || !Enum.TryParse(tag, out var destination)) - throw new InvalidDataException("This support link has no valid destination."); + throw new InvalidDataException(Loc.Get("Error__SupportLink")); var uri = ProjectSupport.Link(destination); if (!await Windows.System.Launcher.LaunchUriAsync(uri)) - throw new IOException($"Your browser could not open {uri}. No payment was made."); + throw new IOException(Loc.Format("Notice__BrowserFailed", uri)); SupportError.IsOpen = false; } catch (Exception error) { - SupportError.Title = "Could not open the support page"; + SupportError.Title = Loc.Get("Notice__SupportPageFailed__Title"); SupportError.Message = ErrorText.Reason(error); SupportError.IsOpen = true; } @@ -653,7 +686,7 @@ private async void OnSupportDestination(object sender, RoutedEventArgs args) private Task SaveFile(string filename, string extension) { var picker = new FileSavePicker(AppWindow.Id) { SuggestedFileName = filename }; - picker.FileTypeChoices.Add(extension == ".iso" ? "ISO image" : "Image recipe", new List { extension }); + picker.FileTypeChoices.Add(Loc.Get(extension == ".iso" ? "File__IsoImage" : "File__ImageRecipe"), new List { extension }); return Pick(async () => (await picker.PickSaveFileAsync())?.Path); } @@ -673,7 +706,7 @@ private void OnSourceChanged(object sender, TextChangedEventArgs args) pendingImageIndex = null; pendingSourcePath = ""; EditionChoice.ItemsSource = null; - ImageBadge.Text = "No image selected"; + ImageBadge.Text = Loc.Get("Image__None"); ClearInventory(); UpdateNavigation(); } @@ -686,9 +719,9 @@ private void OnEditionChanged(object sender, SelectionChangedEventArgs args) SourcePathBox.Text.Trim().Equals(pendingSourcePath, StringComparison.OrdinalIgnoreCase))) ClearInventory(); image = selected; - ImageBadge.Text = image is null ? "No image selected" : $"{image.Name}\n{image.Architecture} · build {image.Version.Build}"; + ImageBadge.Text = image is null ? Loc.Get("Image__None") : $"{image.Name}\n{image.Architecture} · build {image.Version.Build}"; if (image is not null && !image.IsSupported) - ShowNotice("This edition is not supported.", "Choose a Windows 11 client image, 22H2 or newer, in x64 or ARM64 format.", InfoBarSeverity.Warning); + ShowNotice(Loc.Get("Notice__UnsupportedEdition__Title"), Loc.Get("Notice__UnsupportedEdition__Message"), InfoBarSeverity.Warning); UpdateNavigation(); } @@ -696,7 +729,7 @@ private async void OnInspect(object sender, RoutedEventArgs args) { if (!ToolLocator.IsAdministrator) { - ShowNotice("Administrator privileges required.", "Close and reopen Tiny11Builder.exe, then approve the Windows administrator prompt.", InfoBarSeverity.Warning); + ShowNotice(Loc.Get("Notice__AdminRequired__Title"), Loc.Get("Notice__AdminInspect__Message"), InfoBarSeverity.Warning); return; } await RunOperation(OperationKind.Inspect, InspectSourceAsync); @@ -706,10 +739,10 @@ private async Task InspectSourceAsync(CancellationToken token) { if (!ToolLocator.IsAdministrator) { - ShowNotice("Image selected.", "Close and reopen Tiny11Builder.exe with administrator approval to inspect its editions."); + ShowNotice(Loc.Get("Notice__ImageSelected__Title"), Loc.Get("Notice__AdminInspectSelected__Message")); return; } - Working("Inspecting the image…"); + Working(Loc.Get("Working__Inspecting")); var source = SourcePathBox.Text.Trim(); servicingTool = ToolLocator.FindDism(); await Task.Run(() => ToolLocator.Validate(servicingTool, "dism.exe"), token); @@ -717,7 +750,7 @@ private async Task InspectSourceAsync(CancellationToken token) var restored = source.Equals(pendingSourcePath, StringComparison.OrdinalIgnoreCase) ? pendingImageIndex : null; EditionChoice.ItemsSource = images; EditionChoice.SelectedIndex = ImageDefaults.SelectPosition(images, restored); - ShowNotice("Image inspected.", $"{images.Count} editions found. {image?.Name} is selected."); + ShowNotice(Loc.Get("Notice__ImageInspected__Title"), Loc.Format("Notice__ImageInspected__Message", images.Count, image?.Name)); } private void BeginDownloadText(Func format) @@ -754,7 +787,7 @@ private async void OnDownload(object sender, RoutedEventArgs args) => await RunO var hash = OfficialImageDownloader.ValidateHash(PublishedHashBox.Text); var path = await SaveFile(Path.GetFileName(uri.AbsolutePath), ".iso"); if (path is null) return; - Working("Downloading from Microsoft…"); + Working(Loc.Get("Working__Downloading")); using var client = OfficialImageDownloader.CreateClient(); DownloadBar.IsIndeterminate = true; BeginDownloadText(p => FormatDownload(p, "")); @@ -764,17 +797,16 @@ private async void OnDownload(object sender, RoutedEventArgs args) => await RunO status: new UiProgress(text => DispatcherQueue.TryEnqueue(() => DownloadStatus.Text = text))); DrainDownload(); SourcePathBox.Text = downloaded.Path; - DownloadStatus.Text = $"SHA-256: {downloaded.Sha256}"; - ShowNotice(downloaded.VerifiedAgainstPublishedHash ? "Official ISO downloaded and hash-verified." : "Microsoft-hosted ISO downloaded.", - downloaded.VerifiedAgainstPublishedHash ? "The file matches the supplied published SHA-256. Inspect it to select an edition." : - "No published hash was supplied. The computed hash is shown, but authenticity has not been independently verified.", InfoBarSeverity.Informational); + DownloadStatus.Text = Loc.Format("Notice__DownloadHash", downloaded.Sha256); + ShowNotice(Loc.Get(downloaded.VerifiedAgainstPublishedHash ? "Notice__DownloadVerified__Title" : "Notice__DownloadUnverified__Title"), + Loc.Get(downloaded.VerifiedAgainstPublishedHash ? "Notice__DownloadVerified__Message" : "Notice__DownloadUnverified__Message"), InfoBarSeverity.Informational); } finally { EndDownloadText(); } }); private async void OnDownloadLatest(object sender, RoutedEventArgs args) => await RunOperation(OperationKind.Download, async token => { - Working("Finding the latest Windows 11…"); + Working(Loc.Get("Working__FindingLatest")); DownloadBar.IsIndeterminate = true; try { @@ -790,27 +822,27 @@ private async void OnDownloadLatest(object sender, RoutedEventArgs args) => awai var hashFile = path + ".sha256"; if (File.Exists(path) && File.Exists(hashFile)) { - Working("Checking the downloaded ISO…"); + Working(Loc.Get("Working__CheckingDownload")); await using var input = File.OpenRead(path); await IsoStructure.ValidateAsync(input, token); var actual = Convert.ToHexString(await SHA256.HashDataAsync(input, token)); if (actual != (await File.ReadAllTextAsync(hashFile, token)).Trim() || (media.Sha256 is not null && actual != media.Sha256)) - throw new InvalidDataException("The cached ISO has changed. It has not been accepted."); - DownloadStatus.Text = "Previously downloaded ISO verified."; + throw new InvalidDataException(Loc.Get("Notice__CachedIsoChanged")); + DownloadStatus.Text = Loc.Get("Notice__PreviouslyDownloaded"); } else { if (File.Exists(path)) path = Path.Combine(folder, Path.GetFileNameWithoutExtension(media.FileName) + "-" + Guid.NewGuid().ToString("N")[..8] + ".iso"); - Working("Downloading from Microsoft…"); + Working(Loc.Get("Working__Downloading")); using var downloadClient = OfficialImageDownloader.CreateClient(); BeginDownloadText(p => FormatDownload(p, $"{media.Language} · ")); var download = await new OfficialImageDownloader(downloadClient).DownloadAsync(media.Url.AbsoluteUri, path, media.Sha256, downloadUpdates, token, status: catalogProgress); DrainDownload(); await File.WriteAllTextAsync(path + ".sha256", download.Sha256, token); - DownloadStatus.Text = $"Downloaded {media.Description}. SHA-256: {download.Sha256}" + - (download.VerifiedAgainstPublishedHash ? " (published hash verified)" : " (computed hash; Microsoft supplied no published hash)"); + DownloadStatus.Text = Loc.Format("Notice__DownloadedSummary", media.Description, download.Sha256) + + Loc.Get(download.VerifiedAgainstPublishedHash ? "Notice__HashVerifiedSuffix" : "Notice__ComputedHashSuffix"); } SourcePathBox.Text = path; await InspectSourceAsync(token); @@ -825,7 +857,7 @@ private async Task EnsureToolsAsync(CancellationToken token) client.Timeout = TimeSpan.FromSeconds(60); isoTool = await new OscdimgAcquirer(client).EnsureAsync(Path.Combine(dataDirectory, "tools"), new UiProgress(text => DispatcherQueue.TryEnqueue(() => ToolStatus.Text = text)), token); - ToolStatus.Text = "Image and ISO tools ready."; + ToolStatus.Text = Loc.Get("Settings__Tools__Ready"); } private void OnModeChanged(object sender, SelectionChangedEventArgs args) @@ -852,14 +884,14 @@ private void UpdateModePresentation() CustomChoices.Visibility = SelectedMode == BuildMode.Custom ? Visibility.Visible : Visibility.Collapsed; CoreWarning.IsOpen = UsesCore; CoreConsentStatus.Visibility = UsesCore ? Visibility.Visible : Visibility.Collapsed; - CoreConsentStatus.Text = coreAcknowledged ? "Core limitations accepted for this build." : "You will be asked to accept Core's limitations before continuing."; - ModeDescription.Text = SelectedMode switch + CoreConsentStatus.Text = Loc.Get(coreAcknowledged ? "Components__CoreConsent__Accepted" : "Components__CoreConsent__Pending"); + ModeDescription.Text = Loc.Get(SelectedMode switch { - BuildMode.Tiny11 => "Original Tiny11 removals, including Edge/WebView2 and OneDrive binaries. Windows Update, Defender and recovery are kept. Choose Custom to change individual removals.", - BuildMode.Tiny11Core => "Tiny11 Core removals, including irreversible component pruning. Choose Custom to change individual removals.", - _ => "Start with nothing selected, then choose what to remove or disable. You can also list individual components, including protected items." - }; - SourceMode.Text = $"Build mode: {(ModeChoice.SelectedItem as ComboBoxItem)?.Content}"; + BuildMode.Tiny11 => "Components__ModeDescription__Tiny11", + BuildMode.Tiny11Core => "Components__ModeDescription__Core", + _ => "Components__ModeDescription__Custom" + }); + SourceMode.Text = Loc.Format("Source__BuildMode", (ModeChoice.SelectedItem as ComboBoxItem)?.Content); MarkChoice(Tiny11Selection, SelectedMode == BuildMode.Tiny11); MarkChoice(CoreSelection, SelectedMode == BuildMode.Tiny11Core); MarkChoice(CustomSelection, SelectedMode == BuildMode.Custom); @@ -869,7 +901,7 @@ private void UpdateModePresentation() private static void MarkChoice(Button card, bool selected) { card.Style = (Style)Application.Current.Resources[selected ? "ModeCardSelected" : "ModeCard"]; - Microsoft.UI.Xaml.Automation.AutomationProperties.SetItemStatus(card, selected ? "Selected" : "Not selected"); + Microsoft.UI.Xaml.Automation.AutomationProperties.SetItemStatus(card, Loc.Get(selected ? "Components__Selected" : "Components__NotSelected")); } private void OnChoicesChanged(object sender, RoutedEventArgs args) { @@ -886,28 +918,26 @@ private void OnCustomCoreChanged(object sender, RoutedEventArgs args) private void OnOptionFilterChanged(object sender, SelectionChangedEventArgs args) { if (initialized) FilterOptions(); } private void FilterOptions() { - var category = (OptionFilter.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "All"; - OptionsList.ItemsSource = options.Where(o => category == "All" || o.Category == category).ToArray(); + var category = (OptionFilter.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? Loc.Get("Common__All"); + OptionsList.ItemsSource = options.Where(o => category == Loc.Get("Common__All") || o.Category == category).ToArray(); } private void UpdateCounts() { if (!initialized) return; var custom = inventory.Count > 0 ? inventory.Count(i => i.Selected) : pendingActions.Length; - SelectionCount.Text = $"{options.Count(o => o.Selected)} removal groups selected · {custom} advanced actions"; - ExpertCount.Text = $"{inventory.Count:N0} components listed · {custom:N0} advanced actions selected."; + SelectionCount.Text = Loc.Format("Components__Counts__Groups", options.Count(o => o.Selected), custom); + ExpertCount.Text = Loc.Format("Components__Counts__Inventory", inventory.Count.ToString("N0"), custom.ToString("N0")); UpdateNavigation(); } private async void OnOpenExpert(object sender, RoutedEventArgs args) { if (busy || modalOpen || SelectedMode != BuildMode.Custom) return; - if (image is null) { ShowNotice("Inspect an image first.", "Select an edition before listing its components.", InfoBarSeverity.Warning); return; } - if (!await Confirm("Protected components are included", - "This inventory includes boot drivers, security, licensing, system apps and component payloads. Removing them can make Windows unbootable, insecure or impossible to update. Protected packages may refuse removal. Direct payload removal is unsupported.\n\nContinue only for isolated testing, with backups and a known-good installer.", - "Inspect components")) return; + if (image is null) { ShowNotice(Loc.Get("Notice__InspectImageFirst__Title"), Loc.Get("Notice__InspectImageFirst__Message"), InfoBarSeverity.Warning); return; } + if (!await Confirm(Loc.Get("Dialog__Expert__Title"), Loc.Get("Dialog__Expert__Body"), Loc.Get("Dialog__Expert__Inspect"))) return; await RunOperation(OperationKind.Inventory, async token => { - Working("Listing image components…"); + Working(Loc.Get("Working__ListingComponents")); var recipe = CreateRecipe() with { CoreAcknowledged = true, ExpertAcknowledged = true }; var items = await Task.Run(() => engine.InventoryAsync(recipe, BuildProgressReporter(), token), token); var previous = inventory.Count > 0 ? inventory.Where(i => i.Selected).Select(i => i.Action).ToArray() : pendingActions; @@ -925,11 +955,10 @@ await RunOperation(OperationKind.Inventory, async token => FilterInventory(); UpdateCounts(); if (!ToolLocator.SupportsServicing(recipe.DismPath, image!)) - ShowNotice("Component list loaded read-only.", "The installed tools could list this image, but building it requires newer " + - "Windows ADK Deployment Tools. Install them and reopen the app before building.", + ShowNotice(Loc.Get("Notice__ComponentListReadOnly__Title"), Loc.Get("Notice__ComponentListReadOnly__Message"), InfoBarSeverity.Warning, NoticeAction.OpenTools); else - ShowNotice("Component list loaded.", "Existing selections were preserved. Check the individual items you want to remove or disable."); + ShowNotice(Loc.Get("Notice__ComponentListLoaded__Title"), Loc.Get("Notice__ComponentListLoaded__Message")); }); } @@ -946,9 +975,9 @@ private void ClearInventory() private void OnInventoryFilterSelection(object sender, SelectionChangedEventArgs args) { if (initialized) FilterInventory(); } private void FilterInventory() { - var filter = (InventoryFilter.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "All"; + var filter = (InventoryFilter.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? Loc.Get("Common__All"); var query = InventorySearch.Text.Trim(); - InventoryList.ItemsSource = inventory.Where(i => (filter == "All" || i.Item.Kind.ToString() == filter) && + InventoryList.ItemsSource = inventory.Where(i => (filter == Loc.Get("Common__All") || i.Item.Kind.ToString() == filter) && (query.Length == 0 || i.Title.Contains(query, StringComparison.OrdinalIgnoreCase) || i.Detail.Contains(query, StringComparison.OrdinalIgnoreCase))).ToArray(); } @@ -995,44 +1024,47 @@ private void UpdateReview() { var recipe = CreateRecipe(); ReviewText.Text = CreateReview(); - ReviewImage.Text = image?.Summary ?? "No image selected"; + ReviewImage.Text = image?.Summary ?? Loc.Get("Review__NoImage"); ReviewMode.Text = (ModeChoice.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? ""; - ReviewRemovals.Text = $"{recipe.SelectedOptions.Length} removal groups · {recipe.ExpertActions.Length} individual actions\n" + - (UsesCore ? "No servicing, recovery or Defender." : SelectedMode == BuildMode.Custom ? - "Custom removals may affect servicing, recovery or security." : "Windows Update, recovery and Defender retained."); - ReviewPaths.Text = $"ISO\n{recipe.OutputPath}\n\nWorkspace\n{recipe.WorkspacePath}"; - var compression = recipe.Compression switch { Compression.Recovery => "Recovery ESD", Compression.Maximum => "Maximum WIM", _ => "Fast WIM" }; - ReviewOptions.Text = $"{compression} compression\nTurbo mode: {(recipe.TurboMode ? "On" : "Off")}\n" + - $"Redundant data: {(recipe.RemoveRedundantData ? "Remove" : "Keep")}\n.NET setup payload: {(recipe.OmitNetFx3SetupPayload ? "Omit" : "Keep")}\n" + - $"Compact installation: {(recipe.CompactInstallation ? "On" : "Off")}\n.NET 3.5: {(recipe.EnableNetFx3 ? "Included" : "Not included")}\n" + - $"Local-account setup: {(recipe.PreferLocalAccount ? "Preferred" : "Default")}\nWorking files: {(recipe.KeepWorkingFiles ? "Keep" : "Clean after build")}" + + ReviewRemovals.Text = Loc.Format("Review__Removals__Summary", recipe.SelectedOptions.Length, recipe.ExpertActions.Length) + "\n" + + Loc.Get(UsesCore ? "Review__Removals__Core" : SelectedMode == BuildMode.Custom ? + "Review__Removals__Custom" : "Review__Removals__Tiny11"); + ReviewPaths.Text = Loc.Format("Review__Paths__Value", recipe.OutputPath, recipe.WorkspacePath); + var compression = Loc.Get(recipe.Compression switch { Compression.Recovery => "Settings__Compression__Recovery", Compression.Maximum => "Settings__Compression__Maximum", _ => "Settings__Compression__Fast" }); + ReviewOptions.Text = Loc.Format("Review__Options__Compression", compression) + "\n" + + Loc.Format("Review__Options__Turbo", Loc.Get(recipe.TurboMode ? "Common__On" : "Common__Off")) + "\n" + + Loc.Format("Review__Options__Redundant", Loc.Get(recipe.RemoveRedundantData ? "Common__Remove" : "Common__Keep")) + "\n" + + Loc.Format("Review__Options__OmitSetup", Loc.Get(recipe.OmitNetFx3SetupPayload ? "Common__Omit" : "Common__Keep")) + "\n" + + Loc.Format("Review__Options__Compact", Loc.Get(recipe.CompactInstallation ? "Common__On" : "Common__Off")) + "\n" + + Loc.Format("Review__Options__NetFx", Loc.Get(recipe.EnableNetFx3 ? "Review__Options__Included" : "Review__Options__NotIncluded")) + "\n" + + Loc.Format("Review__Options__LocalAccount", Loc.Get(recipe.PreferLocalAccount ? "Review__Options__Preferred" : "Review__Options__Default")) + "\n" + + Loc.Format("Review__Options__KeepFiles", Loc.Get(recipe.KeepWorkingFiles ? "Common__Keep" : "Review__Options__CleanAfterBuild")) + (SetupLabels.Legacy(recipe) is { } legacy ? "\n" + legacy : ""); } - private const string BuildDescriptionText = "The selected edition will be copied, modified, compressed and saved as a bootable ISO."; - // The legacy installer is derived from the removals, never chosen, so this only reports it. - private void UpdateBuildDescription(BuildRecipe recipe) => BuildDescription.Text = BuildDescriptionText + - (SetupLabels.LegacyReason(recipe) is { } reason ? $" The legacy installer is turned on automatically for {reason}." : ""); + private void UpdateBuildDescription(BuildRecipe recipe) => BuildDescription.Text = Loc.Get("Build__Description") + + (SetupLabels.LegacyReason(recipe) is { } reason ? Loc.Format("Build__Description__Legacy", reason) : ""); private async Task StartBuildAsync() { BuildRecipe recipe; try { recipe = CreateRecipe(); recipe.ValidateOptions(); PathSafety.Validate(recipe); } - catch (Exception error) { ShowNotice("The recipe is not ready.", ErrorText.Reason(error), InfoBarSeverity.Error); return; } - if (!await Confirm("Create this offline Windows image?", - $"Output: {recipe.OutputPath}\n\nMode: {(ModeChoice.SelectedItem as ComboBoxItem)?.Content}\nRemoval groups: {recipe.SelectedOptions.Length}\nIndividual actions: {recipe.ExpertActions.Length}" + - (SetupLabels.Legacy(recipe) is { } legacy ? "\n" + legacy : ""), - "Build")) return; + catch (Exception error) { ShowNotice(Loc.Get("Notice__RecipeNotReady__Title"), ErrorText.Reason(error), InfoBarSeverity.Error); return; } + if (!await Confirm(Loc.Get("Dialog__Build__Title"), + Loc.Format("Dialog__Build__Body", recipe.OutputPath, (ModeChoice.SelectedItem as ComboBoxItem)?.Content, + recipe.SelectedOptions.Length, recipe.ExpertActions.Length, + SetupLabels.Legacy(recipe) is { } legacy ? "\n" + legacy : ""), + Loc.Get("Dialog__Build__Action"))) return; await RunOperation(OperationKind.Build, async token => { ResetBuildPage(); BuildActivity.IsActive = true; BuildBar.IsIndeterminate = true; - CurrentMessage.Text = "Preparing image tools…"; + CurrentMessage.Text = Loc.Get("Build__PreparingTools"); await EnsureToolsAsync(token); recipe = CreateRecipe(); - BeginBuildRun("Building"); + BeginBuildRun(Loc.Get("Build__Building")); var reporter = BuildProgressReporter(); var built = await Task.Run(() => engine.BuildAsync(recipe, reporter, token), token); ShowBuildResult(built, resumed: false, turbo: recipe.TurboMode); @@ -1054,9 +1086,9 @@ private void ResetBuildPage() ResultWarnings.IsOpen = false; liveLog.Clear(); RefreshLog(force: true); - BuildHeading.Text = "Build"; - CurrentStage.Text = "Waiting to start"; - CurrentMessage.Text = "No changes have been made."; + BuildHeading.Text = Loc.Get("Build__Heading"); + CurrentStage.Text = Loc.Get("Build__Waiting"); + CurrentMessage.Text = Loc.Get("Build__NoChanges"); BuildGlyph.Glyph = "\uE7C5"; BuildActivity.IsActive = false; BuildBar.IsIndeterminate = false; @@ -1082,41 +1114,41 @@ private void ShowBuildResult(BuildResult built, bool resumed, bool turbo) workDirectory = built.WorkDirectory; ResultPath.Text = built.IsoPath; ResultHash.Text = built.Sha256; - var size = File.Exists(built.IsoPath) ? $" · {new FileInfo(built.IsoPath).Length / (1024d * 1024 * 1024):N2} GiB ISO" : ""; + var size = File.Exists(built.IsoPath) ? " · " + Loc.Format("Build__Result__IsoSize", (new FileInfo(built.IsoPath).Length / (1024d * 1024 * 1024)).ToString("N2")) : ""; + var minutes = Loc.Format("Build__Result__Minutes", built.Duration.TotalMinutes.ToString("N1")); var metrics = new List { - $"{(resumed ? "Resumed build" : turbo ? "Turbo build" : "Standard build")} · {built.Duration.TotalMinutes:N1} minutes{(resumed ? " in total" : "")}{size}" + Loc.Get(resumed ? "Build__Result__Resumed" : turbo ? "Build__Result__Turbo" : "Build__Result__Standard") + " · " + + (resumed ? Loc.Format("Build__Result__InTotal", minutes) : minutes) + size }; if (!resumed || built.RemovedLogicalBytes > 0 || built.OmittedSetupBytes > 0) - metrics.Add($"Redundant data removed: {built.RemovedLogicalBytes / (1024d * 1024):N1} MiB logical · " + - $"Setup payload omitted: {built.OmittedSetupBytes / (1024d * 1024):N1} MiB"); - metrics.Add("Detailed stage timings are saved with the build report."); + metrics.Add(Loc.Format("Build__Result__RedundantRemoved", (built.RemovedLogicalBytes / (1024d * 1024)).ToString("N1"), + (built.OmittedSetupBytes / (1024d * 1024)).ToString("N1"))); + metrics.Add(Loc.Get("Build__Result__StageTimings")); ResultMetrics.Text = string.Join("\n", metrics); var warnings = built.Warnings; // A cancel prompt open for this run reports the warning too, instead of calling the end clean. if (warnings.Count > 0) - active?.NoteWarning("The ISO was saved with warnings. See the warnings on the page."); + active?.NoteWarning(Loc.Get("Build__Result__WarningNotice")); ResultWarnings.IsOpen = warnings.Count > 0; if (warnings.Count > 0) ResultWarnings.Message = string.Join("\n", warnings.Take(4).Select(w => "• " + ErrorText.Summary(w, 200))) + - (warnings.Count > 4 ? $"\n• {warnings.Count - 4} more in the build log" : ""); + (warnings.Count > 4 ? "\n• " + Loc.Format("Build__Result__MoreWarnings", warnings.Count - 4) : ""); ResultPanel.Visibility = Visibility.Visible; - BuildHeading.Text = "ISO created"; - CurrentStage.Text = "Complete"; + BuildHeading.Text = Loc.Get("Build__IsoCreated"); + CurrentStage.Text = Loc.Get("Build__Stage__Complete"); BuildActivity.IsActive = false; BuildGlyph.Glyph = "\uE73E"; if (resumed) board.MarkEarlierPhasesDone(); board.Complete(); OverallBar.Value = 100; BuildPercent.Text = "100%"; - OverallStep.Text = "All stages done"; + OverallStep.Text = Loc.Format("Build__Stages__AllDone", board.Stages.Length); CurrentPercent.Text = "100%"; HistoryCount.Text = StageSummary.Describe(board); BuildBar.IsIndeterminate = false; BuildBar.Value = 100; - CurrentMessage.Text = warnings.Count > 0 ? - "The ISO was saved with warnings. See the skipped work and other details below." : - "The ISO and build report have been saved. Test the ISO in a VM before use."; + CurrentMessage.Text = Loc.Get(warnings.Count > 0 ? "Build__Result__SavedWarnings" : "Build__Result__Saved"); stoppedRun = null; stoppedHasCheckpoint = false; completed = true; @@ -1139,7 +1171,7 @@ private void FlushBuildProgress(bool flushAll = false) // the build, so it must not move the page's stage, bars or history. if (!string.IsNullOrWhiteSpace(p.Message) && !BuildProgressText.IsConsoleProgress(p.Message)) { - if (running is OperationKind.Build or OperationKind.Resume) CurrentMessage.Text = $"Cancellation requested. {p.Message}…"; + if (running is OperationKind.Build or OperationKind.Resume) CurrentMessage.Text = Loc.Format("Build__CancellationRequested", p.Message); liveLog.Add($"[{p.Stage}] {BuildProgressText.ForDisplay(p.Message)}"); } continue; @@ -1152,11 +1184,11 @@ private void FlushBuildProgress(bool flushAll = false) } if (!string.IsNullOrWhiteSpace(p.Message) && !BuildProgressText.IsConsoleProgress(p.Message)) { - CurrentStage.Text = p.Stage; + CurrentStage.Text = StageName(p.Stage); // Once cancellation has been requested, that status stays until the run actually stops. if (!cancelRequested) CurrentMessage.Text = p.State == BuildOperationState.Failed ? - p.Message + " failed. Expand this stage for error details." : p.Message; + Loc.Format("Build__StageFailed__Message", p.Message) : p.Message; } BuildBar.IsIndeterminate = p.Percent is null && p.State is not (BuildOperationState.Completed or BuildOperationState.Failed or BuildOperationState.Cancelled); if (p.Percent is double percent) BuildBar.Value = percent; @@ -1174,7 +1206,7 @@ private void FlushBuildProgress(bool flushAll = false) var overall = start + (end - start) * stage.Fraction(p); OverallBar.Value = Math.Max(OverallBar.Value, overall); BuildPercent.Text = $"{OverallBar.Value:0}%"; - OverallStep.Text = $"Stage {phase + 1} of {board.Stages.Length}"; + OverallStep.Text = Loc.Format("Build__Stage__Of", phase + 1, board.Stages.Length); BuildGlyph.Glyph = stage.Glyph; } var line = p.Detail ?? (p.State is null ? null : p.Message); @@ -1287,8 +1319,8 @@ private async Task WatchSettled(CancellationFlow flow) private void AfterCancelRequested() { if (running is OperationKind.Build or OperationKind.Resume) - CurrentMessage.Text = "Cancellation requested. Waiting for the current operation to finish…"; - Working("Cancelling…"); + CurrentMessage.Text = Loc.Get("Build__CancellationWaiting"); + Working(Loc.Get("Build__Cancelling")); UpdateNavigation(); } @@ -1335,7 +1367,7 @@ public CancelPrompt(CancellationFlow flow, XamlRoot root, ElementTheme theme, Ac detail.Style = hint; footnote.Style = hint; Microsoft.UI.Xaml.Automation.AutomationProperties.SetLiveSetting(status, Microsoft.UI.Xaml.Automation.Peers.AutomationLiveSetting.Polite); - Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(bar, "Cancellation progress"); + Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(bar, Loc.Get("Notice__CancellationProgress")); progress.Children.Add(status); progress.Children.Add(bar); progress.Children.Add(detail); @@ -1420,7 +1452,7 @@ private void OnLink(ContentDialog sender, ContentDialogButtonClickEventArgs args var copy = flow.View.Link == CancelLink.CopyDetails; var done = copy ? end is { Details.Length: > 0 } && copyDetails(end.Details) : end?.WorkDirectory is { } directory && openLogs(directory); Safely("The cancel dialog could not update its link.", - () => dialog.SecondaryButtonText = copy ? done ? "Copied" : "Could not copy" : done ? "Open logs" : "Could not open logs"); + () => dialog.SecondaryButtonText = copy ? Loc.Get(done ? "Notice__Copied" : "Notice__CouldNotCopy") : Loc.Get(done ? "Notice__OpenLogs" : "Notice__CouldNotOpenLogs")); } private void OnClosing(ContentDialog sender, ContentDialogClosingEventArgs args) @@ -1441,14 +1473,14 @@ private async void OnSaveRecipe(object sender, RoutedEventArgs args) => await Ru var recipe = CreateRecipe(); recipe.ValidateOptions(); var path = await SaveFile("tiny11-recipe", ".json"); - if (path is not null) { recipe.Write(path); ShowNotice("Recipe saved.", "It contains selections and local paths, not downloaded media or credentials."); } + if (path is not null) { recipe.Write(path); ShowNotice(Loc.Get("Notice__RecipeSaved__Title"), Loc.Get("Notice__RecipeSaved__Message")); } }); private async void OnLoadRecipe(object sender, RoutedEventArgs args) => await RunOperation(OperationKind.Recipe, async _ => { var path = await PickFile(".json"); if (path is null) return; RestoreRecipe(BuildRecipe.Read(path)); - ShowNotice("Recipe loaded.", "Inspect the source again. Core/expert risk acknowledgements must be renewed."); + ShowNotice(Loc.Get("Notice__RecipeLoaded__Title"), Loc.Get("Notice__RecipeLoaded__Message")); }); private void RestoreRecipe(BuildRecipe recipe) @@ -1487,7 +1519,7 @@ private async void OnRecover(object sender, RoutedEventArgs args) if (busy || modalOpen) return; if (!ToolLocator.IsAdministrator) { - ShowNotice("Administrator privileges required.", "Close and reopen Tiny11Builder.exe with administrator approval to recover a workspace.", InfoBarSeverity.Warning); + ShowNotice(Loc.Get("Notice__AdminRequired__Title"), Loc.Get("Notice__AdminRecover__Message"), InfoBarSeverity.Warning); return; } var path = await PickFile(".json"); @@ -1495,24 +1527,18 @@ private async void OnRecover(object sender, RoutedEventArgs args) // Advisory only: the engine decides. This just avoids offering a discard it would refuse. if (JobCheckpoint.Exists(Path.GetDirectoryName(Path.GetFullPath(path)) ?? "")) { - if (await Confirm("This job has saved progress", - "Recover discards a job's working copy, and it does not do that for a job with saved progress. " + - "Resume continues the build from that progress instead.\n\nNothing has been changed.", "Resume instead")) + if (await Confirm(Loc.Get("Dialog__RecoverProgress__Title"), Loc.Get("Dialog__RecoverProgress__Body"), Loc.Get("Dialog__RecoverProgress__Resume"))) await ResumeJobAsync(path); return; } - if (!await Confirm("Discard this interrupted workspace?", - "Select only job.json from the interrupted tiny11 job. This discards that job's mounted working copy and removes its staged media. " + - "Its logs, recipe, report and your source ISO are kept. A job whose image was already committed has saved progress and is never discarded here; " + - "use Resume build for it. Active builds cannot be recovered.", - "Discard working copy")) return; + if (!await Confirm(Loc.Get("Dialog__Recover__Title"), Loc.Get("Dialog__Recover__Body"), Loc.Get("Dialog__Recover__Discard"))) return; await RunOperation(OperationKind.Recover, async token => { - Working("Recovering workspace…"); + Working(Loc.Get("Working__Recovering")); var dism = ToolLocator.FindDism(); ToolLocator.Validate(dism, "dism.exe"); await Task.Run(() => engine.RecoverAsync(path, dism, token), token); - ShowNotice("Workspace recovered.", "The interrupted working copy was discarded. Your source and logs were preserved."); + ShowNotice(Loc.Get("Notice__WorkspaceRecovered__Title"), Loc.Get("Notice__WorkspaceRecovered__Message")); }); } @@ -1537,22 +1563,22 @@ private async Task ResumeJobAsync(string jobFile) var candidate = ResumeSupport.Inspect(jobFile, JobCheckpoint.Exists, JobCheckpoint.RecipeFileName); if (candidate.Recipe is not { } original) { - ShowNotice("This build can't be resumed.", candidate.Problem ?? ResumeSupport.NoCheckpoint, InfoBarSeverity.Warning); + ShowNotice(Loc.Get("Notice__CannotResume__Title"), candidate.Problem ?? Loc.Get("Resume__NoCheckpoint"), InfoBarSeverity.Warning); return; } if (!ToolLocator.IsAdministrator) { - ShowNotice("Administrator privileges required.", "Close and reopen Tiny11Builder.exe with administrator approval, then choose Resume build. Nothing was changed.", InfoBarSeverity.Warning); + ShowNotice(Loc.Get("Notice__AdminRequired__Title"), Loc.Get("Notice__AdminResume__Message"), InfoBarSeverity.Warning); return; } - if (!await Confirm("Resume this build?", ResumeSupport.Describe(original), "Resume")) return; + if (!await Confirm(Loc.Get("Dialog__Resume__Title"), ResumeSupport.Describe(original), Loc.Get("Dialog__Resume__Action"))) return; var recipe = original; if (File.Exists(recipe.OutputPath)) { var picked = await SaveFile(Path.GetFileNameWithoutExtension(recipe.OutputPath), ".iso"); if (picked is null) { - ShowNotice("Resume not started.", "The output file already exists. Choose a new file name to continue.", InfoBarSeverity.Warning); + ShowNotice(Loc.Get("Notice__ResumeNotStarted__Title"), Loc.Get("Notice__ResumeNotStarted__Message"), InfoBarSeverity.Warning); return; } recipe = recipe with { OutputPath = picked }; @@ -1567,11 +1593,11 @@ await RunOperation(OperationKind.Resume, async token => workDirectory = candidate.JobDirectory; BuildActivity.IsActive = true; BuildBar.IsIndeterminate = true; - CurrentMessage.Text = "Preparing image tools…"; + CurrentMessage.Text = Loc.Get("Build__PreparingTools"); await EnsureToolsAsync(token); recipe = recipe with { DismPath = servicingTool, OscdimgPath = isoTool }; new BuildHost().ValidateTools(recipe); - BeginBuildRun("Resuming"); + BeginBuildRun(Loc.Get("Build__Resuming")); var reporter = BuildProgressReporter(); var built = await Task.Run(() => engine.ResumeAsync(candidate.JobFile, recipe, reporter, token), token); ShowBuildResult(built, resumed: true, turbo: recipe.TurboMode); @@ -1587,7 +1613,7 @@ private void OnOpenLogs(object sender, RoutedEventArgs args) if (workDirectory is null) return; if (!Directory.Exists(workDirectory)) { - ShowNotice("The job folder is gone.", $"{workDirectory} no longer exists.", InfoBarSeverity.Warning); + ShowNotice(Loc.Get("Notice__JobFolderGone__Title"), Loc.Format("Notice__JobFolderGone__Message", workDirectory), InfoBarSeverity.Warning); return; } OpenFolder(workDirectory); @@ -1595,11 +1621,44 @@ private void OnOpenLogs(object sender, RoutedEventArgs args) private void OpenFolder(string path) { try { using var process = Process.Start(new ProcessStartInfo("explorer.exe") { UseShellExecute = true, Arguments = $"\"{path}\"" }); } - catch (Exception error) { ShowNotice("Folder could not be opened.", ErrorText.Reason(error), InfoBarSeverity.Error); } + catch (Exception error) { ShowNotice(Loc.Get("Notice__FolderOpenFailed__Title"), ErrorText.Reason(error), InfoBarSeverity.Error); } } private void OnThemeChanged(object sender, SelectionChangedEventArgs args) => ApplyTheme(); + private void OnLanguageChanged(object sender, SelectionChangedEventArgs args) + { + if (!initialized || LanguageChoice.SelectedItem is not AppLanguage language) return; + var settings = AppSettings.Load(); + settings.Language = language.Tag; + if (!settings.TrySave()) + { + ShowNotice(Loc.Get("Language__SaveFailed__Title"), Loc.Get("Language__SaveFailed__Message"), InfoBarSeverity.Warning); + return; + } + ShowNotice(Loc.Get("Language__Restart__Title"), Loc.Get("Language__Restart__Message"), + InfoBarSeverity.Informational, NoticeAction.Restart); + } + + // A language change is applied by a fresh process: XAML resources are resolved when a page is + // loaded, and restarting is the only way to re-resolve every string without losing the build state. + private void RestartForLanguage() + { + if (busy) return; + try + { + var executable = Environment.ProcessPath; + if (string.IsNullOrEmpty(executable)) throw new InvalidOperationException(Loc.Get("Error__RestartPath")); + Process.Start(new ProcessStartInfo(executable) { UseShellExecute = false }); + Application.Current.Exit(); + } + catch (Exception error) + { + ShowNotice(Loc.Get("Language__RestartFailed__Title"), ErrorText.Reason(error), + InfoBarSeverity.Error, NoticeAction.CopyDetails, error.ToString()); + } + } + private bool HighContrast { get @@ -1627,7 +1686,7 @@ private void ApplyContrast() if (contrast == appliedContrast) return; appliedContrast = contrast; StepNumber.Visibility = contrast ? Visibility.Collapsed : Visibility.Visible; - ToolTipService.SetToolTip(ThemeChoice, contrast ? "Windows high contrast is on, so colors follow your system settings." : null); + ToolTipService.SetToolTip(ThemeChoice, contrast ? Loc.Get("Notice__HighContrastTooltip") : null); ApplyTheme(); UpdateNavigation(); } diff --git a/src/Tiny11Builder.App/OperationFlow.cs b/src/Tiny11Builder.App/OperationFlow.cs index 54f1f48a..e046014d 100644 --- a/src/Tiny11Builder.App/OperationFlow.cs +++ b/src/Tiny11Builder.App/OperationFlow.cs @@ -50,7 +50,7 @@ public static OperationOutcome Classify(OperationKind kind, Exception error, boo var recovery = failure is not null && NeedsRecovery(failure); var (title, message) = cancelled ? Cancelled(kind, canResume) : Failed(kind, ErrorText.Reason(error), canResume); if (recovery) - message += " The job's mounted copy could not be discarded. Use Recover workspace and choose this job's job.json."; + message += " " + Loc.Get("Outcome__RecoveryNeeded"); return new(kind, cancelled, title, message, cancelled && !recovery ? NoticeLevel.Warning : NoticeLevel.Error, failure?.WorkDirectory, error.ToString(), recovery, failure is not null && !recovery ? CleanupProblems(failure) : ""); } @@ -61,7 +61,7 @@ private static string CleanupProblems(BuildFailureException failure) { if (failure.InnerException is not AggregateException { InnerExceptions.Count: > 1 } aggregate) return ""; var reasons = aggregate.InnerExceptions.Skip(1).Select(ErrorText.Reason).Distinct(StringComparer.Ordinal); - return ErrorText.Clip("Some cleanup steps reported problems: " + string.Join(" ", reasons), 320); + return ErrorText.Clip(Loc.Format("Outcome__CleanupProblems", string.Join(" ", reasons)), 320); } public static bool IsCancellation(OperationKind kind, Exception error) => error switch @@ -77,27 +77,28 @@ private static string CleanupProblems(BuildFailureException failure) private static (string Title, string Message) Cancelled(OperationKind kind, bool canResume) => kind switch { - OperationKind.Build => ("Build cancelled.", "Your source was not changed. Working files and logs are kept." + - (canResume ? " Saved progress can be resumed." : "")), - OperationKind.Resume => ("Resume cancelled.", "Saved progress and logs are kept." + (canResume ? " You can resume again." : "")), - OperationKind.Inspect => ("Inspection cancelled.", "The image was not changed."), - OperationKind.Inventory => ("Component list cancelled.", "The image was not changed."), - OperationKind.Download => ("Download cancelled.", "Partial downloads are kept, so you can continue later. No source media was modified."), - OperationKind.Recover => ("Recovery cancelled.", "Run Recover workspace again to finish cleaning up this job."), - OperationKind.Elevate => ("Restart cancelled.", "The app is still running without administrator privileges."), - _ => ("Cancelled.", "Nothing was changed.") + OperationKind.Build => (Loc.Get("Outcome__BuildCancelled__Title"), Loc.Format("Outcome__BuildCancelled__Message", + canResume ? Loc.Get("Outcome__ResumeAvailable") : "")), + OperationKind.Resume => (Loc.Get("Outcome__ResumeCancelled__Title"), Loc.Format("Outcome__ResumeCancelled__Message", + canResume ? Loc.Get("Outcome__ResumeAgain") : "")), + OperationKind.Inspect => (Loc.Get("Outcome__InspectCancelled__Title"), Loc.Get("Outcome__InspectCancelled__Message")), + OperationKind.Inventory => (Loc.Get("Outcome__InventoryCancelled__Title"), Loc.Get("Outcome__InventoryCancelled__Message")), + OperationKind.Download => (Loc.Get("Outcome__DownloadCancelled__Title"), Loc.Get("Outcome__DownloadCancelled__Message")), + OperationKind.Recover => (Loc.Get("Outcome__RecoverCancelled__Title"), Loc.Get("Outcome__RecoverCancelled__Message")), + OperationKind.Elevate => (Loc.Get("Outcome__ElevateCancelled__Title"), Loc.Get("Outcome__ElevateCancelled__Message")), + _ => (Loc.Get("Outcome__Cancelled__Title"), Loc.Get("Outcome__Cancelled__Message")) }; private static (string Title, string Message) Failed(OperationKind kind, string reason, bool canResume) => kind switch { - OperationKind.Build => ("Build failed.", reason + (canResume ? " Saved progress can be resumed." : "")), - OperationKind.Resume => ("Resume failed.", reason + (canResume ? " Saved progress is kept, so you can resume again." : "")), - OperationKind.Inspect => ("The image could not be inspected.", reason), - OperationKind.Inventory => ("The component list could not be loaded.", reason), - OperationKind.Download => ("The download did not complete.", reason), - OperationKind.Recover => ("Recovery did not complete.", reason), - OperationKind.Elevate => ("The app could not restart as administrator.", reason), - _ => ("The operation did not complete.", reason) + OperationKind.Build => (Loc.Get("Outcome__BuildFailed__Title"), reason + (canResume ? Loc.Get("Outcome__ResumeAvailable") : "")), + OperationKind.Resume => (Loc.Get("Outcome__ResumeFailed__Title"), reason + (canResume ? Loc.Get("Outcome__ResumeProgressKept") : "")), + OperationKind.Inspect => (Loc.Get("Outcome__InspectFailed__Title"), reason), + OperationKind.Inventory => (Loc.Get("Outcome__InventoryFailed__Title"), reason), + OperationKind.Download => (Loc.Get("Outcome__DownloadFailed__Title"), reason), + OperationKind.Recover => (Loc.Get("Outcome__RecoverFailed__Title"), reason), + OperationKind.Elevate => (Loc.Get("Outcome__ElevateFailed__Title"), reason), + _ => (Loc.Get("Outcome__GenericFailed__Title"), reason) }; } @@ -117,7 +118,7 @@ internal static class ErrorText public static string Reason(Exception error) { var summary = Summary(Primary(error).Message); - return summary.Length > 0 ? summary : "No further details were reported."; + return summary.Length > 0 ? summary : Loc.Get("Outcome__NoDetails"); } public static string Summary(string? text, int limit = NoticeLimit) @@ -210,9 +211,7 @@ internal static class ResumeSupport { public const string JobFileName = "job.json"; - public const string NoCheckpoint = - "This build has no saved progress to resume. It was started before progress was saved, or it stopped before its image was saved, " + - "so it can't be continued. Nothing was changed, and its logs and files are kept."; + public static string NoCheckpoint => Loc.Get("Resume__NoCheckpoint"); // hasCheckpoint is advisory: it only decides what to offer. The engine verifies the saved work itself. public static ResumeCandidate Inspect(string jobFile, Func hasCheckpoint, string recipeFileName = "recipe.json") @@ -225,18 +224,18 @@ public static ResumeCandidate Inspect(string jobFile, Func hasChec } catch (Exception error) when (error is ArgumentException or NotSupportedException or PathTooLongException) { - return new(jobFile, "", null, "That is not a valid job file path."); + return new(jobFile, "", null, Loc.Get("Resume__InvalidPath")); } if (!Path.GetFileName(file).Equals(JobFileName, StringComparison.OrdinalIgnoreCase)) - return new(file, directory, null, "Choose the job.json file inside the build's tiny11-… folder."); + return new(file, directory, null, Loc.Get("Resume__WrongFile")); if (!hasCheckpoint(directory)) return new(file, directory, null, NoCheckpoint); var recipeFile = Path.Combine(directory, recipeFileName); if (!File.Exists(recipeFile)) - return new(file, directory, null, "This job's saved recipe is missing, so it can't be resumed."); + return new(file, directory, null, Loc.Get("Resume__RecipeMissing")); try { return new(file, directory, BuildRecipe.Read(recipeFile), null); } catch (Exception error) when (error is IOException or UnauthorizedAccessException or InvalidDataException or System.Text.Json.JsonException) { - return new(file, directory, null, "This job's saved recipe could not be read. " + ErrorText.Reason(error)); + return new(file, directory, null, Loc.Format("Resume__RecipeUnreadable", ErrorText.Reason(error))); } } @@ -244,26 +243,28 @@ public static string Describe(BuildRecipe recipe) { var mode = CleanupCatalog.ModeForRecipe(recipe) switch { - BuildMode.Tiny11 => "Tiny11", BuildMode.Tiny11Core => "Tiny11 Core", - _ => recipe.Flavor == BuildFlavor.Core ? "Custom with Core removals" : "Custom" + BuildMode.Tiny11 => Loc.Get("Mode__Tiny11"), BuildMode.Tiny11Core => Loc.Get("Mode__Core"), + _ => Loc.Get(recipe.Flavor == BuildFlavor.Core ? "Resume__Mode__CustomCore" : "Mode__Custom") }; - var compression = recipe.Compression switch + var compression = Loc.Get(recipe.Compression switch { - Compression.Recovery => "Recovery ESD", Compression.Maximum => "Maximum WIM", _ => "Fast WIM" - }; + Compression.Recovery => "Resume__Compression__Recovery", + Compression.Maximum => "Resume__Compression__Maximum", + _ => "Resume__Compression__Fast" + }); var text = new StringBuilder(); - text.AppendLine($"Output: {recipe.OutputPath}"); - text.AppendLine($"Mode: {mode} · {recipe.SelectedOptions.Length} removal groups · {recipe.ExpertActions.Length} individual actions"); - text.AppendLine($"Compression: {compression}"); + text.AppendLine(Loc.Format("Resume__Describe__Output", recipe.OutputPath)); + text.AppendLine(Loc.Format("Resume__Describe__Mode", mode, recipe.SelectedOptions.Length, recipe.ExpertActions.Length)); + text.AppendLine(Loc.Format("Resume__Describe__Compression", compression)); text.AppendLine(); - text.AppendLine("The removal choices from the original build stay as they were. They can't be changed when resuming."); + text.AppendLine(Loc.Get("Resume__Describe__ChoicesFixed")); if (recipe.Flavor == BuildFlavor.Core) - text.AppendLine("Core: no Windows Update, no Microsoft Defender, no recovery environment. Use only in an isolated test VM."); + text.AppendLine(Loc.Get("Resume__Describe__CoreWarning")); if (SetupLabels.Legacy(recipe) is { } legacy) text.AppendLine(legacy + "."); if (recipe.ExpertActions.Length > 0) - text.AppendLine("Individual removals include protected items. The image may not boot."); + text.AppendLine(Loc.Get("Resume__Describe__ExpertWarning")); text.AppendLine(); - text.Append("The saved progress is verified first. If anything does not match, nothing is changed."); + text.Append(Loc.Get("Resume__Describe__Verified")); return text.ToString(); } } @@ -278,12 +279,12 @@ internal static class SetupLabels if (!SetupInstaller.RequiresLegacy(recipe)) return null; var core = recipe.Flavor == BuildFlavor.Core; var payload = recipe.ExpertActions.Any(a => a.Kind == ImageItemKind.ComponentPayload && a.Mode == ExpertActionMode.Remove); - return core && payload ? "Core and component-payload removals" : core ? "Core removals" : - payload ? "component-payload removals" : "the selected removals"; + return Loc.Get(core && payload ? "Setup__LegacyReason__CorePayload" : core ? "Setup__LegacyReason__Core" : + payload ? "Setup__LegacyReason__Payload" : "Setup__LegacyReason__Selected"); } public static string? Legacy(BuildRecipe recipe) => - LegacyReason(recipe) is { } reason ? $"Legacy installer: automatic (needed for {reason})" : null; + LegacyReason(recipe) is { } reason ? Loc.Format("Setup__Legacy", reason) : null; } // "3 of 7 stages done": the count shown over the stage history, read from the same rows the history draws. @@ -297,8 +298,10 @@ public static string Describe(BuildStageBoard board) var done = stages.Count(IsDone); var failed = stages.Count(stage => stage.Operations.Any(o => o.State == BuildOperationState.Failed)); var cancelled = stages.Any(stage => stage.Operations.Any(o => o.State == BuildOperationState.Cancelled)); - var text = done == stages.Length ? $"All {stages.Length} stages done" : $"{done} of {stages.Length} stages done"; - return failed > 0 ? $"{text} · {failed} failed" : cancelled ? text + " · cancelled" : text; + var text = done == stages.Length ? Loc.Format("Stage__Summary__AllDone", stages.Length) : + Loc.Format("Stage__Summary__Done", done, stages.Length); + return failed > 0 ? text + " · " + Loc.Format("Stage__Summary__Failed", failed) : + cancelled ? text + " · " + Loc.Get("Stage__Summary__Cancelled") : text; } } @@ -320,15 +323,15 @@ internal sealed record OperationEnd(OperationKind Kind, RunResult Result, bool R public bool NeedsAttention => RecoveryRequired || Result == RunResult.Failed; public static OperationEnd Succeeded(OperationKind kind) => - new(kind, RunResult.Succeeded, false, "Finished.", "", null, ""); + new(kind, RunResult.Succeeded, false, Loc.Get("Outcome__Finished"), "", null, ""); public static OperationEnd From(OperationOutcome outcome) => new(outcome.Kind, outcome.Cancelled ? RunResult.Cancelled : RunResult.Failed, outcome.RecoveryRequired, outcome.Title, outcome.Message, outcome.WorkDirectory, outcome.Details, outcome.Warning); public static OperationEnd Unknown(OperationKind kind) => - new(kind, RunResult.Failed, false, "The operation did not complete.", - "It ended without saying how, so it is not shown as finished. Check the logs.", null, ""); + new(kind, RunResult.Failed, false, Loc.Get("Outcome__Unknown__Title"), + Loc.Get("Outcome__Unknown__Message"), null, ""); } // One call of RunOperation as seen by whoever may cancel it. A prompt is bound to the run it was opened for, so it can @@ -394,7 +397,7 @@ public CancelRequest RequestCancel() catch (AggregateException failure) { // The token is cancelled all the same, but a step that reacts to it failed. That is shown, never swallowed. - NoteWarning("The cancel was sent, but a step that reacts to it failed: " + ErrorText.Reason(failure)); + NoteWarning(Loc.Format("Cancel__Warning__CallbackFailed", ErrorText.Reason(failure))); } return CancelRequest.Requested; } @@ -417,7 +420,7 @@ public void Settle() end ??= OperationEnd.Unknown(Kind); } try { source.Dispose(); } - catch (Exception error) { NoteWarning("The cancel token could not be released: " + ErrorText.Reason(error)); } + catch (Exception error) { NoteWarning(Loc.Format("Cancel__Warning__TokenFailed", ErrorText.Reason(error))); } finally { settled.TrySetResult(); } } @@ -435,49 +438,25 @@ internal static class CancelPrompts { // The caption of the shell button for every busy operation, exactly as the user named it. What it would cancel is // in its hint (the tooltip and help text), so the caption is the same everywhere. - public const string ButtonLabel = "Cancel safely"; + public static string ButtonLabel => Loc.Get("Cancel__ButtonLabel"); public static CancelCopy For(OperationKind kind) => kind switch { - OperationKind.Build => new("build", "Cancel this build?", - "The build stops at the next safe point. That can take a few minutes, because the image operation that is running is allowed to finish first. " + - "Your source is never changed. The mounted copy is safely unmounted; working files, saved progress and logs are kept.", - "Cancel build", "Keep building", "Stops the build at the next safe point. Your source is never changed.", - "Cancelling the build", "Waiting for the current operation to finish", "The ISO and build report were saved.", true), - OperationKind.Resume => new("resume", "Cancel this resume?", - "The resume stops at the next safe point. That can take a few minutes, because the image operation that is running is allowed to finish first. " + - "The mounted copy is safely unmounted; working files, saved progress and logs are kept, so you can resume again.", - "Cancel resume", "Keep resuming", "Stops the resume at the next safe point. Saved progress is kept.", - "Cancelling the resume", "Waiting for the current operation to finish", "The ISO and build report were saved.", true), - OperationKind.Download => new("download", "Cancel this download?", - "The download stops. The partial file is kept, so you can continue it later. No source media is modified.", - "Cancel download", "Keep downloading", "Stops the download. The partial file is kept.", - "Cancelling the download", "Stopping the download", "The download finished.", false), - OperationKind.Inspect => new("inspection", "Cancel inspecting this image?", - "Inspection stops after the current step. The image is not changed.", - "Cancel inspection", "Continue", "Stops inspecting the image. Nothing is changed.", - "Cancelling the inspection", "Waiting for the current step to finish", "The image was inspected.", false), - OperationKind.Inventory => new("component list", "Cancel listing components?", - "Listing stops at the next safe point. The read-only mount of the image is discarded and the image is not changed.", - "Cancel listing", "Continue", "Stops listing components. The image is not changed.", - "Cancelling the component list", "Waiting for the current operation to finish", "The component list was loaded.", true), - OperationKind.Recover => new("recovery", "Cancel this recovery?", - "Recovery stops at the next safe point. Run Recover workspace again to finish cleaning up this job.", - "Cancel recovery", "Keep recovering", "Stops the recovery at the next safe point.", - "Cancelling the recovery", "Waiting for the current operation to finish", "The workspace was recovered.", false), - OperationKind.Elevate => new("restart", "Cancel the restart?", - "The app keeps running without administrator privileges.", - "Cancel restart", "Continue", "Stops restarting the app as administrator.", - "Cancelling the restart", "Waiting for the current step to finish", "The app was restarted.", false), - OperationKind.Check => new("check", "Cancel the check?", - "The check stops. Nothing has been changed.", - "Cancel check", "Continue", "Stops checking your choices. Nothing is changed.", - "Cancelling the check", "Waiting for the current step to finish", "The check finished.", false), - _ => new("operation", "Cancel this operation?", - "The operation stops after the current step. Nothing has been changed.", - "Cancel", "Continue", "Stops the running operation.", - "Cancelling", "Waiting for the current step to finish", "The operation finished.", false) + OperationKind.Build => Copy("Build", true, "Operation"), + OperationKind.Resume => Copy("Resume", true, "Operation"), + OperationKind.Download => Copy("Download", false, "Download"), + OperationKind.Inspect => Copy("Inspect", false, "Step"), + OperationKind.Inventory => Copy("Inventory", true, "Operation"), + OperationKind.Recover => Copy("Recover", false, "Operation"), + OperationKind.Elevate => Copy("Elevate", false, "Step"), + OperationKind.Check => Copy("Check", false, "Step"), + _ => Copy("Other", false, "Step") }; + + private static CancelCopy Copy(string kind, bool cleansUp, string wait) => new( + Loc.Get($"Cancel__Noun__{kind}"), Loc.Get($"Cancel__Question__{kind}"), Loc.Get($"Cancel__Explanation__{kind}"), + Loc.Get($"Cancel__Confirm__{kind}"), Loc.Get($"Cancel__Keep__{kind}"), Loc.Get($"Cancel__Hint__{kind}"), + Loc.Get($"Cancel__Stopping__{kind}"), Loc.Get($"Cancel__Wait__{wait}"), Loc.Get($"Cancel__Finished__{kind}"), cleansUp); } // Progress events Core may send while it winds a cancelled run down. Until it does, the wait is one honest state. @@ -649,52 +628,51 @@ private void Conclude(bool beforeConfirm) // A problem the cancel itself ran into (a callback that failed) is shown while the stop is still running. private CancelView Stopping() => new(CancelStage.Cancelling, CancelTone.Neutral, copy.Stopping, "", StatusText(), DetailText(), - copy.CleansUp ? "The image tool is never force-stopped, so this can take a few minutes. Do not close the app." : - "Do not close the app until this finishes.", + Loc.Get(copy.CleansUp ? "Cancel__Note__ToolNotStopped" : "Cancel__Note__DoNotClose"), run.Warning, true, true, 0, null, null, false, CancelLink.None, ""); private string StatusText() => phase switch { - CancelPhase.DiscardingMount => "Discarding the working mount", - CancelPhase.SavingLogs => "Saving logs", - _ => percentFinished ? "Finishing the current operation" : copy.Waiting + CancelPhase.DiscardingMount => Loc.Get("Cancel__Status__DiscardingMount"), + CancelPhase.SavingLogs => Loc.Get("Cancel__Status__SavingLogs"), + _ => percentFinished ? Loc.Get("Cancel__Status__Finishing") : copy.Waiting }; private string DetailText() => phase != CancelPhase.Waiting || current.Length == 0 ? "" : - percent is { } value && !percentFinished ? $"Current operation: {current} · {Math.Floor(value):0}%" : $"Current operation: {current}"; + percent is { } value && !percentFinished ? Loc.Format("Cancel__Status__CurrentOperation", current, Math.Floor(value).ToString("0")) : + Loc.Format("Cancel__Status__CurrentOperationNoPercent", current); private CancelView Ended(OperationEnd e) { var tone = e.NeedsAttention ? CancelTone.Error : e.Warning.Length > 0 ? CancelTone.Warning : CancelTone.Done; - var title = e.Title.TrimEnd('.'); + var title = e.Title.TrimEnd('.', '。'); string heading, body, status; if (e.RecoveryRequired) { - heading = title + (e.Result == RunResult.Cancelled ? ", but recovery is needed" : ", and recovery is needed"); + heading = Loc.Format(e.Result == RunResult.Cancelled ? "Cancel__Heading__RecoveryCancelled" : "Cancel__Heading__RecoveryFailed", title); (body, status) = (e.Message, ""); } else if (e.Result == RunResult.Succeeded) { - heading = endedBeforeConfirm ? $"The {copy.Noun} already finished" : "Finished before it could be cancelled"; - body = copy.Finished + (endedBeforeConfirm ? " It finished before it could be cancelled, so nothing was cancelled." : - " The cancellation arrived too late to stop it, so nothing was cancelled."); - status = "Finished."; + heading = endedBeforeConfirm ? Loc.Format("Cancel__Heading__AlreadyFinished", copy.Noun) : Loc.Get("Cancel__Heading__BeforeCancel"); + body = Loc.Format(endedBeforeConfirm ? "Cancel__Body__BeforeCancel" : "Cancel__Body__TooLate", copy.Finished); + status = Loc.Get("Cancel__Status__Finished"); } else { heading = title; body = e.Message; - status = e.Result == RunResult.Cancelled ? copy.CleansUp ? "Cleanup finished." : "Stopped." : ""; + status = e.Result == RunResult.Cancelled ? Loc.Get(copy.CleansUp ? "Cancel__Status__CleanupFinished" : "Cancel__Status__Stopped") : ""; } var link = tone != CancelTone.Done && e.WorkDirectory is not null ? CancelLink.OpenLogs : tone == CancelTone.Error && e.Details.Length > 0 ? CancelLink.CopyDetails : CancelLink.None; // A bar is only ever shown complete for a run that ended cleanly; failures and recovery never wear it. var complete = tone != CancelTone.Error; return new(CancelStage.Ended, tone, heading, body, complete ? status : "", "", - tone != CancelTone.Done && e.WorkDirectory is { } directory ? "Job folder and logs: " + directory : "", + tone != CancelTone.Done && e.WorkDirectory is { } directory ? Loc.Format("Cancel__Footnote__JobFolder", directory) : "", e.Warning, complete, false, complete ? 100 : 0, null, - tone == CancelTone.Error ? "Close" : "Done", true, link, - link == CancelLink.OpenLogs ? "Open logs" : link == CancelLink.CopyDetails ? "Copy details" : ""); + Loc.Get(tone == CancelTone.Error ? "Common__Close" : "Cancel__Button__Done"), true, link, + link == CancelLink.OpenLogs ? Loc.Get("Notice__OpenLogs") : link == CancelLink.CopyDetails ? Loc.Get("Notice__CopyDetails") : ""); } } diff --git a/src/Tiny11Builder.App/ResourceProvider.cs b/src/Tiny11Builder.App/ResourceProvider.cs new file mode 100644 index 00000000..7f826b5f --- /dev/null +++ b/src/Tiny11Builder.App/ResourceProvider.cs @@ -0,0 +1,18 @@ +using Microsoft.Windows.ApplicationModel.Resources; + +namespace Tiny11Builder.App; + +internal static class ResourceProvider +{ + // ResourceManager reads resources.pri beside the executable. A missing key returns null so Loc + // can show the key itself, which is visible during development and caught by the resource tests. + public static void Install() + { + var loader = new ResourceLoader(); + Loc.Use(key => + { + try { return loader.GetString(key); } + catch (Exception) { return null; } + }); + } +} diff --git a/src/Tiny11Builder.App/Strings/en-US/Resources.resw b/src/Tiny11Builder.App/Strings/en-US/Resources.resw new file mode 100644 index 00000000..5073fcd8 --- /dev/null +++ b/src/Tiny11Builder.App/Strings/en-US/Resources.resw @@ -0,0 +1,627 @@ + + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Build + Build again + Build another + Continue + Next + Recover workspace + Resume again + Resume build + Continue a stopped build from the progress it saved. + Continue this build from the progress it saved. + Review + Start + Tiny11 Builder + Commit image + Compress image + Export and mount + Create ISO + Prepare media + Remove components + Verify output + Building + To stop, use Cancel safely at the top right. It stops at the next safe point. Your source is never changed; working files, saved progress and logs are kept. + Cancellation requested. {0}… + Cancellation requested. Waiting for the current operation to finish… + {0} cancelled + Cancelling… + Current operation + The selected edition will be copied, modified, compressed and saved as a bootable ISO. + The legacy installer is turned on automatically for {0}. + Build details + Expand a stage to see each step and any error details. + Finished with warnings + Build + Operation history + ISO created + No changes have been made. + Build + Resume + Open logs and report + Open output folder + Output ISO + Overall progress + Preparing image tools… + {0} in total + {0} GiB ISO + {0} minutes + {0} more in the build log + Redundant data removed: {0} MiB logical · Setup payload omitted: {1} MiB + Resumed build + The ISO and build report have been saved. Test the ISO in a VM before use. + The ISO was saved with warnings. See the skipped work and other details below. + Detailed stage timings are saved with the build report. + Standard build + Turbo build + The ISO was saved with warnings. See the warnings on the page. + Resuming + SHA-256 + {0} failed. Expand this stage for error details. + Advanced payloads + Commit + Component cleanup + Compress + Copy media + Core pruning + Create ISO + Custom servicing + Export edition + Inventory + Mount + .NET 3.5 + Optional features + Prepare + Redundant data + Remove apps + Check saved progress + Setup image + Verify output + Windows settings + Complete + Stage {0} of {1} + All {0} stages done + 0 of 7 stages done + {0} stopped + If Tiny11 Builder saved you time, you can support its development. + Test the ISO in a VM before installing Windows. + Waiting to start + {0} It finished before it could be cancelled, so nothing was cancelled. + {0} The cancellation arrived too late to stop it, so nothing was cancelled. + Cancel safely + Done + Cancel build + Cancel check + Cancel download + Cancel restart + Cancel inspection + Cancel listing + Cancel + Cancel recovery + Cancel resume + The build stops at the next safe point. That can take a few minutes, because the image operation that is running is allowed to finish first. Your source is never changed. The mounted copy is safely unmounted; working files, saved progress and logs are kept. + The check stops. Nothing has been changed. + The download stops. The partial file is kept, so you can continue it later. No source media is modified. + The app keeps running without administrator privileges. + Inspection stops after the current step. The image is not changed. + Listing stops at the next safe point. The read-only mount of the image is discarded and the image is not changed. + The operation stops after the current step. Nothing has been changed. + Recovery stops at the next safe point. Run Recover workspace again to finish cleaning up this job. + The resume stops at the next safe point. That can take a few minutes, because the image operation that is running is allowed to finish first. The mounted copy is safely unmounted; working files, saved progress and logs are kept, so you can resume again. + The ISO and build report were saved. + The check finished. + The download finished. + The app was restarted. + The image was inspected. + The component list was loaded. + The operation finished. + The workspace was recovered. + The ISO and build report were saved. + Job folder and logs: {0} + The {0} already finished + Finished before it could be cancelled + {0}, but recovery is needed + {0}, and recovery is needed + Stops the build at the next safe point. Your source is never changed. + Stops checking your choices. Nothing is changed. + Stops the download. The partial file is kept. + Stops restarting the app as administrator. + Stops inspecting the image. Nothing is changed. + Stops listing components. The image is not changed. + Stops the running operation. + Stops the recovery at the next safe point. + Stops the resume at the next safe point. Saved progress is kept. + Keep building + Continue + Keep downloading + Continue + Continue + Continue + Continue + Keep recovering + Keep resuming + Do not close the app until this finishes. + The image tool is never force-stopped, so this can take a few minutes. Do not close the app. + build + check + download + restart + inspection + component list + operation + recovery + resume + Cancel this build? + Cancel the check? + Cancel this download? + Cancel the restart? + Cancel inspecting this image? + Cancel listing components? + Cancel this operation? + Cancel this recovery? + Cancel this resume? + Cleanup finished. + Current operation: {0} · {1}% + Current operation: {0} + Discarding the working mount + Finished. + Finishing the current operation + Saving logs + Stopped. + Cancelling the build + Cancelling the check + Cancelling the download + Cancelling the restart + Cancelling the inspection + Cancelling the component list + Cancelling + Cancelling the recovery + Cancelling the resume + Stopping the download + Waiting for the current operation to finish + Waiting for the current step to finish + The cancel was sent, but a step that reacts to it failed: {0} + The cancel token could not be released: {0} + Advanced / original builder + Bundled apps + Optional features + Windows settings + Suppress consumer content, Settings suggestions, Explorer promotions and welcome tips. + Ads, recommendations and sponsored apps + Remove the Camera app, not camera drivers. + Camera + Regular mode preserves servicing. Core additionally resets the update base before its irreversible component pruning. + Component cleanup + Remove the bundled video editor. + Clipchamp + Remove preinstalled Copilot apps. Keep shared runtimes and shell components. + Copilot apps + Apply policies to disable Copilot and Recall. Support varies by build and edition. + Disable Copilot and Recall + Remove Dev Home and Power Automate Desktop. + Dev Home and Power Automate + Prevent automatic BitLocker device encryption; do not use if you require encryption by default. + Disable automatic device encryption + Delete Edge and WebView2 files. Apps that require WebView2 may stop working. Updates may reinstall Edge. + Remove Edge and WebView2 + Suppress first-run, sidebar and recommendations. Preserve Edge and WebView2 for app compatibility. + Edge first-run and promotional UI + Remove Feedback Hub, Get Help, and introductory apps. + Feedback, tips and help + Set CPU, RAM, TPM, storage and Secure Boot bypass values in setup and Windows. Unsupported hardware may not work. + Bypass setup hardware requirements + Remove optional language capabilities in every image language; retain basic language support. + Speech, OCR and handwriting payloads + Remove WordPad, Steps Recorder, Math Recognizer, legacy Media Player and extended wallpapers where installed. + Retired accessories and wallpaper payloads + Disable the retired Internet Explorer optional feature where present. + Legacy Internet Explorer + Remove retired Maps and Mixed Reality clients. + Maps and Mixed Reality + Remove bundled media playback apps; codecs and shared frameworks remain. + Media Player and Movies + Remove packaged feed apps; local Windows Search is preserved. + News, weather and Bing apps + Remove Intel management status and Dolby promotional applications, not drivers. + OEM promotional apps + Remove Office Hub and Office push-notification promotional apps. + Microsoft 365 promotions + Remove both architecture-specific OneDrive installers from the offline image. + Remove OneDrive setup binaries + Disable OneDrive sync by policy. OneDrive binaries are retained; use this only if you do not need sync. + OneDrive sync and backup prompts + Suppress privacy setup and post-sign-in setup suggestions without creating accounts or accepting the EULA. + Setup privacy and finish-setting-up nags + Remove Outlook and the retired Mail / Calendar apps. + New Outlook and Mail + Remove Paint, 3D Viewer and OneNote. Leave unchecked to keep them. + Paint, 3D Viewer and OneNote + Remove phone integration apps. Cross-device features may stop working. + Phone Link and cross-device apps + Keep required diagnostic data. Disable advertising ID, personalized suggestions and input collection. + Optional diagnostics and personalization + Remove Microsoft's remote-assistance client. + Quick Assist + Disable and remove Recall's payload only if this image exposes it to DISM. + Recall optional feature + Stop reserving disk space for updates. This can leave less space available during servicing. + Disable reserved storage + Turn off web suggestions, cloud search and dynamic search highlights; keep local search. + Web search and search highlights + Remove optional organizer and communication apps. + People, To Do, Family and Skype + Remove bundled games. + Solitaire + Remove personal Teams clients. + Teams + Remove Terminal. PowerShell and Command Prompt remain. + Windows Terminal + Remove optional personal utilities. + Clock, Recorder and Sticky Notes + Disable Widgets by policy and hide its taskbar entry. + Widgets and taskbar feeds + Disable enterprise Work Folders sync and remove its payload. + Work Folders + Remove Xbox apps and Game Bar. Gaming sign-in and recording may be unavailable. + Xbox and gaming overlays + Disable optional XPS printing and remove its payload. + XPS printing + All + Back + Build activity + Build log + Cancel safely + Close + Component type + Current operation progress + Dark + Download progress + Image components + Keep + Light + Off + Omit + On + Overall progress + Remove + Search components + Start + System + Working + Clear custom selections + I accept the risk of an unbootable, unserviceable or insecure custom image. + Individual removals add to the selected groups. To keep a component, leave both its group and its individual entry unchecked. + List the image's apps, packages, features, services and component files, including protected items. Removing a service deletes its registry entry, not shared executables. Protected packages may refuse removal. + List all components… + Advanced removals + Removing protected or boot-critical items can create an unbootable or insecure image. Direct payload removal bypasses servicing. No dependency solver or boot guarantee is provided. + Unsupported removals + Essentials only. For test VMs. + Choose individual removals. + Original removals. Keep update support. + Core limitations accepted for this build. + Core limitations have not been accepted. + You will be asked to accept Core's limitations before continuing. + Core deletes component files and WinRE, and disables Windows Update and Defender. You cannot add updates, languages or features afterward. Test the image in a VM. + Core removes update and recovery support. + {0} removal groups selected · {1} advanced actions + {0} components listed · {1} advanced actions selected. + Apply Core removals + Keep servicing, recovery and Defender + Remove servicing, recovery and Defender + Show choices + Checked groups are removed or disabled. Unchecked groups are kept. For individual items, use the component list below. + Components + Action for {0} + Application + Disable + Protected / potentially boot-critical + Remove + Choose Tiny11, Tiny11 Core, or select your own removals with Custom. Every change is recorded in the build log. + Tiny11 Core removals, including irreversible component pruning. Choose Custom to change individual removals. + Start with nothing selected, then choose what to remove or disable. You can also list individual components, including protected items. + Original Tiny11 removals, including Edge/WebView2 and OneDrive binaries. Windows Update, Defender and recovery are kept. Choose Custom to change individual removals. + Not selected + Selected + Use Tiny11 Core + Use Custom + Use Tiny11 + Build + Output: {0} + +Mode: {1} +Removal groups: {2} +Individual actions: {3}{4} + Create this offline Windows image? + Cancel + I agree + No Windows Update +No Microsoft Defender +No recovery environment +No added languages or features + +Use only in an isolated test VM. +The image may not boot. + Core is not serviceable + This inventory includes boot drivers, security, licensing, system apps and component payloads. Removing them can make Windows unbootable, insecure or impossible to update. Protected packages may refuse removal. Direct payload removal is unsupported. + +Continue only for isolated testing, with backups and a known-good installer. + Inspect components + Protected components are included + Recover discards a job's working copy, and it does not do that for a job with saved progress. Resume continues the build from that progress instead. + +Nothing has been changed. + Resume instead + This job has saved progress + Select only job.json from the interrupted tiny11 job. This discards that job's mounted working copy and removes its staged media. Its logs, recipe, report and your source ISO are kept. A job whose image was already committed has saved progress and is never discarded here; use Resume build for it. Active builds cannot be recovered. + Discard working copy + Discard this interrupted workspace? + Resume + Resume this build? + The browser could not open Microsoft's image-tools page. + Inspect the selected Windows image first. + The suggested download page is not an approved Microsoft page. + The application path could not be found. + The saved session is empty. + This support link has no valid destination. + Image recipe + ISO image + No image selected + Language + The app could not restart. + The new language is saved. Restart Tiny11 Builder to use it. + Restart to apply the language. + The app will keep its current language until settings can be written. + The language could not be saved. + System default + Tiny11 Core + Custom + Tiny11 + Appearance + Build settings + BUILD STEPS + Create image + Remove components + Review + SELECTED IMAGE + Source files are not modified. + Upstream project ↗ + Welcome + Windows image + Close and reopen Tiny11Builder.exe with administrator approval to inspect its editions. + Close and reopen Tiny11Builder.exe, then approve the Windows administrator prompt. + Close and reopen Tiny11Builder.exe with administrator approval to recover a workspace. + Administrator privileges required. + Close and reopen Tiny11Builder.exe with administrator approval, then choose Resume build. Nothing was changed. + Your browser could not open {0}. No payment was made. + The selected card does not identify a valid build mode. + Build mode could not be selected. + The cached ISO has changed. It has not been accepted. + Cancellation progress + This build can't be resumed. + It is being cancelled. Wait for it to stop before closing. + Choose {0} and wait for the image operation to finish before closing. + An operation is still running. + Existing selections were preserved. Check the individual items you want to remove or disable. + Component list loaded. + The installed tools could list this image, but building it requires newer Windows ADK Deployment Tools. Install them and reopen the app before building. + Component list loaded read-only. + Copied + Copy details + Could not copy + Could not open the download page. + Could not open logs + SHA-256: {0} + Downloaded {0}. SHA-256: {1} + Folder could not be opened. + Windows high contrast is on, so colors follow your system settings. + {0} editions found. {1} is selected. + Image inspected. + Image selected. + Select an edition before listing its components. + Inspect an image first. + {0} no longer exists. + The job folder is gone. + Open logs + Open Microsoft download page + Get image tools + Previously downloaded ISO verified. + Inspect the source again. Core/expert risk acknowledgements must be renewed. + Recipe loaded. + The recipe is not ready. + It contains selections and local paths, not downloaded media or credentials. + Recipe saved. + Restart now + The output file already exists. Choose a new file name to continue. + Resume not started. + Session could not be restored. + Inspect the image again before continuing. No build starts automatically. + Session restored. + Support options could not be opened. + Could not open the support page + Choose a Windows 11 client image, 22H2 or newer, in x64 or ARM64 format. + This edition is not supported. + The interrupted working copy was discarded. Your source and logs were preserved. + Workspace recovered. + Your source was not changed. Working files and logs are kept.{0} + Build cancelled. + Build failed. + Nothing was changed. + Cancelled. + Some cleanup steps reported problems: {0} + Partial downloads are kept, so you can continue later. No source media was modified. + Download cancelled. + The download did not complete. + The app is still running without administrator privileges. + Restart cancelled. + The app could not restart as administrator. + Finished. + The operation did not complete. + The image was not changed. + Inspection cancelled. + The image could not be inspected. + The image was not changed. + Component list cancelled. + The component list could not be loaded. + No further details were reported. + Run Recover workspace again to finish cleaning up this job. + Recovery cancelled. + Recovery did not complete. + The job's mounted copy could not be discarded. Use Recover workspace and choose this job's job.json. + You can resume again. + Saved progress can be resumed. + Saved progress and logs are kept.{0} + Resume cancelled. + Resume failed. + Saved progress is kept, so you can resume again. + It ended without saying how, so it is not shown as finished. Check the logs. + The operation did not complete. + Fast WIM + Maximum WIM + Recovery ESD + The removal choices from the original build stay as they were. They can't be changed when resuming. + Compression: {0} + Core: no Windows Update, no Microsoft Defender, no recovery environment. Use only in an isolated test VM. + Individual removals include protected items. The image may not boot. + Mode: {0} · {1} removal groups · {2} individual actions + Output: {0} + The saved progress is verified first. If anything does not match, nothing is changed. + That is not a valid job file path. + Custom with Core removals + This build has no saved progress to resume. It was started before progress was saved, or it stopped before its image was saved, so it can't be continued. Nothing was changed, and its logs and files are kept. + This job's saved recipe is missing, so it can't be resumed. + This job's saved recipe could not be read. {0} + Choose the job.json file inside the build's tiny11-… folder. + Selected removals and full recipe + Review + Windows image + Check your image, removal set and output before building. + Load recipe + No image selected + Clean after build + Compact installation: {0} + {0} compression + Default + Included + Working files: {0} + Local-account setup: {0} + .NET 3.5: {0} + Not included + .NET setup payload: {0} + Preferred + Redundant data: {0} + Installation options + Turbo mode: {0} + Output and workspace + ISO +{0} + +Workspace +{1} + No servicing, recovery or Defender. + Custom removals may affect servicing, recovery or security. + {0} removal groups · {1} individual actions + Windows Update, recovery and Defender retained. + Save recipe + Some policies apply only to particular editions or builds. Feature updates can reinstall apps. Local-account and hardware bypasses are best effort. Test the resulting ISO before relying on it. + Build-dependent settings + Install Windows using compressed system files to reduce disk usage. + Compact installation + Fast WIM (fastest build, larger ISO) + Installation-image compression + Maximum WIM (easier to service later) + Recovery ESD (smaller, slower to compress) + Files and workspace + Build settings + Retain the modified installation media after the ISO is saved. Uses more disk space. + Keep working files + Choose where to build and how to compress your image. + Prefer local sign-in during setup. Some newer builds may still request a Microsoft account. + Local-account setup + Include support for older apps. Required source files must be present in the ISO. + .NET Framework 3.5 + Leave sources\sxs out of the ISO. You can still include .NET 3.5 now; enabling it later requires separate matching media or Windows Update. Core cannot add it later. + Omit .NET 3.5 setup payload + Choose output + New output ISO file + Remove downloaded update caches, archived servicing logs, temporary files and crash reports. Pending servicing data, drivers and installers are kept. + Remove redundant image data + Image and ISO tools are prepared automatically. + Image and ISO tools ready. + Batch feature and capability removals and reduce log overhead. Core and Fast WIM builds use a faster working image. Uses more CPU and temporary space; final compression stays as selected. + Turbo mode + Choose workspace + Workspace (NTFS, typically 40 to 70 GiB free) + Legacy installer: automatic (needed for {0}) + Core removals + Core and component-payload removals + component-payload removals + the selected removals + Build mode: {0} + Official Microsoft ISO · {0} · {1} + Official Microsoft ISO. Architecture and language selected automatically. + Or use an ISO you already have. + Download latest Windows 11 + Windows image + Download the latest Windows 11 from Microsoft, or select an ISO or extracted installation folder. Inspect the image to select an edition. + Choose folder + Choose ISO + Edition to build + Inspect image + Windows ISO or installation-media folder + Choose a Windows 11 image + Local installation media + Microsoft ARM64 downloads ↗ + Download ISO / resume partial + Published SHA-256 (recommended) + Copy from Microsoft's Verify your download section + Other official downloads + Official Insider / Canary ISO selection ↗ + Choose a language on Microsoft's download page, then paste the ISO download link here. Links usually expire after 24 hours. + Microsoft direct ISO link + Microsoft x64 downloads ↗ + All {0} stages done + cancelled + {0} of {1} stages done + {0} failed + Support development + Optional monthly support or one-time donation. All app features remain free. + Optional support. Opens support choices, not a payment. + Links open in your browser. Nothing is charged or subscribed to in the app. + Monthly support + Help keep the project maintained over time. + Support monthly on GitHub Sponsors + One-time donation + A one-off thank-you, with no commitment. + Donate once on Ko-fi + Prefer another service? + Patreon + PayPal + Help fund development time, testing across Windows releases and fixes for reported problems. All app features remain free. + Support Tiny11 Builder + Build modes + Windows 11 22H2 or newer · x64 and ARM64 · Insider / Canary + For test VMs only + Remove update and recovery support and disable Defender. + Protected items included + Select individual apps, components and services to remove. + Create a customized Windows 11 ISO. Select an image, choose what to remove, and build the ISO. + Select Tiny11 Core and choose a Windows ISO. + Select Custom and choose a Windows ISO. + Select Tiny11 and choose a Windows ISO. + Test Core and custom images in a VM before use. + Update support retained + Use the original Tiny11 removals. Keep Windows Update and recovery. + Welcome + Checking your choices… + Checking the downloaded ISO… + Downloading from Microsoft… + Finding the latest Windows 11… + Inspecting the image… + Listing image components… + Recovering workspace… + diff --git a/src/Tiny11Builder.App/Strings/zh-Hans/Resources.resw b/src/Tiny11Builder.App/Strings/zh-Hans/Resources.resw new file mode 100644 index 00000000..3c8cc1e8 --- /dev/null +++ b/src/Tiny11Builder.App/Strings/zh-Hans/Resources.resw @@ -0,0 +1,627 @@ + + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + 生成 + 重新生成 + 再生成一个 + 继续 + 下一步 + 恢复工作区 + 再次继续 + 继续生成 + 从已保存的进度继续先前停止的生成。 + 从已保存的进度继续此生成。 + 检阅 + 开始 + Tiny11 Builder + 认可映像 + 压缩映像 + 导出与装载 + 创建 ISO + 准备媒体 + 移除组件 + 验证输出 + 正在生成 + 若要停止,请使用右上角的「安全取消」。它会在下一个安全点停止。你的源媒体绝不会被更改;工作文件、已保存的进度与日志都会保留。 + 已要求取消。{0}… + 已要求取消。正在等待当前操作完成… + 已取消{0} + 正在取消… + 当前操作 + 选择的版本会被复制、修改、压缩,并保存为可开机的 ISO。 + 旧版安装程序会自动打开,因为需要它来处理{0}。 + 生成详细信息 + 展开阶段以查看每个步骤与任何错误详细信息。 + 完成,但有警告 + 生成 + 操作历史 + 已创建 ISO + 尚未进行任何更改。 + 生成 + 继续生成 + 打开日志与报告 + 打开输出文件夹 + 输出 ISO + 整体进度 + 正在准备映像工具… + 总计 {0} + {0} GiB ISO + {0} 分钟 + 另有 {0} 项在生成日志中 + 已移除多余数据:{0} MiB 逻辑大小 · 已省略安装承载内容:{1} MiB + 继续的生成 + ISO 与生成报告已保存。使用前,请先在 VM 中测试 ISO。 + ISO 已保存,但有警告。请查看下方略过的工作与其他详细信息。 + 详细的阶段计时会保存在生成报告中。 + 标准生成 + Turbo 生成 + ISO 已保存,但有警告。请查看页面上的警告。 + 正在继续生成 + SHA-256 + {0} 失败。展开此阶段以查看错误详细信息。 + 高级承载内容 + 认可 + 组件清理 + 压缩 + 复制媒体 + Core 剪除 + 创建 ISO + 自定义服务 + 导出版本 + 清查 + 装载 + .NET 3.5 + 可选功能 + 准备 + 多余数据 + 移除应用 + 检查已保存的进度 + 设置映像 + 验证输出 + Windows 设置 + 完成 + 第 {0} 阶段,共 {1} 阶段 + 全部 {0} 个阶段已完成 + 已完成 0/7 个阶段 + {0}已停止 + 如果 Tiny11 Builder 为你节省了时间,你可以赞助它的开发。 + 安装 Windows 前,请先在 VM 中测试 ISO。 + 等待开始 + {0}它在可取消之前已完成,因此未取消任何内容。 + {0}取消要求太晚才送达,因此未取消任何内容。 + 安全取消 + 完成 + 取消生成 + 取消检查 + 取消下载 + 取消重新启动 + 取消检查 + 取消列出 + 取消 + 取消恢复 + 取消继续生成 + 生成会在下一个安全点停止。这可能需要几分钟,因为当前正在运行的映像操作会先被允许完成。你的源媒体绝不会被更改。已装载的副本会安全卸载;工作文件、已保存的进度与日志都会保留。 + 检查会停止。尚未更改任何内容。 + 下载会停止。未完成的文件会保留,因此你可以稍后继续。不会修改任何来源媒体。 + 应用会继续在没有系统管理员权限的情况下运行。 + 检查会在当前步骤之后停止。映像不会被更改。 + 列出会在下一个安全点停止。映像的只读装载会被丢弃,映像不会被更改。 + 操作会在当前步骤之后停止。尚未更改任何内容。 + 恢复会在下一个安全点停止。请再次运行「恢复工作区」,以完成此工作的清理。 + 继续生成会在下一个安全点停止。这可能需要几分钟,因为当前正在运行的映像操作会先被允许完成。已装载的副本会安全卸载;工作文件、已保存的进度与日志都会保留,因此你可以再次继续。 + ISO 与生成报告已保存。 + 检查已完成。 + 下载已完成。 + 应用已重新启动。 + 映像已检查。 + 组件列表已加载。 + 操作已完成。 + 工作区已恢复。 + ISO 与生成报告已保存。 + 工作文件夹与日志:{0} + {0}已完成 + 在可取消之前已完成 + {0},但需要恢复 + {0},且需要恢复 + 会在下一个安全点停止生成。你的源媒体绝不会被更改。 + 会停止检查你的选择。不会更改任何内容。 + 会停止下载。未完成的文件会保留。 + 会停止以系统管理员身分重新启动应用。 + 会停止检查映像。不会更改任何内容。 + 会停止列出组件。映像不会被更改。 + 会停止运行中的操作。 + 会在下一个安全点停止恢复。 + 会在下一个安全点停止继续生成。已保存的进度会保留。 + 继续生成 + 继续 + 继续下载 + 继续 + 继续 + 继续 + 继续 + 继续恢复 + 继续生成 + 在此完成之前,请勿关闭应用。 + 映像工具绝不会被强制停止,因此这可能需要几分钟。请勿关闭应用。 + 生成 + 检查 + 下载 + 重新启动 + 检查 + 组件列表 + 操作 + 恢复 + 继续生成 + 要取消此生成吗? + 要取消检查吗? + 要取消此下载吗? + 要取消重新启动吗? + 要取消检查此映像吗? + 要取消列出组件吗? + 要取消此操作吗? + 要取消此恢复吗? + 要取消此继续生成吗? + 清理已完成。 + 当前操作:{0} · {1}% + 当前操作:{0} + 正在丢弃工作装载 + 已完成。 + 正在完成当前操作 + 正在保存日志 + 已停止。 + 正在取消生成 + 正在取消检查 + 正在取消下载 + 正在取消重新启动 + 正在取消检查 + 正在取消组件列表 + 正在取消 + 正在取消恢复 + 正在取消继续生成 + 正在停止下载 + 正在等待当前操作完成 + 正在等待当前步骤完成 + 已送出取消,但对其作出反应的步骤失败:{0} + 无法释放取消权杖:{0} + 高级/原始生成器 + 随附应用 + 可选功能 + Windows 设置 + 抑制消费者内容、设置建议、文件总管促销与欢迎提示。 + 广告、建议与赞助应用 + 移除相机应用,而非相机驱动程序。 + 相机 + 一般模式会保留服务能力。Core 会在其不可逆的组件剪除之前,另外重置更新基底。 + 组件清理 + 移除随附的影片编辑器。 + Clipchamp + 移除预先安装的 Copilot 应用。保留共用运行阶段与壳层组件。 + Copilot 应用 + 套用原则以禁用 Copilot 与 Recall。支持程度因内部版本与版本而异。 + 禁用 Copilot 与 Recall + 移除 Dev Home 与 Power Automate Desktop。 + Dev Home 与 Power Automate + 防止自动 BitLocker 设备加密;如果你默认需要加密,请勿使用。 + 禁用自动设备加密 + 删除 Edge 与 WebView2 文件。需要 WebView2 的应用可能停止运作。更新可能会重新安装 Edge。 + 移除 Edge 与 WebView2 + 抑制首次运行、侧边栏与建议。保留 Edge 与 WebView2 以维持应用兼容性。 + Edge 首次运行与促销界面 + 移除意见反应中枢、取得协助与入门应用。 + 意见反应、提示与说明 + 在安装程序与 Windows 中设置 CPU、RAM、TPM、存储设备与安全开机的略过值。不受支持的硬件可能无法运作。 + 略过安装硬件需求 + 移除每个映像语言中的可选语言能力;保留基本语言支持。 + 语音、OCR 与手写承载内容 + 在已安装的情况下,移除 WordPad、步骤收录程序、数学辨识器、旧版媒体播放器与扩充桌布。 + 已淘汰的附属应用与桌布承载内容 + 在存在的情况下,禁用已淘汰的 Internet Explorer 可选功能。 + 旧版 Internet Explorer + 移除已淘汰的地图与混合实境客户端。 + 地图与混合实境 + 移除随附的媒体播放应用;编解码器与共用架构会保留。 + 媒体播放器与电影 + 移除封装的信息摘要应用;保留本机 Windows 搜寻。 + 新闻、天气与 Bing 应用 + 移除 Intel 管理状态与 Dolby 促销应用,而非驱动程序。 + OEM 促销应用 + 移除 Office Hub 与 Office 推播通知促销应用。 + Microsoft 365 促销 + 从离线映像中移除两种架构专用的 OneDrive 安装程序。 + 移除 OneDrive 安装二进位档 + 透过原则禁用 OneDrive 同步。OneDrive 二进位档会保留;仅在你不需要同步时使用。 + OneDrive 同步与备份提示 + 抑制隐私设置与登录后的设置建议,而不创建账户或接受 EULA。 + 安装隐私与完成设置提示 + 移除 Outlook 与已淘汰的邮件/行事历应用。 + 新版 Outlook 与邮件 + 移除小画家、3D 检视器与 OneNote。取消勾选即可保留。 + 小画家、3D 检视器与 OneNote + 移除手机整合应用。跨设备功能可能停止运作。 + 手机链接与跨设备应用 + 保留必需的诊断数据。禁用广告识别码、个性化建议与输入收集。 + 可选诊断与个性化 + 移除 Microsoft 的远端协助客户端。 + 快速小帮手 + 仅在此映像向 DISM 显示 Recall 时,才禁用并移除其承载内容。 + Recall 可选功能 + 停止为更新保留磁盘空间。这可能会在服务期间留下较少的可用空间。 + 禁用保留存储空间 + 关闭网页建议、云端搜寻与动态搜寻醒目提示;保留本机搜寻。 + 网页搜寻与搜寻醒目提示 + 移除可选的整理与通讯应用。 + 人员、To Do、Family 与 Skype + 移除随附的游戏。 + 接龙 + 移除个人版 Teams 客户端。 + Teams + 移除 Terminal。PowerShell 与命令提示字元会保留。 + Windows Terminal + 移除可选的个人小工具。 + 时钟、录音机与便笺 + 透过原则禁用小工具,并隐藏其工作列项目。 + 小工具与工作列摘要 + 禁用企业工作文件夹同步并移除其承载内容。 + 工作文件夹 + 移除 Xbox 应用与游戏列。游戏登录与录制功能可能无法使用。 + Xbox 与游戏覆盖层 + 禁用可选的 XPS 列印并移除其承载内容。 + XPS 列印 + 全部 + 返回 + 生成活动 + 生成日志 + 安全取消 + 关闭 + 组件类型 + 当前操作进度 + 深色 + 下载进度 + 映像组件 + 保留 + 浅色 + 关 + 省略 + 开 + 整体进度 + 移除 + 搜寻组件 + 开始 + 系统 + 处理中 + 清除自定义选择项目 + 我接受自定义映像可能无法开机、无法服务或不安全的风险。 + 个别移除项目会加入已选择的群组。若要保留某个组件,请同时取消勾选其群组与个别项目。 + 列出映像的应用、包、功能、服务与组件文件,包括受保护项目。移除服务会删除其登录项目,而非共用运行档。受保护的包可能会拒绝移除。 + 列出所有组件… + 高级移除 + 移除受保护或开机关键项目,可能导致映像无法开机或不安全。直接移除承载内容会略过服务机制。不提供相依性解析或开机保证。 + 不支持的移除项目 + 仅保留必需项目。供测试 VM 使用。 + 选择个别移除项目。 + 原始移除项目。保留更新支持。 + 已接受此生成的 Core 限制。 + 尚未接受 Core 限制。 + 继续之前,系统会要求你接受 Core 的限制。 + Core 会删除组件文件与 WinRE,并禁用 Windows Update 与 Defender。之后将无法新增更新、语言或功能。请先在 VM 中测试映像。 + Core 会移除更新与恢复支持。 + 已选择 {0} 个移除群组 · {1} 个高级动作 + 已列出 {0} 个组件 · 已选择 {1} 个高级动作。 + 套用 Core 移除项目 + 保留服务、恢复与 Defender + 移除服务、恢复与 Defender + 显示选项 + 勾选的群组会被移除或禁用。未勾选的群组会保留。若要处理个别项目,请使用下方的组件列表。 + 组件 + {0} 的动作 + 应用 + 禁用 + 受保护/可能为开机关键 + 移除 + 选择 Tiny11、Tiny11 Core,或使用“自定义”选择自己的移除项目。每项更改都会记录在生成日志中。 + Tiny11 Core 移除项目,包括不可逆的组件剪除。选择自定义以更改个别移除项目。 + 从未选择任何项目开始,再选择要移除或禁用的内容。你也可以列出个别组件,包括受保护项目。 + 原始 Tiny11 移除项目,包括 Edge/WebView2 与 OneDrive 二进位档。保留 Windows Update、Defender 与恢复。选择自定义以更改个别移除项目。 + 未选择 + 已选择 + 使用 Tiny11 Core + 使用自定义 + 使用 Tiny11 + 生成 + 输出:{0} + +模式:{1} +移除群组:{2} +个别动作:{3}{4} + 要创建此离线 Windows 映像吗? + 取消 + 我同意 + 没有 Windows Update +没有 Microsoft Defender +没有恢复环境 +无法新增语言或功能 + +仅可在隔离的测试 VM 中使用。 +映像可能无法开机。 + Core 无法提供服务 + 此清查包含开机驱动程序、安全、授权、系统应用与组件承载内容。移除它们可能导致 Windows 无法开机、不安全或无法更新。受保护的包可能会拒绝移除。不支持直接移除承载内容。 + +仅在隔离测试时继续,并备份及准备已知良好的安装程序。 + 检查组件 + 包含受保护的组件 + 恢复会丢弃工作的副本,但不会对已保存进度的工作这样做。请改用继续生成,从该进度继续。 + +尚未更改任何内容。 + 改为继续 + 此工作有已保存的进度 + 请只选择中断的 tiny11 工作中的 job.json。这会丢弃该工作已装载的副本,并移除其临时媒体。其日志、配方、报告与你的来源 ISO 都会保留。映像已确认的工作会有已保存的进度,绝不会在此丢弃;请对它使用继续生成。无法恢复进行中的生成。 + 丢弃工作副本 + 要丢弃此中断的工作区吗? + 继续 + 要继续此生成吗? + 浏览器无法打开 Microsoft 的映像工具页面。 + 请先检查选择的 Windows 映像。 + 建议的下载页面不是已批准的 Microsoft 页面。 + 找不到应用路径。 + 存储的工作阶段是空的。 + 此赞助链接没有有效的目的地。 + 映像配方 + ISO 映像 + 未选择映像 + 语言 + 无法重新启动应用。 + 新语言已保存。请重新启动 Tiny11 Builder 以套用。 + 重新启动以套用语言。 + 在设置可以写入之前,应用会维持当前的语言。 + 无法存储语言。 + 系统默认 + Tiny11 Core + 自定义 + Tiny11 + 外观 + 生成设置 + 生成步骤 + 创建映像 + 移除组件 + 检阅 + 已选择的映像 + 不会修改来源文件。 + 上游项目 ↗ + 欢迎 + Windows 映像 + 请以管理员身份重新打开 Tiny11Builder.exe,以检查其版本。 + 请关闭并重新打开 Tiny11Builder.exe,然后批准 Windows 管理员提示。 + 请以管理员身份重新打开 Tiny11Builder.exe,以恢复工作区。 + 需要系统管理员权限。 + 请以管理员身份重新打开 Tiny11Builder.exe,然后选择继续生成。尚未更改任何内容。 + 浏览器无法打开 {0}。未进行任何付款。 + 选择的卡片未指出有效的生成模式。 + 无法选择生成模式。 + 快取的 ISO 已更改。未被接受。 + 取消进度 + 无法继续此生成。 + 正在取消。请等待它停止后再关闭。 + 请选择「{0}」,并等待映像操作完成后再关闭。 + 操作仍在运行。 + 已保留先前的选择项目。请勾选你要移除或禁用的个别项目。 + 已加载组件列表。 + 已安装的工具可以列出此映像,但生成它需要较新的 Windows ADK 部署工具。请先安装这些工具并重新打开应用,再进行生成。 + 组件列表已以只读方式加载。 + 已复制 + 复制详细信息 + 无法复制 + 无法打开下载页面。 + 无法打开日志 + SHA-256:{0} + 已下载 {0}。SHA-256:{1} + 无法打开文件夹。 + Windows 高对比已打开,因此颜色会遵循你的系统设置。 + 找到 {0} 个版本。已选择 {1}。 + 已检查映像。 + 已选择映像。 + 列出组件之前,请先选择版本。 + 请先检查映像。 + {0} 已不存在。 + 工作文件夹已消失。 + 打开日志 + 打开 Microsoft 下载页面 + 取得映像工具 + 已验证先前下载的 ISO。 + 请再次检查来源。Core/高级风险确认必须重新进行。 + 已加载配方。 + 配方尚未就绪。 + 其中包含选择项目与本机路径,不包含下载的媒体或认证。 + 已保存配方。 + 立即重新启动 + 输出文件已存在。请选择新的文件名称以继续。 + 未开始继续生成。 + 无法还原工作阶段。 + 继续之前,请再次检查映像。不会自动开始生成。 + 已还原工作阶段。 + 无法打开赞助选项。 + 无法打开赞助页面 + 请选择 Windows 11 客户端映像,22H2 或更新版本,且为 x64 或 ARM64 格式。 + 不支持此版本。 + 已丢弃中断的工作副本。你的源媒体与日志已保留。 + 已恢复工作区。 + 你的源媒体未被更改。工作文件与日志已保留。{0} + 生成已取消。 + 生成失败。 + 未更改任何内容。 + 已取消。 + 部分清理步骤回报问题:{0} + 未完成的下载已保留,因此你可以稍后继续。未修改任何来源媒体。 + 下载已取消。 + 下载未完成。 + 应用仍在没有系统管理员权限的情况下运行。 + 重新启动已取消。 + 应用无法以系统管理员身分重新启动。 + 已完成。 + 操作未完成。 + 映像未被更改。 + 检查已取消。 + 无法检查映像。 + 映像未被更改。 + 组件列表已取消。 + 无法加载组件列表。 + 未回报进一步的详细信息。 + 请再次运行「恢复工作区」,以完成此工作的清理。 + 恢复已取消。 + 恢复未完成。 + 无法丢弃此工作已装载的副本。请使用「恢复工作区」,并选择此工作的 job.json。 + 你可以再次继续。 + 可以继续已保存的进度。 + 已保留已保存的进度与日志。{0} + 继续生成已取消。 + 继续生成失败。 + 已保留已保存的进度,因此你可以再次继续。 + 它结束时未说明结果,因此不会显示为已完成。请检查日志。 + 操作未完成。 + 快速 WIM + 最大 WIM + 恢复 ESD + 原始生成的移除选择会维持不变。继续时无法更改。 + 压缩:{0} + Core:没有 Windows Update、没有 Microsoft Defender、没有恢复环境。仅可在隔离的测试 VM 中使用。 + 个别移除项目包含受保护项目。映像可能无法开机。 + 模式:{0} · {1} 个移除群组 · {2} 个个别动作 + 输出:{0} + 会先验证已保存的进度。若有任何不符,将不会更改任何内容。 + 这不是有效的工作文件路径。 + 自定义(含 Core 移除项目) + 此生成没有可继续的已保存进度。它是在进度存储之前开始,或在映像存储之前停止,因此无法继续。未更改任何内容,其日志与文件都已保留。 + 找不到此工作已保存的配方,因此无法继续。 + 无法读取此工作已保存的配方。{0} + 请选择生成的 tiny11-… 文件夹内的 job.json 文件。 + 已选择的移除项目与完整配方 + 检阅 + Windows 映像 + 生成前,请检查你的映像、移除项目与输出。 + 加载配方 + 未选择映像 + 生成后清理 + 精简安装:{0} + {0} 压缩 + 默认 + 已包含 + 工作文件:{0} + 本机账户设置:{0} + .NET 3.5:{0} + 未包含 + .NET 安装承载内容:{0} + 偏好 + 多余数据:{0} + 安装选项 + Turbo 模式:{0} + 输出与工作区 + ISO +{0} + +工作区 +{1} + 不提供服务、恢复或 Defender。 + 自定义移除项目可能影响服务、恢复或安全。 + {0} 个移除群组 · {1} 个个别动作 + 保留 Windows Update、恢复与 Defender。 + 存储配方 + 部分原则只适用於特定版本或内部版本。功能更新可能会重新安装应用。本机账户与硬件略过设置为尽力而为。仰赖结果前,请先测试生成的 ISO。 + 视内部版本而定的设置 + 使用压缩的系统文件安装 Windows,以减少磁盘使用量。 + 精简安装 + 快速 WIM(生成最快,ISO 较大) + 安装映像压缩 + 最大 WIM(日后较容易服务) + 恢复 ESD(较小,压缩较慢) + 文件与工作区 + 生成设置 + 存储 ISO 后保留修改过的安装媒体。会使用更多磁盘空间。 + 保留工作文件 + 选择生成位置以及映像的压缩方式。 + 在安装期间优先使用本机登录。部分较新的内部版本仍可能要求 Microsoft 账户。 + 本机账户设置 + 包含对较旧应用的支持。ISO 中必须有必需的来源文件。 + .NET Framework 3.5 + 从 ISO 中省略 sources\sxs。你现在仍可包含 .NET 3.5;日后若要启用,需要另外准备匹配的媒体或 Windows Update。Core 之后无法再加入。 + 省略 .NET 3.5 安装承载内容 + 选择输出 + 新的输出 ISO 文件 + 移除已下载的更新快取、封存的服务日志、临时文件与当机报告。会保留待处理的服务数据、驱动程序与安装程序。 + 移除多余的映像数据 + 映像与 ISO 工具会自动准备。 + 映像与 ISO 工具已就绪。 + 批次处理功能与能力移除,并减少日志额外负荷。Core 与快速 WIM 生成会使用更快的工作映像。会使用更多 CPU 与临时空间;最终压缩方式仍依你的选择。 + Turbo 模式 + 选择工作区 + 工作区(NTFS,通常需要 40 到 70 GiB 可用空间) + 旧版安装程序:自动(需要它来处理{0}) + Core 移除项目 + Core 与组件承载内容移除项目 + 组件承载内容移除项目 + 选择的移除项目 + 生成模式:{0} + Microsoft 官方 ISO · {0} · {1} + Microsoft 官方 ISO。架构与语言会自动选择。 + 或使用你已有的 ISO。 + 下载最新的 Windows 11 + Windows 映像 + 从 Microsoft 下载最新的 Windows 11,或选择 ISO 或解压后的安装文件夹。检查映像以选择版本。 + 选择文件夹 + 选择 ISO + 要生成的版本 + 检查映像 + Windows ISO 或安装媒体文件夹 + 选择 Windows 11 映像 + 本机安装媒体 + Microsoft ARM64 下载 ↗ + 下载 ISO/继续未完成的下载 + 已公布的 SHA-256(建议) + 从 Microsoft 的「验证你的下载」区段复制 + 其他官方下载 + 官方 Insider / Canary ISO 选择 ↗ + 在 Microsoft 下载页面选择语言,然后将 ISO 下载链接贴到这里。链接通常会在 24 小时后失效。 + Microsoft 直接 ISO 链接 + Microsoft x64 下载 ↗ + 全部 {0} 个阶段已完成 + 已取消 + 已完成 {0}/{1} 个阶段 + {0} 个失败 + 赞助开发 + 可选的每月赞助或单次捐赠。所有应用功能仍保持免费。 + 可选的赞助。会打开赞助选项,不会直接付款。 + 链接会在你的浏览器中打开。应用内不会收取费用或进行订阅。 + 每月赞助 + 协助项目长期维持。 + 在 GitHub Sponsors 每月赞助 + 单次捐赠 + 一次性感谢,无任何义务。 + 在 Ko-fi 单次捐赠 + 偏好其他服务? + Patreon + PayPal + 协助资助开发时间、跨 Windows 版本的测试,以及修正回报的问题。所有应用功能仍保持免费。 + 赞助 Tiny11 Builder + 生成模式 + Windows 11 22H2 或更新版本 · x64 与 ARM64 · Insider / Canary + 仅供测试 VM 使用 + 移除更新与恢复支持,并禁用 Defender。 + 包含受保护项目 + 选择要移除的个别应用、组件与服务。 + 创建自定义的 Windows 11 ISO。选择映像、选择要移除的内容,然后生成 ISO。 + 选择 Tiny11 Core 并选择 Windows ISO。 + 选择自定义并选择 Windows ISO。 + 选择 Tiny11 并选择 Windows ISO。 + 使用前,请先在 VM 中测试 Core 与自定义映像。 + 保留更新支持 + 使用原始 Tiny11 移除项目。保留 Windows Update 与恢复。 + 欢迎 + 正在检查你的选择… + 正在检查已下载的 ISO… + 正在从 Microsoft 下载… + 正在寻找最新的 Windows 11… + 正在检查映像… + 正在列出映像组件… + 正在恢复工作区… + diff --git a/src/Tiny11Builder.App/Strings/zh-Hant/Resources.resw b/src/Tiny11Builder.App/Strings/zh-Hant/Resources.resw new file mode 100644 index 00000000..97b8cd34 --- /dev/null +++ b/src/Tiny11Builder.App/Strings/zh-Hant/Resources.resw @@ -0,0 +1,627 @@ + + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + 建置 + 重新建置 + 再建置一個 + 繼續 + 下一步 + 復原工作區 + 再次繼續 + 繼續建置 + 從已儲存的進度繼續先前停止的建置。 + 從已儲存的進度繼續此建置。 + 檢閱 + 開始 + Tiny11 Builder + 認可映像 + 壓縮映像 + 匯出與掛接 + 建立 ISO + 準備媒體 + 移除元件 + 驗證輸出 + 正在建置 + 若要停止,請使用右上角的「安全取消」。它會在下一個安全點停止。您的來源媒體絕不會被變更;工作檔案、已儲存的進度與記錄都會保留。 + 已要求取消。{0}… + 已要求取消。正在等待目前操作完成… + 已取消{0} + 正在取消… + 目前操作 + 選取的版本會被複製、修改、壓縮,並儲存為可開機的 ISO。 + 舊版安裝程式會自動開啟,因為需要它來處理{0}。 + 建置詳細資料 + 展開階段以查看每個步驟與任何錯誤詳細資料。 + 完成,但有警告 + 建置 + 操作記錄 + 已建立 ISO + 尚未進行任何變更。 + 建置 + 繼續建置 + 開啟記錄與報告 + 開啟輸出資料夾 + 輸出 ISO + 整體進度 + 正在準備映像工具… + 總計 {0} + {0} GiB ISO + {0} 分鐘 + 另有 {0} 項在建置記錄中 + 已移除多餘資料:{0} MiB 邏輯大小 · 已省略安裝承載內容:{1} MiB + 繼續的建置 + ISO 與建置報告已儲存。使用前,請先在 VM 中測試 ISO。 + ISO 已儲存,但有警告。請查看下方略過的工作與其他詳細資料。 + 詳細的階段計時會儲存在建置報告中。 + 標準建置 + Turbo 建置 + ISO 已儲存,但有警告。請查看頁面上的警告。 + 正在繼續建置 + SHA-256 + {0} 失敗。展開此階段以查看錯誤詳細資料。 + 進階承載內容 + 認可 + 元件清理 + 壓縮 + 複製媒體 + Core 剪除 + 建立 ISO + 自訂服務 + 匯出版本 + 清查 + 掛接 + .NET 3.5 + 選用功能 + 準備 + 多餘資料 + 移除應用程式 + 檢查已儲存的進度 + 設定映像 + 驗證輸出 + Windows 設定 + 完成 + 第 {0} 階段,共 {1} 階段 + 全部 {0} 個階段已完成 + 已完成 0/7 個階段 + {0}已停止 + 如果 Tiny11 Builder 為您節省了時間,您可以贊助它的開發。 + 安裝 Windows 前,請先在 VM 中測試 ISO。 + 等待開始 + {0}它在可取消之前已完成,因此未取消任何內容。 + {0}取消要求太晚才送達,因此未取消任何內容。 + 安全取消 + 完成 + 取消建置 + 取消檢查 + 取消下載 + 取消重新啟動 + 取消檢查 + 取消列出 + 取消 + 取消復原 + 取消繼續建置 + 建置會在下一個安全點停止。這可能需要幾分鐘,因為目前正在執行的映像操作會先被允許完成。您的來源媒體絕不會被變更。已掛接的副本會安全卸載;工作檔案、已儲存的進度與記錄都會保留。 + 檢查會停止。尚未變更任何內容。 + 下載會停止。未完成的檔案會保留,因此您可以稍後繼續。不會修改任何來源媒體。 + 應用程式會繼續在沒有系統管理員權限的情況下執行。 + 檢查會在目前步驟之後停止。映像不會被變更。 + 列出會在下一個安全點停止。映像的唯讀掛接會被捨棄,映像不會被變更。 + 操作會在目前步驟之後停止。尚未變更任何內容。 + 復原會在下一個安全點停止。請再次執行「復原工作區」,以完成此工作的清理。 + 繼續建置會在下一個安全點停止。這可能需要幾分鐘,因為目前正在執行的映像操作會先被允許完成。已掛接的副本會安全卸載;工作檔案、已儲存的進度與記錄都會保留,因此您可以再次繼續。 + ISO 與建置報告已儲存。 + 檢查已完成。 + 下載已完成。 + 應用程式已重新啟動。 + 映像已檢查。 + 元件清單已載入。 + 操作已完成。 + 工作區已復原。 + ISO 與建置報告已儲存。 + 工作資料夾與記錄:{0} + {0}已完成 + 在可取消之前已完成 + {0},但需要復原 + {0},且需要復原 + 會在下一個安全點停止建置。您的來源媒體絕不會被變更。 + 會停止檢查您的選擇。不會變更任何內容。 + 會停止下載。未完成的檔案會保留。 + 會停止以系統管理員身分重新啟動應用程式。 + 會停止檢查映像。不會變更任何內容。 + 會停止列出元件。映像不會被變更。 + 會停止執行中的操作。 + 會在下一個安全點停止復原。 + 會在下一個安全點停止繼續建置。已儲存的進度會保留。 + 繼續建置 + 繼續 + 繼續下載 + 繼續 + 繼續 + 繼續 + 繼續 + 繼續復原 + 繼續建置 + 在此完成之前,請勿關閉應用程式。 + 映像工具絕不會被強制停止,因此這可能需要幾分鐘。請勿關閉應用程式。 + 建置 + 檢查 + 下載 + 重新啟動 + 檢查 + 元件清單 + 操作 + 復原 + 繼續建置 + 要取消此建置嗎? + 要取消檢查嗎? + 要取消此下載嗎? + 要取消重新啟動嗎? + 要取消檢查此映像嗎? + 要取消列出元件嗎? + 要取消此操作嗎? + 要取消此復原嗎? + 要取消此繼續建置嗎? + 清理已完成。 + 目前操作:{0} · {1}% + 目前操作:{0} + 正在捨棄工作掛接 + 已完成。 + 正在完成目前操作 + 正在儲存記錄 + 已停止。 + 正在取消建置 + 正在取消檢查 + 正在取消下載 + 正在取消重新啟動 + 正在取消檢查 + 正在取消元件清單 + 正在取消 + 正在取消復原 + 正在取消繼續建置 + 正在停止下載 + 正在等待目前操作完成 + 正在等待目前步驟完成 + 已送出取消,但對其作出反應的步驟失敗:{0} + 無法釋放取消權杖:{0} + 進階/原始建置器 + 隨附應用程式 + 選用功能 + Windows 設定 + 抑制消費者內容、設定建議、檔案總管促銷與歡迎提示。 + 廣告、建議與贊助應用程式 + 移除相機應用程式,而非相機驅動程式。 + 相機 + 一般模式會保留服務能力。Core 會在其不可逆的元件剪除之前,另外重設更新基底。 + 元件清理 + 移除隨附的影片編輯器。 + Clipchamp + 移除預先安裝的 Copilot 應用程式。保留共用執行階段與殼層元件。 + Copilot 應用程式 + 套用原則以停用 Copilot 與 Recall。支援程度因組建與版本而異。 + 停用 Copilot 與 Recall + 移除 Dev Home 與 Power Automate Desktop。 + Dev Home 與 Power Automate + 防止自動 BitLocker 裝置加密;如果您預設需要加密,請勿使用。 + 停用自動裝置加密 + 刪除 Edge 與 WebView2 檔案。需要 WebView2 的應用程式可能停止運作。更新可能會重新安裝 Edge。 + 移除 Edge 與 WebView2 + 抑制首次執行、側邊欄與建議。保留 Edge 與 WebView2 以維持應用程式相容性。 + Edge 首次執行與促銷介面 + 移除意見反應中樞、取得協助與入門應用程式。 + 意見反應、提示與說明 + 在安裝程式與 Windows 中設定 CPU、RAM、TPM、儲存裝置與安全開機的略過值。不受支援的硬體可能無法運作。 + 略過安裝硬體需求 + 移除每個映像語言中的選用語言能力;保留基本語言支援。 + 語音、OCR 與手寫承載內容 + 在已安裝的情況下,移除 WordPad、步驟收錄程式、數學辨識器、舊版媒體播放器與擴充桌布。 + 已淘汰的附屬應用程式與桌布承載內容 + 在存在的情況下,停用已淘汰的 Internet Explorer 選用功能。 + 舊版 Internet Explorer + 移除已淘汰的地圖與混合實境用戶端。 + 地圖與混合實境 + 移除隨附的媒體播放應用程式;編解碼器與共用架構會保留。 + 媒體播放器與電影 + 移除封裝的資訊摘要應用程式;保留本機 Windows 搜尋。 + 新聞、天氣與 Bing 應用程式 + 移除 Intel 管理狀態與 Dolby 促銷應用程式,而非驅動程式。 + OEM 促銷應用程式 + 移除 Office Hub 與 Office 推播通知促銷應用程式。 + Microsoft 365 促銷 + 從離線映像中移除兩種架構專用的 OneDrive 安裝程式。 + 移除 OneDrive 安裝二進位檔 + 透過原則停用 OneDrive 同步。OneDrive 二進位檔會保留;僅在您不需要同步時使用。 + OneDrive 同步與備份提示 + 抑制隱私權設定與登入後的設定建議,而不建立帳戶或接受 EULA。 + 安裝隱私權與完成設定提示 + 移除 Outlook 與已淘汰的郵件/行事曆應用程式。 + 新版 Outlook 與郵件 + 移除小畫家、3D 檢視器與 OneNote。取消勾選即可保留。 + 小畫家、3D 檢視器與 OneNote + 移除手機整合應用程式。跨裝置功能可能停止運作。 + 手機連結與跨裝置應用程式 + 保留必要的診斷資料。停用廣告識別碼、個人化建議與輸入收集。 + 選用診斷與個人化 + 移除 Microsoft 的遠端協助用戶端。 + 快速小幫手 + 僅在此映像向 DISM 顯示 Recall 時,才停用並移除其承載內容。 + Recall 選用功能 + 停止為更新保留磁碟空間。這可能會在服務期間留下較少的可用空間。 + 停用保留儲存空間 + 關閉網頁建議、雲端搜尋與動態搜尋醒目提示;保留本機搜尋。 + 網頁搜尋與搜尋醒目提示 + 移除選用的整理與通訊應用程式。 + 人員、To Do、Family 與 Skype + 移除隨附的遊戲。 + 接龍 + 移除個人版 Teams 用戶端。 + Teams + 移除 Terminal。PowerShell 與命令提示字元會保留。 + Windows Terminal + 移除選用的個人小工具。 + 時鐘、錄音機與便箋 + 透過原則停用小工具,並隱藏其工作列項目。 + 小工具與工作列摘要 + 停用企業工作資料夾同步並移除其承載內容。 + 工作資料夾 + 移除 Xbox 應用程式與遊戲列。遊戲登入與錄製功能可能無法使用。 + Xbox 與遊戲覆疊 + 停用選用的 XPS 列印並移除其承載內容。 + XPS 列印 + 全部 + 返回 + 建置活動 + 建置記錄 + 安全取消 + 關閉 + 元件類型 + 目前操作進度 + 深色 + 下載進度 + 映像元件 + 保留 + 淺色 + 關 + 省略 + 開 + 整體進度 + 移除 + 搜尋元件 + 開始 + 系統 + 處理中 + 清除自訂選取項目 + 我接受自訂映像可能無法開機、無法服務或不安全的風險。 + 個別移除項目會加入已選取的群組。若要保留某個元件,請同時取消勾選其群組與個別項目。 + 列出映像的應用程式、套件、功能、服務與元件檔案,包括受保護項目。移除服務會刪除其登錄項目,而非共用執行檔。受保護的套件可能會拒絕移除。 + 列出所有元件… + 進階移除 + 移除受保護或開機關鍵項目,可能導致映像無法開機或不安全。直接移除承載內容會略過服務機制。不提供相依性解析或開機保證。 + 不支援的移除項目 + 僅保留必要項目。供測試 VM 使用。 + 選擇個別移除項目。 + 原始移除項目。保留更新支援。 + 已接受此建置的 Core 限制。 + 尚未接受 Core 限制。 + 繼續之前,系統會要求您接受 Core 的限制。 + Core 會刪除元件檔案與 WinRE,並停用 Windows Update 與 Defender。之後將無法新增更新、語言或功能。請先在 VM 中測試映像。 + Core 會移除更新與復原支援。 + 已選取 {0} 個移除群組 · {1} 個進階動作 + 已列出 {0} 個元件 · 已選取 {1} 個進階動作。 + 套用 Core 移除項目 + 保留服務、復原與 Defender + 移除服務、復原與 Defender + 顯示選項 + 勾選的群組會被移除或停用。未勾選的群組會保留。若要處理個別項目,請使用下方的元件清單。 + 元件 + {0} 的動作 + 應用程式 + 停用 + 受保護/可能為開機關鍵 + 移除 + 選擇 Tiny11、Tiny11 Core,或使用自訂選取自己的移除項目。每項變更都會記錄在建置記錄中。 + Tiny11 Core 移除項目,包括不可逆的元件剪除。選擇自訂以變更個別移除項目。 + 從未選取任何項目開始,再選擇要移除或停用的內容。您也可以列出個別元件,包括受保護項目。 + 原始 Tiny11 移除項目,包括 Edge/WebView2 與 OneDrive 二進位檔。保留 Windows Update、Defender 與復原。選擇自訂以變更個別移除項目。 + 未選取 + 已選取 + 使用 Tiny11 Core + 使用自訂 + 使用 Tiny11 + 建置 + 輸出:{0} + +模式:{1} +移除群組:{2} +個別動作:{3}{4} + 要建立此離線 Windows 映像嗎? + 取消 + 我同意 + 沒有 Windows Update +沒有 Microsoft Defender +沒有復原環境 +無法新增語言或功能 + +僅可在隔離的測試 VM 中使用。 +映像可能無法開機。 + Core 無法提供服務 + 此清查包含開機驅動程式、安全性、授權、系統應用程式與元件承載內容。移除它們可能導致 Windows 無法開機、不安全或無法更新。受保護的套件可能會拒絕移除。不支援直接移除承載內容。 + +僅在隔離測試時繼續,並備份及準備已知良好的安裝程式。 + 檢查元件 + 包含受保護的元件 + 復原會捨棄工作的副本,但不會對已儲存進度的工作這樣做。請改用繼續建置,從該進度繼續。 + +尚未變更任何內容。 + 改為繼續 + 此工作有已儲存的進度 + 請只選取中斷的 tiny11 工作中的 job.json。這會捨棄該工作已掛接的副本,並移除其暫存媒體。其記錄、配方、報告與您的來源 ISO 都會保留。映像已認可的工作會有已儲存的進度,絕不會在此捨棄;請對它使用繼續建置。無法復原進行中的建置。 + 捨棄工作副本 + 要捨棄此中斷的工作區嗎? + 繼續 + 要繼續此建置嗎? + 瀏覽器無法開啟 Microsoft 的映像工具頁面。 + 請先檢查選取的 Windows 映像。 + 建議的下載頁面不是已核准的 Microsoft 頁面。 + 找不到應用程式路徑。 + 儲存的工作階段是空的。 + 此贊助連結沒有有效的目的地。 + 映像配方 + ISO 映像 + 未選取映像 + 語言 + 無法重新啟動應用程式。 + 新語言已儲存。請重新啟動 Tiny11 Builder 以套用。 + 重新啟動以套用語言。 + 在設定可以寫入之前,應用程式會維持目前的語言。 + 無法儲存語言。 + 系統預設 + Tiny11 Core + 自訂 + Tiny11 + 外觀 + 建置設定 + 建置步驟 + 建立映像 + 移除元件 + 檢閱 + 已選取的映像 + 不會修改來源檔案。 + 上游專案 ↗ + 歡迎 + Windows 映像 + 請以系統管理員核准重新開啟 Tiny11Builder.exe,以檢查其版本。 + 請關閉並重新開啟 Tiny11Builder.exe,然後核准 Windows 系統管理員提示。 + 請以系統管理員核准重新開啟 Tiny11Builder.exe,以復原工作區。 + 需要系統管理員權限。 + 請以系統管理員核准重新開啟 Tiny11Builder.exe,然後選擇繼續建置。尚未變更任何內容。 + 瀏覽器無法開啟 {0}。未進行任何付款。 + 選取的卡片未指出有效的建置模式。 + 無法選取建置模式。 + 快取的 ISO 已變更。未被接受。 + 取消進度 + 無法繼續此建置。 + 正在取消。請等待它停止後再關閉。 + 請選擇「{0}」,並等待映像操作完成後再關閉。 + 操作仍在執行。 + 已保留先前的選取項目。請勾選您要移除或停用的個別項目。 + 已載入元件清單。 + 已安裝的工具可以列出此映像,但建置它需要較新的 Windows ADK 部署工具。請先安裝這些工具並重新開啟應用程式,再進行建置。 + 元件清單已以唯讀方式載入。 + 已複製 + 複製詳細資料 + 無法複製 + 無法開啟下載頁面。 + 無法開啟記錄 + SHA-256:{0} + 已下載 {0}。SHA-256:{1} + 無法開啟資料夾。 + Windows 高對比已開啟,因此色彩會遵循您的系統設定。 + 找到 {0} 個版本。已選取 {1}。 + 已檢查映像。 + 已選取映像。 + 列出元件之前,請先選取版本。 + 請先檢查映像。 + {0} 已不存在。 + 工作資料夾已消失。 + 開啟記錄 + 開啟 Microsoft 下載頁面 + 取得映像工具 + 已驗證先前下載的 ISO。 + 請再次檢查來源。Core/進階風險確認必須重新進行。 + 已載入配方。 + 配方尚未就緒。 + 其中包含選取項目與本機路徑,不包含下載的媒體或認證。 + 已儲存配方。 + 立即重新啟動 + 輸出檔案已存在。請選擇新的檔案名稱以繼續。 + 未開始繼續建置。 + 無法還原工作階段。 + 繼續之前,請再次檢查映像。不會自動開始建置。 + 已還原工作階段。 + 無法開啟贊助選項。 + 無法開啟贊助頁面 + 請選擇 Windows 11 用戶端映像,22H2 或更新版本,且為 x64 或 ARM64 格式。 + 不支援此版本。 + 已捨棄中斷的工作副本。您的來源媒體與記錄已保留。 + 已復原工作區。 + 您的來源媒體未被變更。工作檔案與記錄已保留。{0} + 建置已取消。 + 建置失敗。 + 未變更任何內容。 + 已取消。 + 部分清理步驟回報問題:{0} + 未完成的下載已保留,因此您可以稍後繼續。未修改任何來源媒體。 + 下載已取消。 + 下載未完成。 + 應用程式仍在沒有系統管理員權限的情況下執行。 + 重新啟動已取消。 + 應用程式無法以系統管理員身分重新啟動。 + 已完成。 + 操作未完成。 + 映像未被變更。 + 檢查已取消。 + 無法檢查映像。 + 映像未被變更。 + 元件清單已取消。 + 無法載入元件清單。 + 未回報進一步的詳細資料。 + 請再次執行「復原工作區」,以完成此工作的清理。 + 復原已取消。 + 復原未完成。 + 無法捨棄此工作已掛接的副本。請使用「復原工作區」,並選擇此工作的 job.json。 + 您可以再次繼續。 + 可以繼續已儲存的進度。 + 已保留已儲存的進度與記錄。{0} + 繼續建置已取消。 + 繼續建置失敗。 + 已保留已儲存的進度,因此您可以再次繼續。 + 它結束時未說明結果,因此不會顯示為已完成。請檢查記錄。 + 操作未完成。 + 快速 WIM + 最大 WIM + 復原 ESD + 原始建置的移除選擇會維持不變。繼續時無法變更。 + 壓縮:{0} + Core:沒有 Windows Update、沒有 Microsoft Defender、沒有復原環境。僅可在隔離的測試 VM 中使用。 + 個別移除項目包含受保護項目。映像可能無法開機。 + 模式:{0} · {1} 個移除群組 · {2} 個個別動作 + 輸出:{0} + 會先驗證已儲存的進度。若有任何不符,將不會變更任何內容。 + 這不是有效的工作檔案路徑。 + 自訂(含 Core 移除項目) + 此建置沒有可繼續的已儲存進度。它是在進度儲存之前開始,或在映像儲存之前停止,因此無法繼續。未變更任何內容,其記錄與檔案都已保留。 + 找不到此工作已儲存的配方,因此無法繼續。 + 無法讀取此工作已儲存的配方。{0} + 請選擇建置的 tiny11-… 資料夾內的 job.json 檔案。 + 已選取的移除項目與完整配方 + 檢閱 + Windows 映像 + 建置前,請檢查您的映像、移除項目與輸出。 + 載入配方 + 未選取映像 + 建置後清理 + 精簡安裝:{0} + {0} 壓縮 + 預設 + 已包含 + 工作檔案:{0} + 本機帳戶設定:{0} + .NET 3.5:{0} + 未包含 + .NET 安裝承載內容:{0} + 偏好 + 多餘資料:{0} + 安裝選項 + Turbo 模式:{0} + 輸出與工作區 + ISO +{0} + +工作區 +{1} + 不提供服務、復原或 Defender。 + 自訂移除項目可能影響服務、復原或安全性。 + {0} 個移除群組 · {1} 個個別動作 + 保留 Windows Update、復原與 Defender。 + 儲存配方 + 部分原則只適用於特定版本或組建。功能更新可能會重新安裝應用程式。本機帳戶與硬體略過設定為盡力而為。仰賴結果前,請先測試產生的 ISO。 + 視組建而定的設定 + 使用壓縮的系統檔案安裝 Windows,以減少磁碟使用量。 + 精簡安裝 + 快速 WIM(建置最快,ISO 較大) + 安裝映像壓縮 + 最大 WIM(日後較容易服務) + 復原 ESD(較小,壓縮較慢) + 檔案與工作區 + 建置設定 + 儲存 ISO 後保留修改過的安裝媒體。會使用更多磁碟空間。 + 保留工作檔案 + 選擇建置位置以及映像的壓縮方式。 + 在安裝期間優先使用本機登入。部分較新的組建仍可能要求 Microsoft 帳戶。 + 本機帳戶設定 + 包含對較舊應用程式的支援。ISO 中必須有必要的來源檔案。 + .NET Framework 3.5 + 從 ISO 中省略 sources\sxs。您現在仍可包含 .NET 3.5;日後若要啟用,需要另外準備相符的媒體或 Windows Update。Core 之後無法再加入。 + 省略 .NET 3.5 安裝承載內容 + 選擇輸出 + 新的輸出 ISO 檔案 + 移除已下載的更新快取、封存的服務記錄、暫存檔案與當機報告。會保留待處理的服務資料、驅動程式與安裝程式。 + 移除多餘的映像資料 + 映像與 ISO 工具會自動準備。 + 映像與 ISO 工具已就緒。 + 批次處理功能與能力移除,並減少記錄額外負荷。Core 與快速 WIM 建置會使用更快的工作映像。會使用更多 CPU 與暫存空間;最終壓縮方式仍依您的選擇。 + Turbo 模式 + 選擇工作區 + 工作區(NTFS,通常需要 40 到 70 GiB 可用空間) + 舊版安裝程式:自動(需要它來處理{0}) + Core 移除項目 + Core 與元件承載內容移除項目 + 元件承載內容移除項目 + 選取的移除項目 + 建置模式:{0} + Microsoft 官方 ISO · {0} · {1} + Microsoft 官方 ISO。架構與語言會自動選取。 + 或使用您已有的 ISO。 + 下載最新的 Windows 11 + Windows 映像 + 從 Microsoft 下載最新的 Windows 11,或選取 ISO 或解壓縮後的安裝資料夾。檢查映像以選取版本。 + 選擇資料夾 + 選擇 ISO + 要建置的版本 + 檢查映像 + Windows ISO 或安裝媒體資料夾 + 選擇 Windows 11 映像 + 本機安裝媒體 + Microsoft ARM64 下載 ↗ + 下載 ISO/繼續未完成的下載 + 已公布的 SHA-256(建議) + 從 Microsoft 的「驗證您的下載」區段複製 + 其他官方下載 + 官方 Insider / Canary ISO 選取 ↗ + 在 Microsoft 下載頁面選擇語言,然後將 ISO 下載連結貼到這裡。連結通常會在 24 小時後失效。 + Microsoft 直接 ISO 連結 + Microsoft x64 下載 ↗ + 全部 {0} 個階段已完成 + 已取消 + 已完成 {0}/{1} 個階段 + {0} 個失敗 + 贊助開發 + 可選的每月贊助或單次捐款。所有應用程式功能仍保持免費。 + 可選的贊助。會開啟贊助選項,不會直接付款。 + 連結會在您的瀏覽器中開啟。應用程式內不會收取費用或進行訂閱。 + 每月贊助 + 協助專案長期維持。 + 在 GitHub Sponsors 每月贊助 + 單次捐款 + 一次性感謝,無任何義務。 + 在 Ko-fi 單次捐款 + 偏好其他服務? + Patreon + PayPal + 協助資助開發時間、跨 Windows 版本的測試,以及修正回報的問題。所有應用程式功能仍保持免費。 + 贊助 Tiny11 Builder + 建置模式 + Windows 11 22H2 或更新版本 · x64 與 ARM64 · Insider / Canary + 僅供測試 VM 使用 + 移除更新與復原支援,並停用 Defender。 + 包含受保護項目 + 選取要移除的個別應用程式、元件與服務。 + 建立自訂的 Windows 11 ISO。選取映像、選擇要移除的內容,然後建置 ISO。 + 選取 Tiny11 Core 並選擇 Windows ISO。 + 選取自訂並選擇 Windows ISO。 + 選取 Tiny11 並選擇 Windows ISO。 + 使用前,請先在 VM 中測試 Core 與自訂映像。 + 保留更新支援 + 使用原始 Tiny11 移除項目。保留 Windows Update 與復原。 + 歡迎 + 正在檢查您的選擇… + 正在檢查已下載的 ISO… + 正在從 Microsoft 下載… + 正在尋找最新的 Windows 11… + 正在檢查映像… + 正在列出映像元件… + 正在復原工作區… + diff --git a/src/Tiny11Builder.App/Tiny11Builder.App.csproj b/src/Tiny11Builder.App/Tiny11Builder.App.csproj index e37fcda4..3f374cf8 100644 --- a/src/Tiny11Builder.App/Tiny11Builder.App.csproj +++ b/src/Tiny11Builder.App/Tiny11Builder.App.csproj @@ -18,6 +18,8 @@ true true false + + resources.pri diff --git a/src/Tiny11Builder.App/TrExtension.cs b/src/Tiny11Builder.App/TrExtension.cs new file mode 100644 index 00000000..9a9cd3b0 --- /dev/null +++ b/src/Tiny11Builder.App/TrExtension.cs @@ -0,0 +1,11 @@ +using Microsoft.UI.Xaml.Markup; + +namespace Tiny11Builder.App; + +[MarkupExtensionReturnType(ReturnType = typeof(string))] +public sealed partial class TrExtension : MarkupExtension +{ + public string Key { get; set; } = ""; + + protected override object ProvideValue() => Loc.Get(Key); +} diff --git a/tests/Tiny11Builder.Core.Tests/LocalizationTests.cs b/tests/Tiny11Builder.Core.Tests/LocalizationTests.cs new file mode 100644 index 00000000..7a1d9330 --- /dev/null +++ b/tests/Tiny11Builder.Core.Tests/LocalizationTests.cs @@ -0,0 +1,80 @@ +using System.Runtime.CompilerServices; +using System.Xml.Linq; +using Tiny11Builder.App; +using Xunit; + +namespace Tiny11Builder.Core.Tests; + +internal static class LocalizationTestHost +{ + [ModuleInitializer] + internal static void Initialize() => Loc.Use(EnglishResources.Get); +} + +internal static class EnglishResources +{ + private static readonly Lazy> Strings = new(() => Read("en-US")); + + public static string? Get(string key) => Strings.Value.TryGetValue(key, out var text) ? text : null; + + public static IReadOnlyDictionary Read(string language) + { + var path = Path.Combine(AppContext.BaseDirectory, "Strings", language, "Resources.resw"); + return XDocument.Load(path).Root!.Elements("data") + .ToDictionary(e => (string)e.Attribute("name")!, e => e.Element("value")?.Value ?? ""); + } + + public static IEnumerable Languages => + Directory.EnumerateDirectories(Path.Combine(AppContext.BaseDirectory, "Strings")) + .Select(Path.GetFileName).OfType(); +} + +public sealed class LocalizationTests +{ + [Fact] + public void Every_language_has_the_same_nonempty_keys_and_placeholders() + { + var english = EnglishResources.Read("en-US"); + Assert.NotEmpty(english); + Assert.All(english, pair => Assert.False(string.IsNullOrWhiteSpace(pair.Value), pair.Key)); + + foreach (var language in EnglishResources.Languages.Where(language => language != "en-US")) + { + var translated = EnglishResources.Read(language); + Assert.Equal(english.Keys.OrderBy(key => key), translated.Keys.OrderBy(key => key)); + foreach (var (key, value) in translated) + { + Assert.False(string.IsNullOrWhiteSpace(value), $"{language}: {key}"); + Assert.Equal(Placeholders(english[key]), Placeholders(value)); + } + } + } + + [Fact] + public void Every_key_used_by_xaml_exists() + { + var english = EnglishResources.Read("en-US"); + var xaml = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Deployment", "MainWindow.xaml")); + var keys = System.Text.RegularExpressions.Regex.Matches(xaml, @"\{loc:Tr Key=([^}]+)\}") + .Select(match => match.Groups[1].Value).Distinct().ToArray(); + Assert.NotEmpty(keys); + Assert.All(keys, key => Assert.True(english.ContainsKey(key), key)); + } + + [Fact] + public void Language_tags_normalize_to_a_shipped_resource_folder() + { + Assert.Equal("", LanguageCatalog.Normalize(null)); + Assert.Equal("", LanguageCatalog.Normalize("system")); + Assert.Equal("zh-Hant", LanguageCatalog.Normalize("zh-TW")); + Assert.Equal("zh-Hant", LanguageCatalog.Normalize("zh-HK")); + Assert.Equal("zh-Hans", LanguageCatalog.Normalize("zh-CN")); + Assert.Equal("en-US", LanguageCatalog.Normalize("en")); + Assert.All(LanguageCatalog.All.Where(language => language.Tag.Length > 0), + language => Assert.True(Directory.Exists(Path.Combine(AppContext.BaseDirectory, "Strings", language.Tag)), language.Tag)); + } + + private static string Placeholders(string text) => + string.Join(",", System.Text.RegularExpressions.Regex.Matches(text, @"\{\d+\}") + .Select(match => match.Value).OrderBy(value => value)); +} diff --git a/tests/Tiny11Builder.Core.Tests/Tiny11Builder.Core.Tests.csproj b/tests/Tiny11Builder.Core.Tests/Tiny11Builder.Core.Tests.csproj index 8abfd299..ff434441 100644 --- a/tests/Tiny11Builder.Core.Tests/Tiny11Builder.Core.Tests.csproj +++ b/tests/Tiny11Builder.Core.Tests/Tiny11Builder.Core.Tests.csproj @@ -15,7 +15,10 @@ + + +