winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 66b851fd9316eeb17f0a99a7681fb50e5064bbb0
parent 6140e6235c21feda291753766b29191ca19d0f02
Author: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com>
Date:   Thu, 15 Dec 2022 17:16:09 -0800

More localization friendly source strings and context commenting (#2454)


Diffstat:
Msrc/AppInstallerCLICore/ChannelStreams.h | 33++-------------------------------
Msrc/AppInstallerCLICore/Command.cpp | 94++++++++++++++++++++++++++-----------------------------------------------------
Msrc/AppInstallerCLICore/Command.h | 17+----------------
Msrc/AppInstallerCLICore/Commands/FeaturesCommand.cpp | 2+-
Msrc/AppInstallerCLICore/Commands/InstallCommand.cpp | 3++-
Msrc/AppInstallerCLICore/Commands/RootCommand.cpp | 8++++----
Msrc/AppInstallerCLICore/Commands/SettingsCommand.cpp | 232+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/AppInstallerCLICore/Commands/UninstallCommand.cpp | 4++--
Msrc/AppInstallerCLICore/Core.cpp | 2+-
Msrc/AppInstallerCLICore/PortableInstaller.cpp | 4++--
Msrc/AppInstallerCLICore/Resources.cpp | 40----------------------------------------
Msrc/AppInstallerCLICore/Resources.h | 49++-----------------------------------------------
Msrc/AppInstallerCLICore/Workflows/ArchiveFlow.cpp | 4+++-
Msrc/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp | 13++++++++-----
Msrc/AppInstallerCLICore/Workflows/DownloadFlow.cpp | 2+-
Msrc/AppInstallerCLICore/Workflows/ImportExportFlow.cpp | 26++++++++++++--------------
Msrc/AppInstallerCLICore/Workflows/InstallFlow.cpp | 13+++++++++----
Msrc/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp | 17+++++++++++++----
Msrc/AppInstallerCLICore/Workflows/PromptFlow.cpp | 7++++---
Msrc/AppInstallerCLICore/Workflows/SettingsFlow.cpp | 16++++++----------
Msrc/AppInstallerCLICore/Workflows/SourceFlow.cpp | 26+++++++++++++-------------
Msrc/AppInstallerCLICore/Workflows/UninstallFlow.cpp | 11++++++++---
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 43+++++++++++++++++++------------------------
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.h | 2+-
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 229+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 1+
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 3+++
Msrc/AppInstallerCLITests/Command.cpp | 56+++++++++++++++-----------------------------------------
Msrc/AppInstallerCLITests/Dependencies.cpp | 3++-
Asrc/AppInstallerCLITests/Resources.cpp | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/Strings.cpp | 25++++++++++++++++++++++++-
Msrc/AppInstallerCLITests/WorkFlow.cpp | 17++++++++++-------
Msrc/AppInstallerCommonCore/AdminSettings.cpp | 14+++++++-------
Msrc/AppInstallerCommonCore/AppInstallerStrings.cpp | 25+++++++++++++++++--------
Msrc/AppInstallerCommonCore/Public/AppInstallerStrings.h | 17++++++++++++++---
Msrc/AppInstallerCommonCore/Public/winget/AdminSettings.h | 2+-
Msrc/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h | 5+++--
Msrc/AppInstallerCommonCore/Public/winget/Resources.h | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 4++--
Msrc/AppInstallerCommonCore/Resources.cpp | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
40 files changed, 729 insertions(+), 595 deletions(-)

diff --git a/src/AppInstallerCLICore/ChannelStreams.h b/src/AppInstallerCLICore/ChannelStreams.h @@ -11,36 +11,6 @@ namespace AppInstaller::CLI::Execution { - namespace details - { - // List of approved types for output, others are potentially not localized. - template <typename T> - struct IsApprovedForOutput - { - static constexpr bool value = false; - }; - -#define WINGET_CREATE_ISAPPROVEDFOROUTPUT_SPECIALIZATION(_t_) \ - template <> \ - struct IsApprovedForOutput<_t_> \ - { \ - static constexpr bool value = true; \ - } - - // It is assumed that single char values need not be localized, as they are matched - // ordinally or they are punctuation / other. - WINGET_CREATE_ISAPPROVEDFOROUTPUT_SPECIALIZATION(char); - // Localized strings (and from an Id for one for convenience). - WINGET_CREATE_ISAPPROVEDFOROUTPUT_SPECIALIZATION(Resource::StringId); - WINGET_CREATE_ISAPPROVEDFOROUTPUT_SPECIALIZATION(Resource::LocString); - // Strings explicitly declared as localization independent. - WINGET_CREATE_ISAPPROVEDFOROUTPUT_SPECIALIZATION(Utility::LocIndView); - WINGET_CREATE_ISAPPROVEDFOROUTPUT_SPECIALIZATION(Utility::LocIndString); - // Normalized strings come from user data and should therefore already by localized - // by how they are chosen (or there is no localized version). - WINGET_CREATE_ISAPPROVEDFOROUTPUT_SPECIALIZATION(Utility::NormalizedString); - } - // The base stream for all channels. struct BaseStream { @@ -102,7 +72,8 @@ namespace AppInstaller::CLI::Execution // * If your string came from outside of the source code, it is best to store it in a // Utility::NormalizedString so that it has a normalized representation. This also // informs the output that there is no localized version to use. - // TODO: Convert the rest of the code base and uncomment to enforce localization. + // TODO: This assertion is currently only applied to placeholders in localized strings. + // Convert the rest of the code base and uncomment to enforce localization. //static_assert(details::IsApprovedForOutput<std::decay_t<T>>::value, "This type may not be localized, see comment for more information"); if (m_enabled) { diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp @@ -11,18 +11,7 @@ using namespace AppInstaller::Settings; namespace AppInstaller::CLI { - constexpr std::string_view s_Command_ArgName_SilentAndInteractive = "silent|interactive"sv; - - const Utility::LocIndString CommandException::Message() const - { - if (m_replace) - { - return Utility::LocIndString{ Utility::FindAndReplaceMessageToken(m_message, m_replace.value()) }; - } - - // Fall back to just using the message. - return Utility::LocIndString{ m_message.get() }; - } + constexpr Utility::LocIndView s_Command_ArgName_SilentAndInteractive = "silent|interactive"_liv; Command::Command( std::string_view name, @@ -49,9 +38,8 @@ namespace AppInstaller::CLI void Command::OutputIntroHeader(Execution::Reporter& reporter) const { - reporter.Info() << - (Runtime::IsReleaseBuild() ? Resource::String::WindowsPackageManager : Resource::String::WindowsPackageManagerPreview) << " v"_liv << Runtime::GetClientVersion() << std::endl << - Resource::String::MainCopyrightNotice << std::endl; + auto productName = Runtime::IsReleaseBuild() ? Resource::String::WindowsPackageManager : Resource::String::WindowsPackageManagerPreview; + reporter.Info() << productName(Runtime::GetClientVersion()) << std::endl << Resource::String::MainCopyrightNotice << std::endl; } void Command::OutputHelp(Execution::Reporter& reporter, const CommandException* exception) const @@ -63,29 +51,7 @@ namespace AppInstaller::CLI // Error if given if (exception) { - auto error = reporter.Error(); - error << exception->Message(); - - if (!exception->Params().empty()) - { - error << " :"_liv; - bool first = true; - for (const auto& param : exception->Params()) - { - if (first) - { - first = false; - } - else - { - error << ','; - } - error << " '"_liv << param << '\''; - } - } - - error << std::endl << - std::endl; + reporter.Error() << exception->Message() << std::endl << std::endl; } // Description @@ -115,7 +81,7 @@ namespace AppInstaller::CLI } // Output the command preamble and command chain - infoOut << Resource::String::Usage << ": winget"_liv << Utility::LocIndView{ commandChain }; + infoOut << Resource::String::Usage("winget"_liv, Utility::LocIndView{ commandChain }); auto commandAliases = Aliases(); auto commands = GetVisibleCommands(); @@ -280,10 +246,10 @@ namespace AppInstaller::CLI } // Finally, the link to the documentation pages - std::string helpLink = HelpLink(); + auto helpLink = Utility::LocIndString{ HelpLink() }; if (!helpLink.empty()) { - infoOut << std::endl << Resource::String::HelpLinkPreamble << ' ' << helpLink << std::endl; + infoOut << std::endl << Resource::String::HelpLinkPreamble(helpLink) << std::endl; } } @@ -314,7 +280,7 @@ namespace AppInstaller::CLI { auto feature = ExperimentalFeature::GetFeature(command->Feature()); AICLI_LOG(CLI, Error, << "Trying to use command: " << *itr << " without enabling feature " << feature.JsonName()); - throw CommandException(Resource::String::FeatureDisabledMessage, feature.JsonName()); + throw CommandException(Resource::String::FeatureDisabledMessage(feature.JsonName())); } if (!Settings::GroupPolicies().IsEnabled(command->GroupPolicy())) @@ -331,7 +297,7 @@ namespace AppInstaller::CLI } // TODO: If we get to a large number of commands, do a fuzzy search much like git - throw CommandException(Resource::String::UnrecognizedCommand, *itr); + throw CommandException(Resource::String::UnrecognizedCommand(Utility::LocIndView{ *itr })); } // The argument parsing state machine. @@ -367,14 +333,14 @@ namespace AppInstaller::CLI const std::optional<Execution::Args::Type>& Type() const { return m_type; } // The actual argument string associated with Type. - const std::string& Arg() const { return m_arg; } + const Utility::LocIndString& Arg() const { return m_arg; } // If set, indicates that the last argument produced an error. const std::optional<CommandException>& Exception() const { return m_exception; } private: std::optional<Execution::Args::Type> m_type; - std::string m_arg; + Utility::LocIndString m_arg; std::optional<CommandException> m_exception; }; @@ -432,7 +398,7 @@ namespace AppInstaller::CLI // If the next argument was to be a value, but none was provided, convert it to an exception. else if (m_state.Type() && m_invocationItr == m_invocation.end()) { - throw CommandException(Resource::String::MissingArgumentError, m_state.Arg()); + throw CommandException(Resource::String::MissingArgumentError(m_state.Arg())); } } @@ -472,7 +438,7 @@ namespace AppInstaller::CLI // 4. If the argument is only a double --, all further arguments are only considered as positional. ParseArgumentsStateMachine::State ParseArgumentsStateMachine::StepInternal() { - std::string_view currArg = *m_invocationItr; + auto currArg = Utility::LocIndView{ *m_invocationItr }; ++m_invocationItr; // If the previous step indicated a value was needed, set it and forget it. @@ -488,7 +454,7 @@ namespace AppInstaller::CLI const CLI::Argument* nextPositional = NextPositional(); if (!nextPositional) { - return CommandException(Resource::String::ExtraPositionalError, currArg); + return CommandException(Resource::String::ExtraPositionalError(currArg)); } m_executionArgs.AddArg(nextPositional->ExecArgType(), currArg); @@ -496,7 +462,7 @@ namespace AppInstaller::CLI // The currentArg must not be empty, and starts with a - else if (currArg.length() == 1) { - return CommandException(Resource::String::InvalidArgumentSpecifierError, currArg); + return CommandException(Resource::String::InvalidArgumentSpecifierError(currArg)); } // Now it must be at least 2 chars else if (currArg[1] != APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR) @@ -507,7 +473,7 @@ namespace AppInstaller::CLI auto itr = std::find_if(m_arguments.begin(), m_arguments.end(), [&](const Argument& arg) { return (currChar == arg.Alias()); }); if (itr == m_arguments.end()) { - return CommandException(Resource::String::InvalidAliasError, currArg); + return CommandException(Resource::String::InvalidAliasError(currArg)); } if (itr->Type() == ArgumentType::Flag) @@ -521,11 +487,11 @@ namespace AppInstaller::CLI auto itr2 = std::find_if(m_arguments.begin(), m_arguments.end(), [&](const Argument& arg) { return (currChar == arg.Alias()); }); if (itr2 == m_arguments.end()) { - return CommandException(Resource::String::AdjoinedNotFoundError, currArg); + return CommandException(Resource::String::AdjoinedNotFoundError(currArg)); } else if (itr2->Type() != ArgumentType::Flag) { - return CommandException(Resource::String::AdjoinedNotFlagError, currArg); + return CommandException(Resource::String::AdjoinedNotFlagError(currArg)); } else { @@ -541,7 +507,7 @@ namespace AppInstaller::CLI } else { - return CommandException(Resource::String::SingleCharAfterDashError, currArg); + return CommandException(Resource::String::SingleCharAfterDashError(currArg)); } } else @@ -584,7 +550,7 @@ namespace AppInstaller::CLI { if (hasValue) { - return CommandException(Resource::String::FlagContainAdjoinedError, currArg); + return CommandException(Resource::String::FlagContainAdjoinedError(currArg)); } m_executionArgs.AddArg(arg.ExecArgType()); @@ -604,7 +570,7 @@ namespace AppInstaller::CLI if (!argFound) { - return CommandException(Resource::String::InvalidNameError, currArg); + return CommandException(Resource::String::InvalidNameError(currArg)); } } @@ -661,36 +627,36 @@ namespace AppInstaller::CLI { auto setting = Settings::AdminSettingToString(arg.AdminSetting()); AICLI_LOG(CLI, Error, << "Trying to use argument: " << arg.Name() << " disabled by admin setting " << setting); - throw CommandException(Resource::String::FeatureDisabledByAdminSettingMessage, Utility::LocIndView{ setting }, {}); + throw CommandException(Resource::String::FeatureDisabledByAdminSettingMessage(setting)); } if (!ExperimentalFeature::IsEnabled(arg.Feature()) && execArgs.Contains(arg.ExecArgType())) { auto feature = ExperimentalFeature::GetFeature(arg.Feature()); AICLI_LOG(CLI, Error, << "Trying to use argument: " << arg.Name() << " without enabling feature " << feature.JsonName()); - throw CommandException(Resource::String::FeatureDisabledMessage, feature.JsonName()); + throw CommandException(Resource::String::FeatureDisabledMessage(feature.JsonName())); } if (arg.Required() && !execArgs.Contains(arg.ExecArgType())) { - throw CommandException(Resource::String::RequiredArgError, arg.Name()); + throw CommandException(Resource::String::RequiredArgError(arg.Name())); } if (arg.Limit() < execArgs.GetCount(arg.ExecArgType())) { - throw CommandException(Resource::String::TooManyArgError, arg.Name()); + throw CommandException(Resource::String::TooManyArgError(arg.Name())); } } if (execArgs.Contains(Execution::Args::Type::Silent) && execArgs.Contains(Execution::Args::Type::Interactive)) { - throw CommandException(Resource::String::TooManyBehaviorsError, s_Command_ArgName_SilentAndInteractive); + throw CommandException(Resource::String::TooManyBehaviorsError(s_Command_ArgName_SilentAndInteractive)); } if (execArgs.Contains(Execution::Args::Type::CustomHeader) && !execArgs.Contains(Execution::Args::Type::Source) && !execArgs.Contains(Execution::Args::Type::SourceName)) { - throw CommandException(Resource::String::HeaderArgumentNotApplicableWithoutSource, Argument::ForType(Execution::Args::Type::CustomHeader).Name()); + throw CommandException(Resource::String::HeaderArgumentNotApplicableWithoutSource(Argument::ForType(Execution::Args::Type::CustomHeader).Name())); } if (execArgs.Contains(Execution::Args::Type::Count)) @@ -719,7 +685,9 @@ namespace AppInstaller::CLI { applicableArchitectures.emplace_back(Utility::ToString(i)); } - throw CommandException(Resource::String::InvalidArgumentValueError, Argument::ForType(Execution::Args::Type::InstallArchitecture).Name(), std::forward<std::vector<Utility::LocIndString>>((applicableArchitectures))); + + auto validOptions = Utility::Join(", "_liv, applicableArchitectures); + throw CommandException(Resource::String::InvalidArgumentValueError(Argument::ForType(Execution::Args::Type::InstallArchitecture).Name(), validOptions)); } } @@ -727,7 +695,7 @@ namespace AppInstaller::CLI { if (!Locale::IsWellFormedBcp47Tag(execArgs.GetArg(Execution::Args::Type::Locale))) { - throw CommandException(Resource::String::InvalidArgumentValueErrorWithoutValidValues, Argument::ForType(Execution::Args::Type::Locale).Name(), {}); + throw CommandException(Resource::String::InvalidArgumentValueErrorWithoutValidValues(Argument::ForType(Execution::Args::Type::Locale).Name())); } } diff --git a/src/AppInstallerCLICore/Command.h b/src/AppInstallerCLICore/Command.h @@ -23,25 +23,10 @@ namespace AppInstaller::CLI struct CommandException { CommandException(Resource::LocString message) : m_message(std::move(message)) {} - - // The message should be a localized string. - // The parameters can be either localized or not. - // We 'convert' the param to a localization independent view here if needed. - CommandException(Resource::LocString message, Resource::LocString param) : m_message(std::move(message)), m_params({ param }) {} - CommandException(Resource::LocString message, std::string_view param) : m_message(std::move(message)), m_params({ Utility::LocIndString{ param } }) {} - - // The message should be a localized string, but the replacement and parameters are not. - // This supports replacing %1 in the message with the replace value. - CommandException(Resource::LocString message, Utility::LocIndView replace, std::vector<Utility::LocIndString>&& params) : - m_message(std::move(message)), m_replace(replace), m_params(std::move(params)) {} - - const Utility::LocIndString Message() const; - const std::vector<Utility::LocIndString>& Params() const { return m_params; } + const Utility::LocIndString Message() const { return m_message; } private: Resource::LocString m_message; - std::optional<Utility::LocIndString> m_replace; - std::vector<Utility::LocIndString> m_params; }; // Flags to control the behavior of the command output. diff --git a/src/AppInstallerCLICore/Commands/FeaturesCommand.cpp b/src/AppInstallerCLICore/Commands/FeaturesCommand.cpp @@ -53,7 +53,7 @@ namespace AppInstaller::CLI { table.OutputLine({ std::string{ feature.Name() }, - Resource::Loader::Instance().ResolveString(ExperimentalFeature::IsEnabled(feature.GetFeature()) ? Resource::String::FeaturesEnabled : Resource::String::FeaturesDisabled), + Resource::LocString{ ExperimentalFeature::IsEnabled(feature.GetFeature()) ? Resource::String::FeaturesEnabled : Resource::String::FeaturesDisabled}, std::string { feature.JsonName() }, std::string{ feature.Link() } }); } diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -117,7 +117,8 @@ namespace AppInstaller::CLI { if (ConvertToScopeEnum(execArgs.GetArg(Args::Type::InstallScope)) == Manifest::ScopeEnum::Unknown) { - throw CommandException(Resource::String::InvalidArgumentValueError, s_ArgumentName_Scope, { "user"_lis, "machine"_lis }); + auto validOptions = Utility::Join(", "_liv, std::vector<Utility::LocIndString>{ "user"_lis, "machine"_lis}); + throw CommandException(Resource::String::InvalidArgumentValueError(s_ArgumentName_Scope, validOptions)); } } } diff --git a/src/AppInstallerCLICore/Commands/RootCommand.cpp b/src/AppInstallerCLICore/Commands/RootCommand.cpp @@ -187,15 +187,15 @@ namespace AppInstaller::CLI info << std::endl << "Windows: "_liv << Runtime::GetOSVersion() << std::endl; - info << Resource::String::SystemArchitecture << ": "_liv << Utility::ToString(Utility::GetSystemArchitecture()) << std::endl; + info << Resource::String::SystemArchitecture(Utility::ToString(Utility::GetSystemArchitecture())) << std::endl; if (Runtime::IsRunningInPackagedContext()) { - info << Resource::String::Package << ": "_liv << Runtime::GetPackageVersion() << std::endl; + info << Resource::String::Package(Runtime::GetPackageVersion()) << std::endl; }; - info << std::endl << Resource::String::Logs << ": "_liv << Runtime::GetPathTo(Runtime::PathName::DefaultLogLocationForDisplay).u8string() << std::endl; - info << std::endl << Resource::String::UserSettings << ": "_liv << UserSettings::SettingsFilePath(true).u8string() << std::endl; + info << std::endl << Resource::String::Logs(Utility::LocIndView{ Runtime::GetPathTo(Runtime::PathName::DefaultLogLocationForDisplay).u8string() }) << std::endl; + info << std::endl << Resource::String::UserSettings(Utility::LocIndView{ UserSettings::SettingsFilePath(true).u8string() }) << std::endl; info << std::endl; diff --git a/src/AppInstallerCLICore/Commands/SettingsCommand.cpp b/src/AppInstallerCLICore/Commands/SettingsCommand.cpp @@ -1,111 +1,121 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "SettingsCommand.h" -#include "Workflows/WorkflowBase.h" -#include "Workflows/SettingsFlow.h" - -namespace AppInstaller::CLI -{ - using namespace Utility::literals; - using namespace AppInstaller::Settings; - using namespace std::string_view_literals; - - namespace - { - constexpr Utility::LocIndView s_ArgumentName_Enable = "enable"_liv; - constexpr Utility::LocIndView s_ArgumentName_Disable = "disable"_liv; - constexpr Utility::LocIndView s_ArgName_EnableAndDisable = "enable|disable"_liv; - static constexpr std::string_view s_SettingsCommand_HelpLink = "https://aka.ms/winget-settings"sv; - } - - std::vector<std::unique_ptr<Command>> SettingsCommand::GetCommands() const - { - return InitializeFromMoveOnly<std::vector<std::unique_ptr<Command>>>({ - std::make_unique<SettingsExportCommand>(FullName()), - }); - } - - std::vector<Argument> SettingsCommand::GetArguments() const - { - return { - Argument{ s_ArgumentName_Enable, Argument::NoAlias, Execution::Args::Type::AdminSettingEnable, Resource::String::AdminSettingEnableDescription, ArgumentType::Standard, Argument::Visibility::Help }, - Argument{ s_ArgumentName_Disable, Argument::NoAlias, Execution::Args::Type::AdminSettingDisable, Resource::String::AdminSettingDisableDescription, ArgumentType::Standard, Argument::Visibility::Help }, - }; - } - - Resource::LocString SettingsCommand::ShortDescription() const - { - return { Resource::String::SettingsCommandShortDescription }; - } - - Resource::LocString SettingsCommand::LongDescription() const - { - return { Resource::String::SettingsCommandLongDescription }; - } - - std::string SettingsCommand::HelpLink() const - { - return std::string{ s_SettingsCommand_HelpLink }; - } - - void SettingsCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const - { - if (execArgs.Contains(Execution::Args::Type::AdminSettingEnable) && execArgs.Contains(Execution::Args::Type::AdminSettingDisable)) - { - throw CommandException(Resource::String::TooManyAdminSettingArgumentsError, s_ArgName_EnableAndDisable); - } - - if (execArgs.Contains(Execution::Args::Type::AdminSettingEnable) && AdminSetting::Unknown == StringToAdminSetting(execArgs.GetArg(Execution::Args::Type::AdminSettingEnable))) - { - throw CommandException(Resource::String::InvalidArgumentValueError, s_ArgumentName_Enable, { "LocalManifestFiles"_lis }); - } - - if (execArgs.Contains(Execution::Args::Type::AdminSettingDisable) && AdminSetting::Unknown == StringToAdminSetting(execArgs.GetArg(Execution::Args::Type::AdminSettingDisable))) - { - throw CommandException(Resource::String::InvalidArgumentValueError, s_ArgumentName_Disable, { "LocalManifestFiles"_lis }); - } - } - - void SettingsCommand::ExecuteInternal(Execution::Context& context) const - { - if (context.Args.Contains(Execution::Args::Type::AdminSettingEnable)) - { - context << - Workflow::EnsureRunningAsAdmin << - Workflow::EnableAdminSetting; - - } - else if (context.Args.Contains(Execution::Args::Type::AdminSettingDisable)) - { - context << - Workflow::EnsureRunningAsAdmin << - Workflow::DisableAdminSetting; - } - else - { - context << Workflow::OpenUserSetting; - } - } - - Resource::LocString SettingsExportCommand::ShortDescription() const - { - return { Resource::String::SettingsExportCommandShortDescription }; - } - - Resource::LocString SettingsExportCommand::LongDescription() const - { - return { Resource::String::SettingsExportCommandLongDescription }; - } - - std::string SettingsExportCommand::HelpLink() const - { - return std::string{ s_SettingsCommand_HelpLink }; - } - - void SettingsExportCommand::ExecuteInternal(Execution::Context& context) const - { - context << - Workflow::ExportSettings; - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "SettingsCommand.h" +#include "Workflows/WorkflowBase.h" +#include "Workflows/SettingsFlow.h" + +namespace AppInstaller::CLI +{ + using namespace Utility::literals; + using namespace AppInstaller::Settings; + using namespace std::string_view_literals; + + namespace + { + constexpr Utility::LocIndView s_ArgumentName_Enable = "enable"_liv; + constexpr Utility::LocIndView s_ArgumentName_Disable = "disable"_liv; + constexpr Utility::LocIndView s_ArgName_EnableAndDisable = "enable|disable"_liv; + static constexpr std::string_view s_SettingsCommand_HelpLink = "https://aka.ms/winget-settings"sv; + } + + std::vector<std::unique_ptr<Command>> SettingsCommand::GetCommands() const + { + return InitializeFromMoveOnly<std::vector<std::unique_ptr<Command>>>({ + std::make_unique<SettingsExportCommand>(FullName()), + }); + } + + std::vector<Argument> SettingsCommand::GetArguments() const + { + return { + Argument{ s_ArgumentName_Enable, Argument::NoAlias, Execution::Args::Type::AdminSettingEnable, Resource::String::AdminSettingEnableDescription, ArgumentType::Standard, Argument::Visibility::Help }, + Argument{ s_ArgumentName_Disable, Argument::NoAlias, Execution::Args::Type::AdminSettingDisable, Resource::String::AdminSettingDisableDescription, ArgumentType::Standard, Argument::Visibility::Help }, + }; + } + + Resource::LocString SettingsCommand::ShortDescription() const + { + return { Resource::String::SettingsCommandShortDescription }; + } + + Resource::LocString SettingsCommand::LongDescription() const + { + return { Resource::String::SettingsCommandLongDescription }; + } + + std::string SettingsCommand::HelpLink() const + { + return std::string{ s_SettingsCommand_HelpLink }; + } + + void SettingsCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + if (execArgs.Contains(Execution::Args::Type::AdminSettingEnable) && execArgs.Contains(Execution::Args::Type::AdminSettingDisable)) + { + throw CommandException(Resource::String::TooManyAdminSettingArgumentsError(s_ArgName_EnableAndDisable)); + } + + // Get admin setting string for all available options except Unknown + using AdminSetting_t = std::underlying_type_t<AdminSetting>; + std::vector<Utility::LocIndString> adminSettingList; + for (AdminSetting_t i = 1 + static_cast<AdminSetting_t>(AdminSetting::Unknown); i < static_cast<AdminSetting_t>(AdminSetting::Max); ++i) + { + adminSettingList.emplace_back(AdminSettingToString(static_cast<AdminSetting>(i))); + } + + Utility::LocIndString validOptions = Join(", "_liv, adminSettingList); + + if (execArgs.Contains(Execution::Args::Type::AdminSettingEnable) && AdminSetting::Unknown == StringToAdminSetting(execArgs.GetArg(Execution::Args::Type::AdminSettingEnable))) + { + throw CommandException(Resource::String::InvalidArgumentValueError(s_ArgumentName_Enable, validOptions)); + } + + if (execArgs.Contains(Execution::Args::Type::AdminSettingDisable) && AdminSetting::Unknown == StringToAdminSetting(execArgs.GetArg(Execution::Args::Type::AdminSettingDisable))) + { + throw CommandException(Resource::String::InvalidArgumentValueError(s_ArgumentName_Disable, validOptions)); + } + } + + void SettingsCommand::ExecuteInternal(Execution::Context& context) const + { + if (context.Args.Contains(Execution::Args::Type::AdminSettingEnable)) + { + context << + Workflow::EnsureRunningAsAdmin << + Workflow::EnableAdminSetting; + + } + else if (context.Args.Contains(Execution::Args::Type::AdminSettingDisable)) + { + context << + Workflow::EnsureRunningAsAdmin << + Workflow::DisableAdminSetting; + } + else + { + context << Workflow::OpenUserSetting; + } + } + + Resource::LocString SettingsExportCommand::ShortDescription() const + { + return { Resource::String::SettingsExportCommandShortDescription }; + } + + Resource::LocString SettingsExportCommand::LongDescription() const + { + return { Resource::String::SettingsExportCommandLongDescription }; + } + + std::string SettingsExportCommand::HelpLink() const + { + return std::string{ s_SettingsCommand_HelpLink }; + } + + void SettingsExportCommand::ExecuteInternal(Execution::Context& context) const + { + context << + Workflow::ExportSettings; + } +} diff --git a/src/AppInstallerCLICore/Commands/UninstallCommand.cpp b/src/AppInstallerCLICore/Commands/UninstallCommand.cpp @@ -101,12 +101,12 @@ namespace AppInstaller::CLI execArgs.Contains(Execution::Args::Type::Source) || execArgs.Contains(Execution::Args::Type::Exact))) { - throw CommandException(Resource::String::BothManifestAndSearchQueryProvided, ""); + throw CommandException(Resource::String::BothManifestAndSearchQueryProvided); } if (execArgs.Contains(Execution::Args::Type::Purge) && execArgs.Contains(Execution::Args::Type::Preserve)) { - throw CommandException(Resource::String::BothPurgeAndPreserveFlagsProvided, ""); + throw CommandException(Resource::String::BothPurgeAndPreserveFlagsProvided); } } diff --git a/src/AppInstallerCLICore/Core.cpp b/src/AppInstallerCLICore/Core.cpp @@ -136,7 +136,7 @@ namespace AppInstaller::CLI // Report any action blocked by Group Policy. auto policy = Settings::TogglePolicy::GetPolicy(e.Policy()); AICLI_LOG(CLI, Error, << "Operation blocked by Group Policy: " << policy.RegValueName()); - context.Reporter.Error() << Resource::String::DisabledByGroupPolicy << " : "_liv << policy.PolicyName() << std::endl; + context.Reporter.Error() << Resource::String::DisabledByGroupPolicy(policy.PolicyName()) << std::endl; return APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY; } diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -129,7 +129,7 @@ namespace AppInstaller::CLI::Portable if (std::filesystem::remove(filePath)) { AICLI_LOG(CLI, Info, << "Removed existing file at " << filePath); - m_stream << Resource::String::OverwritingExistingFileAtMessage << ' ' << filePath.u8string() << std::endl; + m_stream << Resource::String::OverwritingExistingFileAtMessage(Utility::LocIndView{ filePath.u8string() }) << std::endl; } if (Filesystem::CreateSymlink(entry.SymlinkTarget, filePath)) @@ -299,7 +299,7 @@ namespace AppInstaller::CLI::Portable else { AICLI_LOG(CLI, Info, << "Unable to remove install directory as there are remaining files in: " << InstallLocation); - m_stream << Resource::String::FilesRemainInInstallDirectory << ' ' << InstallLocation.u8string() << std::endl; + m_stream << Resource::String::FilesRemainInInstallDirectory(Utility::LocIndView{ InstallLocation.u8string() }) << std::endl; } } } diff --git a/src/AppInstallerCLICore/Resources.cpp b/src/AppInstallerCLICore/Resources.cpp @@ -7,41 +7,6 @@ using namespace AppInstaller::Utility::literals; namespace AppInstaller::CLI::Resource { - LocString::LocString(StringId id) : Utility::LocIndString(Loader::Instance().ResolveString(id)) {} - - const Loader& Loader::Instance() - { - static Loader instance; - return instance; - } - - Loader::Loader() : m_wingetLoader(nullptr) - { - try - { - // The default constructor of ResourceLoader throws a winrt::hresult_error exception - // when resource.pri is not found. ResourceLoader::GetForViewIndependentUse also throws - // a winrt::hresult_error but for reasons unknown it only gets catch when running on the - // debugger. Running without a debugger will result in a crash that not even adding a - // catch all fix. To provide a good error message we call the default constructor - // before calling GetForViewIndependentUse. - m_wingetLoader = winrt::Windows::ApplicationModel::Resources::ResourceLoader(); - m_wingetLoader = winrt::Windows::ApplicationModel::Resources::ResourceLoader::GetForViewIndependentUse(L"winget"); - } - catch (const winrt::hresult_error& hre) - { - // This message cannot be localized. - AICLI_LOG(CLI, Error, << "Failure loading resource file with error: " << hre.code()); - throw ResourceOpenException(hre); - } - } - - std::string Loader::ResolveString( - std::wstring_view resKey) const - { - return Utility::ConvertToUTF8(m_wingetLoader.GetString(resKey)); - } - Utility::LocIndView GetFixedString(FixedString fs) { switch (fs) @@ -51,9 +16,4 @@ namespace AppInstaller::CLI::Resource THROW_HR(E_UNEXPECTED); } - - ResourceOpenException::ResourceOpenException(const winrt::hresult_error& hre) - { - m_message = "Could not open the resource file: " + GetUserPresentableMessage(hre); - } } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once -#include <winget/LocIndependent.h> #include <winget/Resources.h> #include <winrt/Windows.ApplicationModel.Resources.h> @@ -11,6 +10,7 @@ namespace AppInstaller::CLI::Resource { using AppInstaller::StringResource::StringId; + using AppInstaller::Resource::LocString; // Resource string identifiers. // This list can mostly be generated by the following PowerShell: @@ -65,6 +65,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(DisableAdminSettingFailed); WINGET_DEFINE_RESOURCE_STRINGID(DisableInteractivityArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Done); + WINGET_DEFINE_RESOURCE_STRINGID(Downloading); WINGET_DEFINE_RESOURCE_STRINGID(EnableAdminSettingFailed); WINGET_DEFINE_RESOURCE_STRINGID(ExactArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalArgumentDescription); @@ -129,7 +130,6 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer1); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer2); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimerMSStore); - WINGET_DEFINE_RESOURCE_STRINGID(InstallationRequiresHigherWindows); WINGET_DEFINE_RESOURCE_STRINGID(InstallCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstallCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstalledPackageNotAvailable); @@ -445,36 +445,6 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(WordArgumentDescription); }; - // A localized string - struct LocString : public Utility::LocIndString - { - LocString() = default; - - LocString(StringId id); - - LocString(const LocString&) = default; - LocString& operator=(const LocString&) = default; - - LocString(LocString&&) = default; - LocString& operator=(LocString&&) = default; - }; - - // Utility class to load resources - class Loader - { - public: - // Gets the singleton instance of the resource loader. - static const Loader& Instance(); - - // Gets the string resource value. - std::string ResolveString(std::wstring_view resKey) const; - - private: - winrt::Windows::ApplicationModel::Resources::ResourceLoader m_wingetLoader; - - Loader(); - }; - // Fixed strings are not localized, but we use a similar system to prevent duplication enum class FixedString { @@ -482,21 +452,6 @@ namespace AppInstaller::CLI::Resource }; Utility::LocIndView GetFixedString(FixedString fs); - - struct ResourceOpenException : std::exception - { - ResourceOpenException(const winrt::hresult_error& hre); - - const char* what() const noexcept override { return m_message.c_str(); } - - private: - std::string m_message; - }; -} - -inline std::ostream& operator<<(std::ostream& out, AppInstaller::CLI::Resource::StringId si) -{ - return (out << AppInstaller::CLI::Resource::LocString{ si }); } inline std::ostream& operator<<(std::ostream& out, AppInstaller::CLI::Resource::FixedString fs) diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -92,7 +92,9 @@ namespace AppInstaller::CLI::Workflow else if (!std::filesystem::exists(nestedInstallerPath)) { AICLI_LOG(CLI, Error, << "Unable to locate nested installer at: " << nestedInstallerPath); - context.Reporter.Error() << Resource::String::NestedInstallerNotFound << ' ' << nestedInstallerPath << std::endl; + context.Reporter.Error() + << Resource::String::NestedInstallerNotFound(Utility::LocIndView{ nestedInstallerPath.u8string() }) + << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_NOT_FOUND); } else if (!IsPortableType(installer.NestedInstallerType)) diff --git a/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp b/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp @@ -31,7 +31,8 @@ namespace AppInstaller::CLI::Workflow if (matches.size() > 1) { - error << Resource::String::DependenciesFlowSourceTooManyMatches << " " << Utility::Normalize(dependencyNode.Id()); + auto dependencyNodeId = Utility::LocIndString{ Utility::Normalize(dependencyNode.Id()) }; + error << Resource::String::DependenciesFlowSourceTooManyMatches(dependencyNodeId); AICLI_LOG(CLI, Error, << "Too many matches for package " << dependencyNode.Id()); return DependencyNodeProcessorResult::Error; } @@ -52,14 +53,14 @@ namespace AppInstaller::CLI::Workflow if (!m_nodePackageLatestVersion) { - error << Resource::String::DependenciesFlowPackageVersionNotFound << " " << Utility::Normalize(packageId); + error << Resource::String::DependenciesFlowPackageVersionNotFound(Utility::LocIndView{ Utility::Normalize(packageId) }); AICLI_LOG(CLI, Error, << "Latest available version not found for package " << packageId); return DependencyNodeProcessorResult::Error; } if (!dependencyNode.IsVersionOk(Utility::Version(m_nodePackageLatestVersion->GetProperty(PackageVersionProperty::Version)))) { - error << Resource::String::DependenciesFlowNoMinVersion << " " << Utility::Normalize(packageId); + error << Resource::String::DependenciesFlowNoMinVersion(Utility::LocIndView{ Utility::Normalize(packageId) }); AICLI_LOG(CLI, Error, << "No suitable min version found for package " << packageId); return DependencyNodeProcessorResult::Error; } @@ -69,7 +70,7 @@ namespace AppInstaller::CLI::Workflow if (m_nodeManifest.Installers.empty()) { - error << Resource::String::DependenciesFlowNoInstallerFound << " " << Utility::Normalize(m_nodeManifest.Id); + error << Resource::String::DependenciesFlowNoInstallerFound(Utility::LocIndView{ Utility::Normalize(m_nodeManifest.Id) }); AICLI_LOG(CLI, Error, << "Installer not found for manifest " << m_nodeManifest.Id << " with version" << m_nodeManifest.Version); return DependencyNodeProcessorResult::Error; } @@ -85,7 +86,9 @@ namespace AppInstaller::CLI::Workflow if (!installer.has_value()) { - error << Resource::String::DependenciesFlowNoSuitableInstallerFound << " " << Utility::Normalize(m_nodeManifest.Id) << m_nodeManifest.Version; + auto manifestId = Utility::LocIndString{ Utility::Normalize(m_nodeManifest.Id) }; + auto manifestVersion = Utility::LocIndString{ m_nodeManifest.Version }; + error << Resource::String::DependenciesFlowNoSuitableInstallerFound(manifestId, manifestVersion); AICLI_LOG(CLI, Error, << "No suitable installer found for manifest " << m_nodeManifest.Id << " with version " << m_nodeManifest.Version); return DependencyNodeProcessorResult::Error; } diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp @@ -239,7 +239,7 @@ namespace AppInstaller::CLI::Workflow // Use the SHA256 hash of the installer as the identifier for the download downloadInfo.ContentId = SHA256::ConvertToString(installer.Sha256); - context.Reporter.Info() << "Downloading " << Execution::UrlEmphasis << installer.Url << std::endl; + context.Reporter.Info() << Resource::String::Downloading << ' ' << Execution::UrlEmphasis << installer.Url << std::endl; std::optional<std::vector<BYTE>> hash; diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -59,8 +59,8 @@ namespace AppInstaller::CLI::Workflow std::shared_ptr<IPackageVersion> GetAvailableVersionForInstalledPackage( Execution::Context& context, std::shared_ptr<IPackage> package, - std::string_view version, - std::string_view channel, + Utility::LocIndView version, + Utility::LocIndView channel, bool checkVersion) { if (!checkVersion) @@ -81,10 +81,7 @@ namespace AppInstaller::CLI::Workflow << "Installed package version is not available." << " Package Id [" << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << "], Version [" << version << "], Channel [" << channel << "]" << ". Found Version [" << availablePackageVersion->GetProperty(PackageVersionProperty::Version) << "], Channel [" << availablePackageVersion->GetProperty(PackageVersionProperty::Version) << "]"); - context.Reporter.Warn() - << Resource::String::InstalledPackageVersionNotAvailable - << ' ' << availablePackageVersion->GetProperty(PackageVersionProperty::Id) - << ' ' << version << ' ' << channel << std::endl; + context.Reporter.Warn() << Resource::String::InstalledPackageVersionNotAvailable(availablePackageVersion->GetProperty(PackageVersionProperty::Id), version, channel) << std::endl; } } @@ -106,12 +103,12 @@ namespace AppInstaller::CLI::Workflow auto channel = installedPackageVersion->GetProperty(PackageVersionProperty::Channel); // Find an available version of this package to determine its source. - auto availablePackageVersion = GetAvailableVersionForInstalledPackage(context, packageMatch.Package, version, channel, includeVersions); + auto availablePackageVersion = GetAvailableVersionForInstalledPackage(context, packageMatch.Package, Utility::LocIndView{ version }, Utility::LocIndView{ channel }, includeVersions); if (!availablePackageVersion) { // Report package not found and move to next package. AICLI_LOG(CLI, Warning, << "No available version of package [" << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << "] was found to export"); - context.Reporter.Warn() << Resource::String::InstalledPackageNotAvailable << ' ' << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << std::endl; + context.Reporter.Warn() << Resource::String::InstalledPackageNotAvailable(installedPackageVersion->GetProperty(PackageVersionProperty::Name)) << std::endl; continue; } @@ -123,7 +120,7 @@ namespace AppInstaller::CLI::Workflow { // Report that the package requires accepting license terms AICLI_LOG(CLI, Warning, << "Package [" << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << "] requires license agreement to install"); - context.Reporter.Warn() << Resource::String::ExportedPackageRequiresLicenseAgreement << ' ' << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << std::endl; + context.Reporter.Warn() << Resource::String::ExportedPackageRequiresLicenseAgreement(installedPackageVersion->GetProperty(PackageVersionProperty::Name)) << std::endl; } // Find the exported source for this package @@ -231,7 +228,9 @@ namespace AppInstaller::CLI::Workflow else { AICLI_LOG(CLI, Error, << "Missing required source: " << requiredSource.Details.Name); - context.Reporter.Warn() << Resource::String::ImportSourceNotInstalled << ' ' << requiredSource.Details.Name << std::endl; + context.Reporter.Warn() + << Resource::String::ImportSourceNotInstalled(Utility::LocIndView{ requiredSource.Details.Name }) + << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); } @@ -316,13 +315,13 @@ namespace AppInstaller::CLI::Workflow searchTerminationHR == APPINSTALLER_CLI_ERROR_PACKAGE_ALREADY_INSTALLED) { AICLI_LOG(CLI, Info, << "Package is already installed: [" << packageRequest.Id << "]"); - context.Reporter.Info() << Resource::String::ImportPackageAlreadyInstalled << ' ' << packageRequest.Id << std::endl; + context.Reporter.Info() << Resource::String::ImportPackageAlreadyInstalled(packageRequest.Id) << std::endl; continue; } else { AICLI_LOG(CLI, Info, << "Package not found for import: [" << packageRequest.Id << "], Version " << packageRequest.VersionAndChannel.ToString()); - context.Reporter.Info() << Resource::String::ImportSearchFailed << ' ' << packageRequest.Id << std::endl; + context.Reporter.Info() << Resource::String::ImportSearchFailed(packageRequest.Id) << std::endl; // Keep searching for the remaining packages and only fail at the end. foundAll = false; @@ -361,4 +360,4 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Error() << Resource::String::ImportInstallFailed << std::endl; } } -}- \ No newline at end of file +} diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -248,7 +248,7 @@ namespace AppInstaller::CLI::Workflow if (!installationNotes.empty()) { - context.Reporter.Info() << Resource::String::Notes << ' ' << installationNotes << std::endl; + context.Reporter.Info() << Resource::String::Notes(installationNotes) << std::endl; } } } @@ -398,17 +398,22 @@ namespace AppInstaller::CLI::Workflow if (m_isHResult) { - context.Reporter.Error() << Resource::String::InstallerFailedWithCode << ' ' << GetUserPresentableMessage(installResult) << std::endl; + context.Reporter.Error() + << Resource::String::InstallerFailedWithCode(Utility::LocIndView{ GetUserPresentableMessage(installResult) }) + << std::endl; } else { - context.Reporter.Error() << Resource::String::InstallerFailedWithCode << ' ' << installResult << std::endl; + context.Reporter.Error() + << Resource::String::InstallerFailedWithCode(installResult) + << std::endl; } // Show installer log path if exists if (context.Contains(Execution::Data::LogPath) && std::filesystem::exists(context.Get<Execution::Data::LogPath>())) { - context.Reporter.Info() << Resource::String::InstallerLogAvailable << ' ' << context.Get<Execution::Data::LogPath>().u8string() << std::endl; + auto installerLogPath = Utility::LocIndString{ context.Get<Execution::Data::LogPath>().u8string() }; + context.Reporter.Info() << Resource::String::InstallerLogAvailable(installerLogPath) << std::endl; } // Show a specific message if we can identify the return code diff --git a/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp @@ -112,6 +112,13 @@ namespace AppInstaller::CLI::Workflow } } + Utility::LocIndString GetErrorCodeString(const HRESULT errorCode) + { + std::ostringstream ssError; + ssError << WINGET_OSTREAM_FORMAT_HRESULT(errorCode); + return Utility::LocIndString{ ssError.str() }; + } + void MSStoreInstall(Execution::Context& context) { auto productId = Utility::ConvertToUTF16(context.Get<Execution::Data::Installer>()->ProductId); @@ -148,9 +155,10 @@ namespace AppInstaller::CLI::Workflow } else { - context.Reporter.Info() << Resource::String::MSStoreInstallOrUpdateFailed << ' ' << WINGET_OSTREAM_FORMAT_HRESULT(errorCode) << std::endl; + auto errorCodeString = GetErrorCodeString(errorCode); + context.Reporter.Info() << Resource::String::MSStoreInstallOrUpdateFailed(errorCodeString) << std::endl; context.Add<Execution::Data::OperationReturnCode>(errorCode); - AICLI_LOG(CLI, Error, << "MSStore install failed. ProductId: " << Utility::ConvertToUTF8(productId) << " HResult: " << WINGET_OSTREAM_FORMAT_HRESULT(errorCode)); + AICLI_LOG(CLI, Error, << "MSStore install failed. ProductId: " << Utility::ConvertToUTF8(productId) << " HResult: " << errorCodeString); AICLI_TERMINATE_CONTEXT(errorCode); } } @@ -196,9 +204,10 @@ namespace AppInstaller::CLI::Workflow } else { - context.Reporter.Info() << Resource::String::MSStoreInstallOrUpdateFailed << ' ' << WINGET_OSTREAM_FORMAT_HRESULT(errorCode) << std::endl; + auto errorCodeString = GetErrorCodeString(errorCode); + context.Reporter.Info() << Resource::String::MSStoreInstallOrUpdateFailed(errorCodeString) << std::endl; context.Add<Execution::Data::OperationReturnCode>(errorCode); - AICLI_LOG(CLI, Error, << "MSStore execution failed. ProductId: " << Utility::ConvertToUTF8(productId) << " HResult: " << WINGET_OSTREAM_FORMAT_HRESULT(errorCode)); + AICLI_LOG(CLI, Error, << "MSStore execution failed. ProductId: " << Utility::ConvertToUTF8(productId) << " HResult: " << errorCodeString); AICLI_TERMINATE_CONTEXT(errorCode); } } diff --git a/src/AppInstallerCLICore/Workflows/PromptFlow.cpp b/src/AppInstallerCLICore/Workflows/PromptFlow.cpp @@ -52,9 +52,10 @@ namespace AppInstaller::CLI::Workflow } // Show source agreements - std::string agreementsTitleMessage = Resource::LocString{ Resource::String::SourceAgreementsTitle }; - context.Reporter.Info() << Execution::SourceInfoEmphasis << - Utility::LocIndString{ Utility::FindAndReplaceMessageToken(agreementsTitleMessage, details.Name) } << std::endl; + context.Reporter.Info() + << Execution::SourceInfoEmphasis + << Resource::String::SourceAgreementsTitle(Utility::LocIndView{ details.Name }) + << std::endl; const auto& agreements = source.GetInformation().SourceAgreements; diff --git a/src/AppInstallerCLICore/Workflows/SettingsFlow.cpp b/src/AppInstallerCLICore/Workflows/SettingsFlow.cpp @@ -43,7 +43,7 @@ namespace AppInstaller::CLI::Workflow void EnableAdminSetting(Execution::Context& context) { - std::string_view adminSettingString = context.Args.GetArg(Execution::Args::Type::AdminSettingEnable); + auto adminSettingString = LocIndString{ context.Args.GetArg(Execution::Args::Type::AdminSettingEnable) }; AdminSetting adminSetting = Settings::StringToAdminSetting(adminSettingString); if (Settings::EnableAdminSetting(adminSetting)) { @@ -51,15 +51,13 @@ namespace AppInstaller::CLI::Workflow } else { - std::string adminSettingErrorMessage = Resource::LocString{ Resource::String::EnableAdminSettingFailed }; - context.Reporter.Error() << - Utility::LocIndString{ FindAndReplaceMessageToken(adminSettingErrorMessage, adminSettingString) }; + context.Reporter.Error() << Resource::String::EnableAdminSettingFailed(adminSettingString); } } void DisableAdminSetting(Execution::Context& context) { - std::string_view adminSettingString = context.Args.GetArg(Execution::Args::Type::AdminSettingDisable); + auto adminSettingString = LocIndString{ context.Args.GetArg(Execution::Args::Type::AdminSettingDisable) }; AdminSetting adminSetting = Settings::StringToAdminSetting(adminSettingString); if (Settings::DisableAdminSetting(adminSetting)) { @@ -67,9 +65,7 @@ namespace AppInstaller::CLI::Workflow } else { - std::string adminSettingErrorMessage = Resource::LocString{ Resource::String::DisableAdminSettingFailed }; - context.Reporter.Error() << - Utility::LocIndString{ FindAndReplaceMessageToken(adminSettingErrorMessage, adminSettingString) }; + context.Reporter.Error() << Resource::String::DisableAdminSettingFailed(adminSettingString); } } @@ -87,7 +83,7 @@ namespace AppInstaller::CLI::Workflow { if (warning.IsFieldWarning) { - warn << ' ' << Resource::String::SettingsWarningField << ' ' << warning.Path; + warn << ' ' << Resource::String::SettingsWarningField(warning.Path); } else { @@ -99,7 +95,7 @@ namespace AppInstaller::CLI::Workflow { if (warning.IsFieldWarning) { - warn << ' ' << Resource::String::SettingsWarningValue << ' ' << warning.Data; + warn << ' ' << Resource::String::SettingsWarningValue(warning.Data); } else { diff --git a/src/AppInstallerCLICore/Workflows/SourceFlow.cpp b/src/AppInstallerCLICore/Workflows/SourceFlow.cpp @@ -23,7 +23,7 @@ namespace AppInstaller::CLI::Workflow auto currentSources = Repository::Source::GetCurrentSources(); if (context.Args.Contains(Args::Type::SourceName)) { - std::string_view name = context.Args.GetArg(Args::Type::SourceName); + auto name = Utility::LocIndString{ context.Args.GetArg(Args::Type::SourceName) }; for (auto const& source : currentSources) { @@ -36,7 +36,7 @@ namespace AppInstaller::CLI::Workflow } } - context.Reporter.Error() << Resource::String::SourceListNoneFound << ' ' << name << std::endl; + context.Reporter.Error() << Resource::String::SourceListNoneFound(name) << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); } else @@ -145,24 +145,24 @@ namespace AppInstaller::CLI::Workflow Execution::TableOutput<2> table(context.Reporter, { Resource::String::SourceListField, Resource::String::SourceListValue }); - table.OutputLine({ Resource::Loader::Instance().ResolveString(Resource::String::SourceListName), source.Name }); - table.OutputLine({ Resource::Loader::Instance().ResolveString(Resource::String::SourceListType), source.Type }); - table.OutputLine({ Resource::Loader::Instance().ResolveString(Resource::String::SourceListArg), source.Arg }); - table.OutputLine({ Resource::Loader::Instance().ResolveString(Resource::String::SourceListData), source.Data }); - table.OutputLine({ Resource::Loader::Instance().ResolveString(Resource::String::SourceListIdentifier), source.Identifier }); + table.OutputLine({ Resource::LocString(Resource::String::SourceListName), source.Name }); + table.OutputLine({ Resource::LocString(Resource::String::SourceListType), source.Type }); + table.OutputLine({ Resource::LocString(Resource::String::SourceListArg), source.Arg }); + table.OutputLine({ Resource::LocString(Resource::String::SourceListData), source.Data }); + table.OutputLine({ Resource::LocString(Resource::String::SourceListIdentifier), source.Identifier }); if (source.LastUpdateTime == Utility::ConvertUnixEpochToSystemClock(0)) { table.OutputLine({ - Resource::Loader::Instance().ResolveString(Resource::String::SourceListUpdated), - Resource::Loader::Instance().ResolveString(Resource::String::SourceListUpdatedNever) + Resource::LocString(Resource::String::SourceListUpdated), + Resource::LocString(Resource::String::SourceListUpdatedNever) }); } else { std::ostringstream strstr; strstr << source.LastUpdateTime; - table.OutputLine({ Resource::Loader::Instance().ResolveString(Resource::String::SourceListUpdated), strstr.str() }); + table.OutputLine({ Resource::LocString(Resource::String::SourceListUpdated), strstr.str() }); } table.Complete(); @@ -196,7 +196,7 @@ namespace AppInstaller::CLI::Workflow for (const auto& sd : sources) { Repository::Source source{ sd.Name }; - context.Reporter.Info() << Resource::String::SourceUpdateOne << ' ' << sd.Name << "..."_liv << std::endl; + context.Reporter.Info() << Resource::String::SourceUpdateOne(Utility::LocIndView{ sd.Name }) << std::endl; auto updateFunction = [&](IProgressCallback& progress)->std::vector<Repository::SourceDetails> { return source.Update(progress); }; if (!context.Reporter.ExecuteWithProgress(updateFunction).empty()) { @@ -222,7 +222,7 @@ namespace AppInstaller::CLI::Workflow for (const auto& sd : sources) { Repository::Source source{ sd.Name }; - context.Reporter.Info() << Resource::String::SourceRemoveOne << ' ' << sd.Name << "..."_liv << std::endl; + context.Reporter.Info() << Resource::String::SourceRemoveOne(Utility::LocIndView{ sd.Name }) << std::endl; auto removeFunction = [&](IProgressCallback& progress)->bool { return source.Remove(progress); }; if (context.Reporter.ExecuteWithProgress(removeFunction)) { @@ -258,7 +258,7 @@ namespace AppInstaller::CLI::Workflow for (const auto& source : sources) { - context.Reporter.Info() << Resource::String::SourceResetOne << ' ' << source.Name << "..."_liv; + context.Reporter.Info() << Resource::String::SourceResetOne(Utility::LocIndView{ source.Name }); Repository::Source::DropSource(source.Name); context.Reporter.Info() << Resource::String::Done << std::endl; } diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp @@ -253,17 +253,22 @@ namespace AppInstaller::CLI::Workflow if (m_isHResult) { - context.Reporter.Error() << Resource::String::UninstallFailedWithCode << ' ' << GetUserPresentableMessage(uninstallResult) << std::endl; + context.Reporter.Error() + << Resource::String::UninstallFailedWithCode(Utility::LocIndView{ GetUserPresentableMessage(uninstallResult) }) + << std::endl; } else { - context.Reporter.Error() << Resource::String::UninstallFailedWithCode << ' ' << uninstallResult << std::endl; + context.Reporter.Error() + << Resource::String::UninstallFailedWithCode(uninstallResult) + << std::endl; } // Show installer log path if exists if (context.Contains(Execution::Data::LogPath) && std::filesystem::exists(context.Get<Execution::Data::LogPath>())) { - context.Reporter.Info() << Resource::String::InstallerLogAvailable << ' ' << context.Get<Execution::Data::LogPath>().u8string() << std::endl; + auto installerLogPath = Utility::LocIndString{ context.Get<Execution::Data::LogPath>().u8string() }; + context.Reporter.Info() << Resource::String::InstallerLogAvailable(installerLogPath) << std::endl; } AICLI_TERMINATE_CONTEXT(m_hr); diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -57,7 +57,7 @@ namespace AppInstaller::CLI::Workflow out << std::endl; } - Repository::Source OpenNamedSource(Execution::Context& context, std::string_view sourceName) + Repository::Source OpenNamedSource(Execution::Context& context, Utility::LocIndView sourceName) { Repository::Source source; @@ -72,7 +72,7 @@ namespace AppInstaller::CLI::Workflow if (!sourceName.empty() && !sources.empty()) { // A bad name was given, try to help. - context.Reporter.Error() << Resource::String::OpenSourceFailedNoMatch << ' ' << sourceName << std::endl; + context.Reporter.Error() << Resource::String::OpenSourceFailedNoMatch(sourceName) << std::endl; context.Reporter.Info() << Resource::String::OpenSourceFailedNoMatchHelp << std::endl; for (const auto& details : sources) { @@ -104,7 +104,7 @@ namespace AppInstaller::CLI::Workflow // We'll only report the source update failure as warning and continue for (const auto& s : updateFailures) { - context.Reporter.Warn() << Resource::String::SourceOpenWithFailedUpdate << ' ' << s.Name << std::endl; + context.Reporter.Warn() << Resource::String::SourceOpenWithFailedUpdate(Utility::LocIndView{ s.Name }) << std::endl; } } catch (const wil::ResultException& re) @@ -259,15 +259,10 @@ namespace AppInstaller::CLI::Workflow catch (const Settings::GroupPolicyException& e) { auto policy = Settings::TogglePolicy::GetPolicy(e.Policy()); - context.Reporter.Error() << Resource::String::DisabledByGroupPolicy << ": "_liv << policy.PolicyName() << std::endl; + auto policyNameId = policy.PolicyName(); + context.Reporter.Error() << Resource::String::DisabledByGroupPolicy(policyNameId) << std::endl; return APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY; } - catch (const Resource::ResourceOpenException& e) - { - Logging::Telemetry().LogException(Logging::FailureTypeEnum::ResourceOpen, e.what()); - context.Reporter.Error() << GetUserPresentableMessage(e) << std::endl; - return APPINSTALLER_CLI_ERROR_MISSING_RESOURCE_FILE; - } catch (const std::exception& e) { Logging::Telemetry().LogException(Logging::FailureTypeEnum::StdException, e.what()); @@ -306,7 +301,7 @@ namespace AppInstaller::CLI::Workflow } } - auto source = OpenNamedSource(context, sourceName); + auto source = OpenNamedSource(context, Utility::LocIndView{ sourceName }); if (context.IsTerminated()) { return; @@ -579,7 +574,7 @@ namespace AppInstaller::CLI::Workflow auto warn = context.Reporter.Warn(); for (const auto& failure : searchResult.Failures) { - warn << Resource::String::SearchFailureWarning << ' ' << failure.SourceName << std::endl; + warn << Resource::String::SearchFailureWarning(Utility::LocIndView{ failure.SourceName }) << std::endl; } } else @@ -588,7 +583,7 @@ namespace AppInstaller::CLI::Workflow auto error = context.Reporter.Error(); for (const auto& failure : searchResult.Failures) { - error << Resource::String::SearchFailureError << ' ' << failure.SourceName << std::endl; + error << Resource::String::SearchFailureError(Utility::LocIndView{ failure.SourceName }) << std::endl; HRESULT failureHR = HandleException(context, failure.Exception); // Just take first failure for now @@ -770,7 +765,7 @@ namespace AppInstaller::CLI::Workflow if (m_onlyShowUpgrades) { - context.Reporter.Info() << availableUpgradesCount << ' ' << Resource::String::AvailableUpgrades << std::endl; + context.Reporter.Info() << Resource::String::AvailableUpgrades(availableUpgradesCount) << std::endl; } } @@ -860,18 +855,17 @@ namespace AppInstaller::CLI::Workflow if (!manifest) { - auto errorStream = context.Reporter.Error(); - errorStream << Resource::String::GetManifestResultVersionNotFound << ' '; + std::ostringstream ssVersionInfo; if (!m_version.empty()) { - errorStream << m_version; + ssVersionInfo << m_version; } if (!m_channel.empty()) { - errorStream << '[' << m_channel << ']'; + ssVersionInfo << '[' << m_channel << ']'; } - errorStream << std::endl; + context.Reporter.Error() << Resource::String::GetManifestResultVersionNotFound(Utility::LocIndView{ ssVersionInfo.str()}) << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND); } @@ -899,13 +893,13 @@ namespace AppInstaller::CLI::Workflow if (!std::filesystem::exists(path)) { - context.Reporter.Error() << Resource::String::VerifyFileFailedNotExist << ' ' << path.u8string() << std::endl; + context.Reporter.Error() << Resource::String::VerifyFileFailedNotExist(Utility::LocIndView{ path.u8string() }) << std::endl; AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)); } if (std::filesystem::is_directory(path)) { - context.Reporter.Error() << Resource::String::VerifyFileFailedIsDirectory << ' ' << path.u8string() << std::endl; + context.Reporter.Error() << Resource::String::VerifyFileFailedIsDirectory(Utility::LocIndView{ path.u8string() }) << std::endl; AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_DIRECTORY_NOT_SUPPORTED)); } } @@ -916,7 +910,7 @@ namespace AppInstaller::CLI::Workflow if (!std::filesystem::exists(path)) { - context.Reporter.Error() << Resource::String::VerifyPathFailedNotExist << ' ' << path.u8string() << std::endl; + context.Reporter.Error() << Resource::String::VerifyPathFailedNotExist(Utility::LocIndView{ path.u8string() }) << std::endl; AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)); } } @@ -1030,8 +1024,9 @@ namespace AppInstaller::CLI::Workflow { if (!Settings::ExperimentalFeature::IsEnabled(m_feature)) { - context.Reporter.Error() << Resource::String::FeatureDisabledMessage << " : '" << - Settings::ExperimentalFeature::GetFeature(m_feature).JsonName() << '\'' << std::endl; + context.Reporter.Error() + << Resource::String::FeatureDisabledMessage(Utility::LocIndView{ Settings::ExperimentalFeature::GetFeature(m_feature).JsonName() }) + << std::endl; AICLI_LOG(CLI, Error, << Settings::ExperimentalFeature::GetFeature(m_feature).Name() << " feature is disabled. Execution cancelled."); AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED); } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -85,7 +85,7 @@ namespace AppInstaller::CLI::Workflow void operator()(Execution::Context& context) const override; private: - std::string_view m_sourceName; + Utility::LocIndView m_sourceName; }; // Creates a source object for a predefined source. diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -118,20 +118,24 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="AdjoinedNotFlagError" xml:space="preserve"> - <value>Adjoined alias is not a flag</value> + <value>Adjoined alias is not a flag: '{0}'</value> + <comment>{Locked="{0}"} Error message displayed when the user provides an adjoined alias that is not a flag argument. {0} is a placeholder replaced by the user input argument (e.g. '-ab').</comment> </data> <data name="AdjoinedNotFoundError" xml:space="preserve"> - <value>Adjoined flag alias not found</value> + <value>Adjoined flag alias not found: '{0}'</value> + <comment>{Locked="{0}"} Error message displayed when the user provides an adjoined flag alias argument that was not found. {0} is a placeholder replaced by the user input argument (e.g. '-ab').</comment> </data> <data name="AvailableArguments" xml:space="preserve"> <value>The following arguments are available:</value> + <comment>Message displayed to inform the user about the available command line arguments.</comment> </data> <data name="AvailableCommandAliases" xml:space="preserve"> <value>The following command aliases are available:</value> + <comment>Message displayed to inform the user about the available command line alias arguments.</comment> </data> <data name="AvailableCommands" xml:space="preserve"> <value>The following commands are available:</value> - <comment>Commands the tool supports</comment> + <comment>Title displayed to inform the user about the available commands.</comment> </data> <data name="AvailableHeader" xml:space="preserve"> <value>Available</value> @@ -139,23 +143,26 @@ </data> <data name="AvailableOptions" xml:space="preserve"> <value>The following options are available:</value> + <comment>Message displayed to inform the user about the available options.</comment> </data> <data name="AvailableSubcommands" xml:space="preserve"> <value>The following sub-commands are available:</value> - <comment>Nested commands that can be run in context of the selected command</comment> + <comment>Message displayed to inform the user about the available nested commands that run in context of the selected command.</comment> </data> <data name="AvailableUpgrades" xml:space="preserve"> - <value>upgrades available.</value> + <value>{0} upgrades available.</value> + <comment>{Locked="{0}"} Message displayed to inform the user about available package upgrades. {0} is a placeholder replaced by the number of package upgrades.</comment> </data> <data name="ChannelArgumentDescription" xml:space="preserve"> <value>Use the specified channel; default is general audience</value> </data> <data name="Command" xml:space="preserve"> <value>command</value> - <comment>A command to give the software</comment> + <comment>Label displayed for a command to give the software.</comment> </data> <data name="CommandArgumentDescription" xml:space="preserve"> <value>Filter results by command</value> + <comment>Description message displayed to inform the user about filtering the search results by a package command.</comment> </data> <data name="CommandLineArgumentDescription" xml:space="preserve"> <value>The full command line for completion</value> @@ -174,9 +181,11 @@ </data> <data name="Done" xml:space="preserve"> <value>Done</value> + <comment>Label displayed when an operation completes or is done executing.</comment> </data> <data name="ExactArgumentDescription" xml:space="preserve"> <value>Find package using exact match</value> + <comment>Description message displayed to inform the user about finding an application package using an exact matching criteria.</comment> </data> <data name="ExperimentalArgumentDescription" xml:space="preserve"> <value>Experimental argument for demonstration purposes</value> @@ -189,11 +198,12 @@ <value>Experimental feature example</value> </data> <data name="ExtraPositionalError" xml:space="preserve"> - <value>Found a positional argument when none was expected</value> + <value>Found a positional argument when none was expected: '{0}'</value> + <comment>{Locked="{0}"} Error message displayed when the user provides an extra positional argument when none was expected. {0} is a placeholder replaced by the user's extra argument input.</comment> </data> <data name="FeatureDisabledMessage" xml:space="preserve"> - <value>This feature is a work in progress, and may be changed dramatically or removed altogether in the future. To enable it, edit your settings ('winget settings') to include the experimental feature</value> - <comment>{Locked="winget settings"}</comment> + <value>This feature is a work in progress, and may be changed dramatically or removed altogether in the future. To enable it, edit your settings ('winget settings') to include the experimental feature: '{0}'</value> + <comment>{Locked="winget settings","{0}"}. Error message displayed when the user uses an experimental feature that is disabled. {0} is a placeholder replaced by the experimental feature name.</comment> </data> <data name="FeaturesCommandLongDescription" xml:space="preserve"> <value>Shows the status of experimental features. Experimental features can be turn on via 'winget settings'.</value> @@ -229,7 +239,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>File to be hashed</value> </data> <data name="FlagContainAdjoinedError" xml:space="preserve"> - <value>Flag argument cannot contain adjoined value</value> + <value>Flag argument cannot contain adjoined value: '{0}'</value> + <comment>{Locked="{0}"} Error message displayed when the user provides a flag argument containing an unexpected adjoined value. {0} is a placeholder replaced by the user input.</comment> </data> <data name="HashCommandLongDescription" xml:space="preserve"> <value>Computes the hash of a local file, appropriate for entry into a manifest. It can also compute the hash of the signature file of an MSIX package to enable streaming installations.</value> @@ -244,7 +255,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>For more details on a specific command, pass it the help argument.</value> </data> <data name="HelpLinkPreamble" xml:space="preserve"> - <value>More help can be found at:</value> + <value>More help can be found at: {0}</value> + <comment>{Locked="{0}"} Message displayed to inform the user about a link where they can learn more about the subject context. {0} is a placeholder replaced by a website address.</comment> </data> <data name="IdArgumentDescription" xml:space="preserve"> <value>Filter results by id</value> @@ -259,9 +271,6 @@ They can be configured through the settings file 'winget settings'.</value> <value>This package is provided through Microsoft Store. winget may need to acquire the package from Microsoft Store on behalf of the current user.</value> <comment>{Locked="winget"}</comment> </data> - <data name="InstallationRequiresHigherWindows" xml:space="preserve"> - <value>Cannot install package, as it requires a higher version of Windows:</value> - </data> <data name="InstallCommandLongDescription" xml:space="preserve"> <value>Installs the selected package, either found by searching a configured source or directly from a manifest. By default, the query must case-insensitively match the id, name, or moniker of the package. Other fields can be used by passing their appropriate option. By default, install command will check package installed status and try to perform an upgrade if applicable. Override with --force to perform a direct install.</value> <comment>{Locked="--force","id","name","moniker"}; id, name, and moniker are all named values in our context, and may benefit from not being translated. The match must be for any of them, with comparison ignoring case.</comment> @@ -299,13 +308,16 @@ They can be configured through the settings file 'winget settings'.</value> <value>Request interactive installation; user input may be needed</value> </data> <data name="InvalidAliasError" xml:space="preserve"> - <value>Argument alias was not recognized for the current command</value> + <value>Argument alias was not recognized for the current command: '{0}'</value> + <comment>{Locked="{0}"} Error message displayed when the user provides a command line argument alias that was not recognized for a selected command. {0} is a placeholder replaced by the user's argument alias input (e.g. '-a').</comment> </data> <data name="InvalidArgumentSpecifierError" xml:space="preserve"> - <value>Invalid argument specifier</value> + <value>Invalid argument specifier: '{0}'</value> + <comment>{Locked="{0}"} Error message displayed when the user provides an invalid argument specifier. {0} is a placeholder replaced by an argument specifier (e.g. '-').</comment> </data> <data name="InvalidNameError" xml:space="preserve"> - <value>Argument name was not recognized for the current command</value> + <value>Argument name was not recognized for the current command: '{0}'</value> + <comment>{Locked="{0}"} Error message displayed when the user provides an unrecognized command line argument name for the selected command. {0} is a placeholder replaced by the user's argument name input (e.g. '--example').</comment> </data> <data name="LocaleArgumentDescription" xml:space="preserve"> <value>Locale to use (BCP47 format)</value> @@ -351,7 +363,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>Manifest validation succeeded with warnings.</value> </data> <data name="MissingArgumentError" xml:space="preserve"> - <value>Argument value required, but none found</value> + <value>Argument value required, but none found: '{0}'</value> + <comment>{Locked="{0}"} Error message displayed when the user does not provide a required command line argument value. {0} is a placeholder replaced by the argument name.</comment> </data> <data name="MonikerArgumentDescription" xml:space="preserve"> <value>Filter results by moniker</value> @@ -366,7 +379,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>Failed to install or upgrade Microsoft Store package because the specific app is blocked by policy</value> </data> <data name="MSStoreInstallOrUpdateFailed" xml:space="preserve"> - <value>Failed to install or upgrade Microsoft Store package. Error code:</value> + <value>Failed to install or upgrade Microsoft Store package. Error code: {0}</value> + <comment>{Locked="{0}"} Error message displayed when a Microsoft Store application package fails to install or upgrade. {0} is a placeholder replaced by an error code.</comment> </data> <data name="MSStoreInstallGetEntitlementNetworkError" xml:space="preserve"> <value>Verifying/Requesting package acquisition failed: network error</value> @@ -422,8 +436,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>Override arguments to be passed on to the installer</value> </data> <data name="Package" xml:space="preserve"> - <value>Package</value> - <comment>A software package</comment> + <value>Package: {0}</value> + <comment>{Locked="{0}"} Label displayed for a software package. {0} is a placeholder replaced by the software package name.</comment> </data> <data name="PendingWorkError" xml:space="preserve"> <value>Oops, we forgot to do this...</value> @@ -441,7 +455,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>Progress display a rainbow of colors</value> </data> <data name="RequiredArgError" xml:space="preserve"> - <value>Required argument not provided</value> + <value>Required argument not provided: '{0}'</value> + <comment>{Locked="{0}"} Error message displayed when the user does not provide a required command line argument. {0} is a placeholder replaced by an argument name.</comment> </data> <data name="RetroArgumentDescription" xml:space="preserve"> <value>Progress display as the default color</value> @@ -502,7 +517,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>Request silent installation</value> </data> <data name="SingleCharAfterDashError" xml:space="preserve"> - <value>Only the single character alias can occur after a single -</value> + <value>Only the single character alias can occur after a single -: '{0}'</value> + <comment>{Locked="{0}"} Error message displayed when the user provides more than a single character command line alias argument after an alias argument specifier '-'. {0} is a placeholder replaced by the user's argument input.</comment> </data> <data name="SourceAddAlreadyExistsDifferentArg" xml:space="preserve"> <value>A source with the given name already exists and refers to a different location:</value> @@ -561,7 +577,8 @@ They can be configured through the settings file 'winget settings'.</value> <comment>The source's unique identifier.</comment> </data> <data name="SourceListNoneFound" xml:space="preserve"> - <value>Did not find a source named:</value> + <value>Did not find a source named: {0}</value> + <comment>Error message displayed when the user provides a repository source name that was not found. {0} is a placeholder replaced by the user input.</comment> </data> <data name="SourceListNoSources" xml:space="preserve"> <value>There are no sources configured.</value> @@ -603,7 +620,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>Remove current sources</value> </data> <data name="SourceRemoveOne" xml:space="preserve"> - <value>Removing source:</value> + <value>Removing source: {0}...</value> + <comment>{Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being removed. {0} is a placeholder replaced by the repository source name.</comment> </data> <data name="SourceResetAll" xml:space="preserve"> <value>Resetting all sources...</value> @@ -622,7 +640,8 @@ They can be configured through the settings file 'winget settings'.</value> <comment>{Locked="--force"}</comment> </data> <data name="SourceResetOne" xml:space="preserve"> - <value>Resetting source:</value> + <value>Resetting source: {0}...</value> + <comment>{Locked="{0}"} Message displayed to inform the user about a repository source that is currently being reset. {0} is a placeholder replaced by the repository source name.</comment> </data> <data name="SourceTypeArgumentDescription" xml:space="preserve"> <value>Type of the source</value> @@ -637,7 +656,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>Update current sources</value> </data> <data name="SourceUpdateOne" xml:space="preserve"> - <value>Updating source:</value> + <value>Updating source: {0}...</value> + <comment>{Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being updated. {0} is a placeholder replaced by the repository source name.</comment> </data> <data name="TagArgumentDescription" xml:space="preserve"> <value>Filter results by tag</value> @@ -658,16 +678,19 @@ They can be configured through the settings file 'winget settings'.</value> <value>Display the version of the tool</value> </data> <data name="TooManyArgError" xml:space="preserve"> - <value>Argument provided more times than allowed</value> + <value>Argument provided more times than allowed: '{0}'</value> + <comment>{Locked="{0}"} Error message displayed when the user provides a command line argument more times than it is allowed. {0} is a placeholder replaced by the user's argument name input.</comment> </data> <data name="TooManyBehaviorsError" xml:space="preserve"> - <value>More than one execution behavior argument provided</value> + <value>More than one execution behavior argument provided: '{0}'</value> + <comment>{Locked="{0}"} Error message displayed when the user provides more than one execution behavior argument when installing an application package. {0} is a placeholder replaced by the user specified execution behaviors (e.g. 'silent|interactive').</comment> </data> <data name="UnexpectedErrorExecutingCommand" xml:space="preserve"> <value>An unexpected error occurred while executing the command:</value> </data> <data name="UnrecognizedCommand" xml:space="preserve"> - <value>Unrecognized command</value> + <value>Unrecognized command: '{0}'</value> + <comment>{Locked="{0}"} Error message displayed when the user provides an unrecognized command. {0} is a placeholder replaced by the user input.</comment> </data> <data name="UpdateAllArgumentDescription" xml:space="preserve"> <value>Upgrade all installed packages to latest if available</value> @@ -683,8 +706,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>Shows and performs available upgrades</value> </data> <data name="Usage" xml:space="preserve"> - <value>usage</value> - <comment>The way to use the software</comment> + <value>usage: {0} {1}</value> + <comment>{Locked="{0}","{1}"} Message displayed to provide the user with instructions on how to use a command. {0} is a placeholder replaced by the program name (e.g. 'winget'). {1} is a placeholder replaced by the pattern for using the selected command.</comment> </data> <data name="ValidateCommandLongDescription" xml:space="preserve"> <value>Validates a manifest using a strict set of guidelines. This is intended to enable you to check your manifest before submitting to a repo.</value> @@ -711,10 +734,12 @@ They can be configured through the settings file 'winget settings'.</value> <value>The value provided before completion is requested</value> </data> <data name="GetManifestResultVersionNotFound" xml:space="preserve"> - <value>No version found matching:</value> + <value>No version found matching: {0}</value> + <comment>{Locked="{0}"} Error message displayed when the user attempts to upgrade an application package to a version that was not found. {0} is a placeholder replaced by the user's provided upgrade package version.</comment> </data> <data name="OpenSourceFailedNoMatch" xml:space="preserve"> - <value>No sources match the given value:</value> + <value>No sources match the given value: {0}</value> + <comment>{Locked="{0}"} Error message displayed when the user attempts to install or upgrade an application package from a repository source that was not found. {0} is a placeholder replaced by the user's repository source name input.</comment> </data> <data name="OpenSourceFailedNoMatchHelp" xml:space="preserve"> <value>The configured sources are:</value> @@ -727,17 +752,19 @@ They can be configured through the settings file 'winget settings'.</value> <value>Found</value> </data> <data name="VerifyFileFailedIsDirectory" xml:space="preserve"> - <value>Path is a directory:</value> + <value>Path is a directory: {0}</value> + <comment>{Locked="{0}"} Error message displayed when the user provides a system path that is a directory. {0} is a placeholder replaced by the provided directory path.</comment> </data> <data name="VerifyFileFailedNotExist" xml:space="preserve"> - <value>File does not exist:</value> + <value>File does not exist: {0}</value> + <comment>{Locked="{0}"} Error message displayed when the user provides a system file that does not exist. {0} is a placeholder replaced by the provided file path.</comment> </data> <data name="BothManifestAndSearchQueryProvided" xml:space="preserve"> <value>Both local manifest and search query arguments are provided</value> </data> <data name="Logs" xml:space="preserve"> - <value>Logs</value> - <comment>Diagnostic files containing information about application use.</comment> + <value>Logs: {0}</value> + <comment>{Locked="{0}"} Label displayed for diagnostic files containing information about the application use. {0} is a placeholder replaced by the logs directory path.</comment> </data> <data name="InstallerBlockedByPolicy" xml:space="preserve"> <value>The installer is blocked by policy</value> @@ -749,7 +776,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>An anti-virus product reports an infection in the installer</value> </data> <data name="SourceOpenWithFailedUpdate" xml:space="preserve"> - <value>Failed in attempting to update the source:</value> + <value>Failed in attempting to update the source: {0}</value> + <comment>{Locked="{0}"} Error message displayed when an attempt to update the repository source fails. {0} is a placeholder replaced by the repository source name.</comment> </data> <data name="UninstallCommandLongDescription" xml:space="preserve"> <value>Uninstalls the selected package, either found by searching the installed packages list or directly from a manifest. By default, the query must case-insensitively match the id, name, or moniker of the package. Other fields can be used by passing their appropriate option.</value> @@ -772,7 +800,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>Uninstallation abandoned</value> </data> <data name="UninstallFailedWithCode" xml:space="preserve"> - <value>Uninstall failed with exit code:</value> + <value>Uninstall failed with exit code: {0}</value> + <comment>{Locked="{0}"} Error message displayed when an attempt to uninstall an application package fails. {0} is a placeholder replaced by an error code.</comment> </data> <data name="ExportCommandShortDescription" xml:space="preserve"> <value>Exports a list of the installed packages</value> @@ -800,16 +829,20 @@ They can be configured through the settings file 'winget settings'.</value> <value>One or more imported packages failed to install</value> </data> <data name="ImportSearchFailed" xml:space="preserve"> - <value>Package not found for import:</value> + <value>Package not found for import: {0}</value> + <comment>{Locked="{0}"} Error message displayed when the user attempts to import an application package that was not found. {0} is a placeholder replaced by the import package name .</comment> </data> <data name="ImportSourceNotInstalled" xml:space="preserve"> - <value>Source required for import is not installed:</value> + <value>Source required for import is not installed: {0}</value> + <comment>{Locked="{0}"} Error message displayed when the user attempts to import application package(s) from a repository source that is not installed. {0} is a placeholder replaced by the repository source name.</comment> </data> <data name="InstalledPackageNotAvailable" xml:space="preserve"> - <value>Installed package is not available from any source:</value> + <value>Installed package is not available from any source: {0}</value> + <comment>{Locked="{0}"} Warning message displayed when the user attempts to export an installed application package that is not available from any repository source. {0} is a placeholder replaced by the installed package name.</comment> </data> <data name="InstalledPackageVersionNotAvailable" xml:space="preserve"> - <value>Installed version of package is not available from any source:</value> + <value>Installed version of package is not available from any source: {0} {1} {2}</value> + <comment>{Locked="{0}","{1}","{2}"} Warning message displayed when the user attempts to export an installed application package with a version that is not available from any repository source. {0} is a placeholder replaced by the installed package identifier. {1} is a placeholder replaced by the installed package version. {2} is a placeholder replaced by the installed package channel.</comment> </data> <data name="NoPackagesInImportFile" xml:space="preserve"> <value>No packages found in import file</value> @@ -818,7 +851,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>JSON file is not valid</value> </data> <data name="ImportPackageAlreadyInstalled" xml:space="preserve"> - <value>Package is already installed:</value> + <value>Package is already installed: {0}</value> + <comment>{Locked="{0}"} Message displayed to inform the user that an import application package is already installed. {0} is a placeholder replaced by the package identifier.</comment> </data> <data name="ImportIgnoreUnavailableArgumentDescription" xml:space="preserve"> <value>Ignore unavailable packages</value> @@ -830,7 +864,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>Ignore package versions from import file</value> </data> <data name="VerifyPathFailedNotExist" xml:space="preserve"> - <value>Path does not exist:</value> + <value>Path does not exist: {0}</value> + <comment>{Locked="{0}"} Error message displayed when the user provides a system path argument value that does not exist. {0} is a placeholder replaced by the user's provided path.</comment> </data> <data name="ImportFileHasInvalidSchema" xml:space="preserve"> <value>The JSON file does not specify a recognized schema.</value> @@ -840,11 +875,12 @@ They can be configured through the settings file 'winget settings'.</value> <comment>This argument allows the user to select between installing for just the user or for the entire machine.</comment> </data> <data name="InvalidArgumentValueError" xml:space="preserve"> - <value>The value provided for the `%1` argument is invalid; valid values are</value> - <comment>{Locked="%1"} The value will be replaced with the argument name</comment> + <value>The value provided for the `{0}` argument is invalid; valid values are: {1}</value> + <comment>{Locked="{0}","{1}"} Error message displayed when the user provides an invalid command line argument value. {0} is a placeholder replaced by the argument name. {1} is a placeholder replaced by a list of valid options.</comment> </data> <data name="DisabledByGroupPolicy" xml:space="preserve"> - <value>This operation is disabled by Group Policy</value> + <value>This operation is disabled by Group Policy: {0}</value> + <comment>{Locked="{0}"} Error message displayed when the user performs a command operation that is disabled by a group policy. {0} is a placeholder replaced by a group policy description.</comment> </data> <data name="PolicyAdditionalSources" xml:space="preserve"> <value>Enable Additional Windows App Installer Sources</value> @@ -896,7 +932,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>Enable Windows App Installer Local Archive Malware Scan Override</value> </data> <data name="SettingsWarningField" xml:space="preserve"> - <value>Field:</value> + <value>Field: {0}</value> + <comment>{Locked="{0}"} Warning message displayed when a user setting field has invalid syntax or semantics. {0} is a placeholder replaced by the setting field path.</comment> </data> <data name="SettingsWarningInvalidFieldFormat" xml:space="preserve"> <value>Invalid field format.</value> @@ -914,7 +951,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>Error parsing file:</value> </data> <data name="SettingsWarningValue" xml:space="preserve"> - <value>Value:</value> + <value>Value: {0}</value> + <comment>{Locked="{0}"} Warning message displayed when a user setting value has invalid syntax or semantics. {0} is a placeholder replaced by the setting data value.</comment> </data> <data name="FeaturesMessageDisabledByPolicy" xml:space="preserve"> <value>The following experimental features are in progress. @@ -941,8 +979,8 @@ Configuration is disabled due to Group Policy.</value> <comment>A source that the user is allowed to add.</comment> </data> <data name="InvalidArgumentValueErrorWithoutValidValues" xml:space="preserve"> - <value>The value provided for the `%1` argument is invalid</value> - <comment>{Locked="%1"} The value will be replaced with the argument name</comment> + <value>The value provided for the `{0}` argument is invalid</value> + <comment>{Locked="{0}"} Error message displayed when the user provides an invalid command line argument value. {0} is a placeholder replaced by the argument name.</comment> </data> <data name="Cancelled" xml:space="preserve"> <value>Cancelled</value> @@ -965,18 +1003,20 @@ Configuration is disabled due to Group Policy.</value> <value>Dependency source not found</value> </data> <data name="DependenciesFlowSourceTooManyMatches" xml:space="preserve"> - <value>Package search yield more than one result.</value> - <comment>When node package id search yield too many matches</comment> + <value>Package search yield more than one result: {0}</value> + <comment>{Locked="{0}"} Error message displayed when application packages search yield more than one result. {0} is a placeholder replaced by the dependency package identifier.</comment> </data> <data name="DependenciesFlowPackageVersionNotFound" xml:space="preserve"> - <value>Latest version not found for package</value> - <comment>When no suitable version found for the specific package.</comment> + <value>Latest version not found for package: {0}</value> + <comment>{Locked="{0}"} Error message displayed when no suitable version found for the specific application package. {0} is a placeholder replaced by the package identifier.</comment> </data> <data name="DependenciesFlowNoInstallerFound" xml:space="preserve"> - <value>No installers found</value> + <value>No installers found: {0}</value> + <comment>{Locked="{0}"} Error message displayed when no installer found for a manifest. {0} is a placeholder replaced by the manifest identifier.</comment> </data> <data name="DependenciesFlowNoMinVersion" xml:space="preserve"> - <value>Minimum required version not available for package</value> + <value>Minimum required version not available for package: {0}</value> + <comment>{Locked="{0}"} Error message displayed when the minimum required version is not available for an application package. {0} is a placeholder replaced by the package identifier.</comment> </data> <data name="DependenciesFlowNoMatches" xml:space="preserve"> <value>No matches</value> @@ -987,8 +1027,8 @@ Configuration is disabled due to Group Policy.</value> <comment>Dependency graph has loop</comment> </data> <data name="DependenciesFlowNoSuitableInstallerFound" xml:space="preserve"> - <value>No suitable installer found for manifest</value> - <comment>Attempt to get preferred installer for manifest failed.</comment> + <value>No suitable installer found for manifest: {0} {1}</value> + <comment>{Locked="{0}","{1}"} Error message displayed when an attempt to get a preferred installer for a manifest fails. {0} is a placeholder replaced by the manifest identifier. {1} is a placeholder replaced by the manifest version.</comment> </data> <data name="DependenciesManagementError" xml:space="preserve"> <value>Error processing package dependencies, do you wish to continue installation?</value> @@ -1025,7 +1065,8 @@ Configuration is disabled due to Group Policy.</value> <value>Accept all license agreements for packages</value> </data> <data name="ExportedPackageRequiresLicenseAgreement" xml:space="preserve"> - <value>Exported package requires license agreement to install:</value> + <value>Exported package requires license agreement to install: {0}</value> + <comment>{Locked="{0}"} Warning message displayed when an exported application package requires license agreement to install. {0} is a placeholder replaced by the package name.</comment> </data> <data name="PackageAgreementsPrompt" xml:space="preserve"> <value>The publisher requires that you view the above information and accept the agreements before installing. @@ -1110,8 +1151,8 @@ Do you agree to the terms?</value> <value>Accept all source agreements during source operations</value> </data> <data name="SourceAgreementsTitle" xml:space="preserve"> - <value>The `%1` source requires that you view the following agreements before using.</value> - <comment>{Locked="%1"} The value will be replaced with the source name</comment> + <value>The `{0}` source requires that you view the following agreements before using.</value> + <comment>{Locked="{0}"} Message displayed to inform the user that a repository source requires viewing agreements before using. {0} is a placeholder replaced by the repository source name.</comment> </data> <data name="SourceAgreementsPrompt" xml:space="preserve"> <value>Do you agree to all the source agreements terms?</value> @@ -1132,7 +1173,8 @@ Do you agree to the terms?</value> <value>Ignoring the optional header as it is not applicable for this source.</value> </data> <data name="HeaderArgumentNotApplicableWithoutSource" xml:space="preserve"> - <value>The optional header is not applicable without specifying a source</value> + <value>The optional header is not applicable without specifying a source: '{0}'</value> + <comment>{Locked="{0}"} Error message displayed when the user performs an operation (e.g install) and provides the HTTP 'header' argument without specifying the repository source. {0} is a placeholder replaced by the header argument name.</comment> </data> <data name="ShowLabelInstallerReleaseDate" xml:space="preserve"> <value>Release Date:</value> @@ -1162,14 +1204,16 @@ Do you agree to the terms?</value> <value>Release Notes Url:</value> </data> <data name="SearchFailureWarning" xml:space="preserve"> - <value>Failed when searching source; results will not be included:</value> + <value>Failed when searching source; results will not be included: {0}</value> + <comment>{Locked="{0}"} Warning message displayed when searching a repository source fails. {0} is a placeholder replaced by the repository source name.</comment> </data> <data name="SearchFailureError" xml:space="preserve"> - <value>Failed when searching source:</value> + <value>Failed when searching source: {0}</value> + <comment>{Locked="{0}"} Error message displayed when searching a repository source fails. {0} is a placeholder replaced by the repository source name.</comment> </data> <data name="FeatureDisabledByAdminSettingMessage" xml:space="preserve"> - <value>This feature needs to be enabled by administrators. To enable it, run 'winget settings --enable %1' as administrator.</value> - <comment>{Locked="winget settings --enable %1"} The value will be replaced with the admin setting</comment> + <value>This feature needs to be enabled by administrators. To enable it, run 'winget settings --enable {0}' as administrator.</value> + <comment>{Locked="winget settings --enable", "{0}"}. Error message displayed when the user uses a feature that needs to be enabled by administrators. {0} is a placeholder replaced by the admin setting.</comment> </data> <data name="AdminSettingEnableDescription" xml:space="preserve"> <value>Enables the specific administrator setting</value> @@ -1178,7 +1222,8 @@ Do you agree to the terms?</value> <value>Disables the specific administrator setting</value> </data> <data name="TooManyAdminSettingArgumentsError" xml:space="preserve"> - <value>Too many admin setting arguments provided</value> + <value>Too many admin setting arguments provided: '{0}'</value> + <comment>{Locked="{0}"} Error message displayed when the user provides too many admin setting arguments. {0} is a placeholder replaced by the admin setting arguments (e.g. 'enable|disable').</comment> </data> <data name="AdminSettingEnabled" xml:space="preserve"> <value>Admin setting enabled.</value> @@ -1244,10 +1289,12 @@ Do you agree to the terms?</value> <value>Installation abandoned</value> </data> <data name="InstallerFailedWithCode" xml:space="preserve"> - <value>Installer failed with exit code:</value> + <value>Installer failed with exit code: {0}</value> + <comment>{Locked="{0}"} Error message displayed when the application installer fails. {0} is a placeholder replaced by an error code.</comment> </data> <data name="InstallerLogAvailable" xml:space="preserve"> - <value>Installer log is available at:</value> + <value>Installer log is available at: {0}</value> + <comment>{Locked="{0}"} Message displayed to inform the user about the system path of a diagnostic files containing information about the installer. {0} is a placeholder replaced by the diagnostic file system path.</comment> </data> <data name="SearchFailureErrorListMatches" xml:space="preserve"> <value>The following packages were found among the working sources. @@ -1263,8 +1310,8 @@ Please specify one of them using the --source option to proceed.</value> <comment>{Locked="https://github.com/microsoft/winget-cli"}</comment> </data> <data name="WindowsPackageManager" xml:space="preserve"> - <value>Windows Package Manager</value> - <comment>The product name.</comment> + <value>Windows Package Manager v{0}</value> + <comment>{Locked="{0}"} Label displaying the product name and version. {0} is a placeholder replaced by the product version.</comment> </data> <data name="ImportIgnorePackageVersionsArgumentDescription" xml:space="preserve"> <value>Ignore package versions in import file</value> @@ -1279,8 +1326,8 @@ Please specify one of them using the --source option to proceed.</value> <value>The install technology of the newer version specified is different from the current version installed. Please uninstall the package and install the newer version.</value> </data> <data name="WindowsPackageManagerPreview" xml:space="preserve"> - <value>Windows Package Manager (Preview)</value> - <comment>The product name plus an indicator that this is a pre-release version.</comment> + <value>Windows Package Manager (Preview) v{0}</value> + <comment>{Locked="{0}"} Label displaying the preview product name and pre-release version. {0} is a placeholder replaced by the product version.</comment> </data> <data name="InstallArchitectureArgumentDescription" xml:space="preserve"> <value>Select the architecture to install</value> @@ -1312,7 +1359,8 @@ Please specify one of them using the --source option to proceed.</value> <value>The arguments provided can only be used with a query.</value> </data> <data name="SystemArchitecture" xml:space="preserve"> - <value>System Architecture</value> + <value>System Architecture: {0}</value> + <comment>{Locked="{0}"} Label displayed for the system architecture. {0} is a placeholder replaced by the value of the system architecture (e.g. X64).</comment> </data> <data name="PreserveArgumentDescription" xml:space="preserve"> <value>Retains all files and directories created by the package (portable)</value> @@ -1352,7 +1400,8 @@ Please specify one of them using the --source option to proceed.</value> <value>The specified filename is not a valid filename</value> </data> <data name="OverwritingExistingFileAtMessage" xml:space="preserve"> - <value>Overwriting existing file:</value> + <value>Overwriting existing file: {0}</value> + <comment>{Locked="{0}"} Warning message displayed to inform the user that an existing file is being overwritten. {0} is a placeholder replaced by the file system path.</comment> </data> <data name="NoPackageSelectionArgumentProvided" xml:space="preserve"> <value>No package selection argument was provided; see the help for details about finding a package.</value> @@ -1376,7 +1425,8 @@ Please specify one of them using the --source option to proceed.</value> <comment>{Locked="--force"}</comment> </data> <data name="FilesRemainInInstallDirectory" xml:space="preserve"> - <value>Files remain in install directory:</value> + <value>Files remain in install directory: {0}</value> + <comment>{Locked="{0}"} Warning message displayed when files remain in install directory. {0} is a placeholder replaced by the directory path.</comment> </data> <data name="PurgeInstallDirectory" xml:space="preserve"> <value>Purging install directory...</value> @@ -1391,7 +1441,8 @@ Please specify one of them using the --source option to proceed.</value> <value>Documentation:</value> </data> <data name="Notes" xml:space="preserve"> - <value>Notes:</value> + <value>Notes: {0}</value> + <comment>{Locked="{0}"} Label displayed for installation notes. {0} is a placeholder replaced by installation notes.</comment> </data> <data name="ShowLabelInstallationNotes" xml:space="preserve"> <value>Installation Notes:</value> @@ -1403,7 +1454,8 @@ Please specify one of them using the --source option to proceed.</value> <value>Failed to extract the contents of the archive</value> </data> <data name="NestedInstallerNotFound" xml:space="preserve"> - <value>Nested installer file does not exist. Ensure the specified relative path of the nested installer matches: </value> + <value>Nested installer file does not exist. Ensure the specified relative path of the nested installer matches: {0}</value> + <comment>{Locked="{0}"} Error message displayed when nested installer file does not exist. {0} is a placeholder replaced by the nested installer file path.</comment> </data> <data name="InvalidPathToNestedInstaller" xml:space="preserve"> <value>Invalid relative file path to the nested installer; path points to a location outside of the install directory</value> @@ -1421,6 +1473,10 @@ Please specify one of them using the --source option to proceed.</value> <value>The following packages have an upgrade available, but require explicit targeting for upgrade:</value> <comment>"require explicit targeting for upgrade" means that the package will not be upgraded with all others unless an extra flag is added, or the package is mentioned explicitly</comment> </data> + <data name="Downloading" xml:space="preserve"> + <value>Downloading</value> + <comment>Label displayed while downloading an application installer.</comment> + </data> <data name="NestedInstallerNotSupported" xml:space="preserve"> <value>The nested installer type is not supported</value> </data> @@ -1484,12 +1540,12 @@ Please specify one of them using the --source option to proceed.</value> <value>A package version is already installed. Installation cancelled.</value> </data> <data name="EnableAdminSettingFailed" xml:space="preserve"> - <value>Cannot enable %1. This setting is controlled by policy. For more information contact your system administrator.</value> - <comment>{Locked="%1"} The value will be replaced with the feature name</comment> + <value>Cannot enable {0}. This setting is controlled by policy. For more information contact your system administrator.</value> + <comment>{Locked="{0}"} The value will be replaced with the feature name</comment> </data> <data name="DisableAdminSettingFailed" xml:space="preserve"> - <value>Cannot disable %1. This setting is controlled by policy. For more information contact your system administrator.</value> - <comment>{Locked="%1"} The value will be replaced with the feature name</comment> + <value>Cannot disable {0}. This setting is controlled by policy. For more information contact your system administrator.</value> + <comment>{Locked="{0}"} The value will be replaced with the feature name</comment> </data> <data name="PinAddCommandLongDescription" xml:space="preserve"> <value>Add a new pin. A pin can limit the Windows Package Manager from updating a package to specific ranges of versions, or it can prevent it from updating the package altogether. A pinned package may still update on its own and be updated from outside the Windows Package Manager. By default, a pinned package can be updated by mentioning it explicitly in the 'upgrade' command or by adding the '--include-pinned' flag to 'winget upgrade --all'.</value> @@ -1539,7 +1595,8 @@ Please specify one of them using the --source option to proceed.</value> <value>Export settings</value> </data> <data name="UserSettings" xml:space="preserve"> - <value>User Settings</value> + <value>User Settings: {0}</value> + <comment>{Locked="{0}"} Label displayed for the file containing the user settings. {0} is a placeholder replaced by the user settings file path.</comment> </data> <data name="SettingsWarningUsingDefault" xml:space="preserve"> <value>Settings file couldn't load. Using default values.</value> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -220,6 +220,7 @@ <ClCompile Include="PreIndexedPackageSource.cpp" /> <ClCompile Include="Regex.cpp" /> <ClCompile Include="Registry.cpp" /> + <ClCompile Include="Resources.cpp" /> <ClCompile Include="RestClient.cpp" /> <ClCompile Include="RestHelper.cpp" /> <ClCompile Include="RestInterface_1_0.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -227,6 +227,9 @@ <ClCompile Include="FolderFileWatcher.cpp"> <Filter>Source Files\Common</Filter> </ClCompile> + <ClCompile Include="Resources.cpp"> + <Filter>Source Files\Common</Filter> + </ClCompile> <ClCompile Include="PortableInstaller.cpp"> <Filter>Source Files\Common</Filter> </ClCompile> diff --git a/src/AppInstallerCLITests/Command.cpp b/src/AppInstallerCLITests/Command.cpp @@ -202,55 +202,29 @@ struct TestCommand : public Command // Matcher that lets us verify CommandExceptions. struct CommandExceptionMatcher : public Catch::MatcherBase<CommandException> { - CommandExceptionMatcher(const std::string &arg) : m_expectedArg(arg) {} + CommandExceptionMatcher(CLI::Resource::LocString message) : m_expectedMessage(std::move(message)) {} bool match(const CommandException& ce) const override { - const auto& params = ce.Params(); - return params.size() == 1 && params[0].get() == m_expectedArg; + return ce.Message() == m_expectedMessage; } std::string describe() const override { std::ostringstream result; - result << "has param == " << m_expectedArg; + result << "has message == " << m_expectedMessage; return result.str(); } private: - std::string m_expectedArg; + CLI::Resource::LocString m_expectedMessage; }; namespace Catch { template<> struct StringMaker<CommandException> { static std::string convert(CommandException const& ce) { - std::string result{ "CommandException{ '" }; - result += ce.Message().get(); - result += '\''; - - bool first = true; - for (const auto& param : ce.Params()) - { - if (first) - { - first = false; - result += ", ['"; - } - else - { - result += "', '"; - } - result += param.get(); - } - - if (!first) - { - result += "']"; - } - - result += " }"; - return result; + return Utility::Format("CommandException{ '{0}' }", ce.Message().get()); } }; } @@ -313,7 +287,7 @@ TEST_CASE("ParseArguments_TooManyPositional", "[command]") std::vector<std::string> values{ "val1", "--", "-std1" }; Invocation inv{ std::vector<std::string>(values) }; - REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[2]); + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::ExtraPositionalError(Utility::LocIndView{ values[2] })); } TEST_CASE("ParseArguments_InvalidChar", "[command]") @@ -328,7 +302,7 @@ TEST_CASE("ParseArguments_InvalidChar", "[command]") std::vector<std::string> values{ "val1", "-", "-std1" }; Invocation inv{ std::vector<std::string>(values) }; - REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[1]); + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::InvalidArgumentSpecifierError(Utility::LocIndView{ values[1] })); } TEST_CASE("ParseArguments_InvalidAlias", "[command]") @@ -343,7 +317,7 @@ TEST_CASE("ParseArguments_InvalidAlias", "[command]") std::vector<std::string> values{ "val1", "-b", "-std1" }; Invocation inv{ std::vector<std::string>(values) }; - REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[1]); + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::InvalidAliasError(Utility::LocIndView{ values[1] })); } TEST_CASE("ParseArguments_MultiFlag", "[command]") @@ -378,7 +352,7 @@ TEST_CASE("ParseArguments_FlagThenUnknown", "[command]") std::vector<std::string> values{ "val1", "-sr", "val2" }; Invocation inv{ std::vector<std::string>(values) }; - REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[1]); + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::AdjoinedNotFoundError(Utility::LocIndView{ values[1] })); } TEST_CASE("ParseArguments_FlagThenNonFlag", "[command]") @@ -394,7 +368,7 @@ TEST_CASE("ParseArguments_FlagThenNonFlag", "[command]") std::vector<std::string> values{ "val1", "-sp", "val2" }; Invocation inv{ std::vector<std::string>(values) }; - REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[1]); + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::AdjoinedNotFlagError(Utility::LocIndView{ values[1] })); } TEST_CASE("ParseArguments_NameUsingAliasSpecifier", "[command]") @@ -410,7 +384,7 @@ TEST_CASE("ParseArguments_NameUsingAliasSpecifier", "[command]") std::vector<std::string> values{ "another", "-flag1" }; Invocation inv{ std::vector<std::string>(values) }; - REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[1]); + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::AdjoinedNotFoundError(Utility::LocIndView{ values[1] })); } TEST_CASE("ParseArguments_AliasWithAdjoinedValue", "[command]") @@ -459,7 +433,7 @@ TEST_CASE("ParseArguments_AliasWithSeparatedValueMissing", "[command]") std::vector<std::string> values{ "-s" }; Invocation inv{ std::vector<std::string>(values) }; - REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[0]); + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::MissingArgumentError(Utility::LocIndView{ values[0] })); } TEST_CASE("ParseArguments_NameWithAdjoinedValue", "[command]") @@ -528,7 +502,7 @@ TEST_CASE("ParseArguments_NameFlagWithAdjoinedValue", "[command]") std::vector<std::string> values{ "another", "--flag1=arbitrary" }; Invocation inv{ std::vector<std::string>(values) }; - REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[1]); + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::FlagContainAdjoinedError(Utility::LocIndView{ values[1] })); } TEST_CASE("ParseArguments_NameWithSeparatedValue", "[command]") @@ -562,7 +536,7 @@ TEST_CASE("ParseArguments_NameWithSeparatedValueMissing", "[command]") std::vector<std::string> values{ "--pos2" }; Invocation inv{ std::vector<std::string>(values) }; - REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[0]); + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::MissingArgumentError(Utility::LocIndView{ values[0] })); } TEST_CASE("ParseArguments_UnknownName", "[command]") @@ -578,5 +552,5 @@ TEST_CASE("ParseArguments_UnknownName", "[command]") std::vector<std::string> values{ "another", "--nope" }; Invocation inv{ std::vector<std::string>(values) }; - REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[1]); + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::InvalidNameError(Utility::LocIndView{ values[1] })); } diff --git a/src/AppInstallerCLITests/Dependencies.cpp b/src/AppInstallerCLITests/Dependencies.cpp @@ -23,6 +23,7 @@ using namespace AppInstaller::Manifest; using namespace AppInstaller::Repository; using namespace AppInstaller::Settings; using namespace AppInstaller::Utility; +using namespace AppInstaller::Utility::literals; TEST_CASE("DependencyGraph_BFirst", "[dependencyGraph][dependencies]") { @@ -161,7 +162,7 @@ TEST_CASE("DependencyNodeProcessor_NoInstallers", "[dependencies]") Dependency rootAsDependency(DependencyType::Package, manifest.Id); DependencyNodeProcessorResult result = nodeProcessor.EvaluateDependencies(rootAsDependency); - REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::DependenciesFlowNoInstallerFound)) != std::string::npos); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::DependenciesFlowNoInstallerFound("withoutInstallers"_liv))) != std::string::npos); REQUIRE(result == DependencyNodeProcessorResult::Error); } diff --git a/src/AppInstallerCLITests/Resources.cpp b/src/AppInstallerCLITests/Resources.cpp @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <AppInstallerStrings.h> +#include <ChannelStreams.h> +#include <ExecutionReporter.h> + +using namespace std::string_view_literals; +using namespace AppInstaller::Utility; +using namespace AppInstaller::Utility::literals; +using namespace AppInstaller::CLI; + +#define WINGET_TEST_OUTPUT_STREAM(_expected_, _input_) \ + do { \ + std::istringstream iInput; \ + std::ostringstream oInput; \ + std::istringstream iExpected; \ + std::ostringstream oExpected; \ + Execution::Reporter(oInput, iInput).Info() << _input_; \ + Execution::Reporter(oExpected, iExpected).Info() << _expected_; \ + REQUIRE(oExpected.str()== oInput.str()); \ + } while(0); + +TEST_CASE("Resources_StringId", "[resources]") +{ + WINGET_TEST_OUTPUT_STREAM( + "Filter results by command"_liv, + Resource::String::CommandArgumentDescription + ); +} + +TEST_CASE("Resources_StringIdWithPlaceholders_LocIndString", "[resources]") +{ + WINGET_TEST_OUTPUT_STREAM( + "The value provided for the `First` argument is invalid; valid values are: Second"_liv , + Resource::String::InvalidArgumentValueError("First"_liv, "Second"_liv) + ); +} + +TEST_CASE("Resources_StringIdWithPlaceholders_StringId", "[resources]") +{ + WINGET_TEST_OUTPUT_STREAM( + "This operation is disabled by Group Policy: Enable Additional Windows App Installer Sources"_liv , + Resource::String::DisabledByGroupPolicy(AppInstaller::StringResource::String::PolicyAdditionalSources) + ); +} + +TEST_CASE("Resources_StringIdWithPlaceholders_Arithmetic", "[resources]") +{ + WINGET_TEST_OUTPUT_STREAM( + "42 upgrades available."_liv , + Resource::String::AvailableUpgrades(42) + ); +} diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp @@ -8,7 +8,7 @@ using namespace std::string_view_literals; using namespace AppInstaller::Utility; - +using namespace AppInstaller::Utility::literals; TEST_CASE("UTF8Length", "[strings]") { @@ -225,3 +225,26 @@ TEST_CASE("HexStrings", "[strings]") REQUIRE(value == ConvertToHexString(buffer)); REQUIRE(std::equal(buffer.begin(), buffer.end(), ParseFromHexString(value).begin())); } + +TEST_CASE("Join", "[strings]") +{ + std::vector<LocIndString> list_0{ }; + std::vector<LocIndString> list_1{ "A"_lis }; + std::vector<LocIndString> list_2{ "A"_lis, "B"_lis }; + + REQUIRE(""_lis == Join(", "_liv, list_0)); + REQUIRE("A"_lis == Join(", "_liv, list_1)); + REQUIRE("A, B"_lis == Join(", "_liv, list_2)); + REQUIRE("AB"_lis == Join(""_liv, list_2)); +} + +TEST_CASE("Format", "[strings]") +{ + REQUIRE("First Second" == Format("{0} {1}", "First", "Second")); + REQUIRE("First Second" == Format("{1} {0}", "Second", "First")); + REQUIRE("First Second" == Format("{0} {1}", "First", "Second", "(Extra", "Input", "Ignored)")); + REQUIRE("First Second First Second" == Format("{0} {1} {0} {1}", "First", "Second")); + + // Note: C++20 std::format will throw an exception for this test case + REQUIRE("First {1}" == Format("{0} {1}", "First")); +} diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -57,6 +57,7 @@ using namespace AppInstaller::Manifest; using namespace AppInstaller::Repository; using namespace AppInstaller::Settings; using namespace AppInstaller::Utility; +using namespace AppInstaller::Utility::literals; using namespace AppInstaller::Settings; using namespace AppInstaller::CLI::Portable; @@ -1191,7 +1192,9 @@ TEST_CASE("InstallFlow_Zip_BadRelativePath", "[InstallFlow][workflow]") // Verify Installer was not called REQUIRE(!std::filesystem::exists(installResultPath.GetPath())); - REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::NestedInstallerNotFound).get()) != std::string::npos); + auto relativePath = context.Get<Execution::Data::InstallerPath>().parent_path() / "extracted" / "relativeFilePath"; + auto expectedMessage = Resource::String::NestedInstallerNotFound(AppInstaller::Utility::LocIndString{ relativePath.u8string()}); + REQUIRE(installOutput.str().find(Resource::LocString(expectedMessage).get()) != std::string::npos); } TEST_CASE("InstallFlow_Zip_MissingNestedInstaller", "[InstallFlow][workflow]") @@ -2477,7 +2480,7 @@ TEST_CASE("UpdateFlow_UpdateExeSpecificVersionNotFound", "[UpdateFlow][workflow] // Verify Installer is not called. REQUIRE(!std::filesystem::exists(updateResultPath.GetPath())); - REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::GetManifestResultVersionNotFound).get()) != std::string::npos); + REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::GetManifestResultVersionNotFound("1.2.3.4"_liv)).get()) != std::string::npos); REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND); } @@ -3044,7 +3047,7 @@ TEST_CASE("ImportFlow_PackageAlreadyInstalled", "[ImportFlow][workflow]") // Exe should not have been installed again REQUIRE(!std::filesystem::exists(exeInstallResultPath.GetPath())); - REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportPackageAlreadyInstalled).get()) != std::string::npos); + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportPackageAlreadyInstalled("AppInstallerCliTest.TestExeInstaller"_liv)).get()) != std::string::npos); } TEST_CASE("ImportFlow_IgnoreVersions", "[ImportFlow][workflow]") @@ -3082,7 +3085,7 @@ TEST_CASE("ImportFlow_MissingSource", "[ImportFlow][workflow]") // Installer should not be called REQUIRE(!std::filesystem::exists(exeInstallResultPath.GetPath())); - REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSourceNotInstalled).get()) != std::string::npos); + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSourceNotInstalled("TestSource"_liv)).get()) != std::string::npos); REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); } @@ -3102,7 +3105,7 @@ TEST_CASE("ImportFlow_MissingPackage", "[ImportFlow][workflow]") // Installer should not be called REQUIRE(!std::filesystem::exists(exeInstallResultPath.GetPath())); - REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSearchFailed).get()) != std::string::npos); + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSearchFailed("MissingPackage"_liv)).get()) != std::string::npos); REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND); } @@ -3124,7 +3127,7 @@ TEST_CASE("ImportFlow_IgnoreMissingPackage", "[ImportFlow][workflow]") // Verify installer was called for the package that was available. REQUIRE(std::filesystem::exists(exeInstallResultPath.GetPath())); - REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSearchFailed).get()) != std::string::npos); + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSearchFailed("MissingPackage"_liv)).get()) != std::string::npos); } TEST_CASE("ImportFlow_MissingVersion", "[ImportFlow][workflow]") @@ -3143,7 +3146,7 @@ TEST_CASE("ImportFlow_MissingVersion", "[ImportFlow][workflow]") // Installer should not be called REQUIRE(!std::filesystem::exists(exeInstallResultPath.GetPath())); - REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSearchFailed).get()) != std::string::npos); + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSearchFailed("AppInstallerCliTest.TestExeInstaller"_liv)).get()) != std::string::npos); REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND); } diff --git a/src/AppInstallerCommonCore/AdminSettings.cpp b/src/AppInstallerCommonCore/AdminSettings.cpp @@ -11,14 +11,14 @@ namespace AppInstaller::Settings { using namespace std::string_view_literals; - using namespace Utility; + using namespace Utility::literals; namespace { - constexpr std::string_view s_AdminSettingsYaml_LocalManifestFiles = "LocalManifestFiles"sv; - constexpr std::string_view s_AdminSettingsYaml_BypassCertificatePinningForMicrosoftStore = "BypassCertificatePinningForMicrosoftStore"sv; - constexpr std::string_view s_AdminSettingsYaml_InstallerHashOverride = "InstallerHashOverride"sv; - constexpr std::string_view s_AdminSettingsYaml_LocalArchiveMalwareScanOverride = "LocalArchiveMalwareScanOverride"sv; + constexpr Utility::LocIndView s_AdminSettingsYaml_LocalManifestFiles = "LocalManifestFiles"_liv; + constexpr Utility::LocIndView s_AdminSettingsYaml_BypassCertificatePinningForMicrosoftStore = "BypassCertificatePinningForMicrosoftStore"_liv; + constexpr Utility::LocIndView s_AdminSettingsYaml_InstallerHashOverride = "InstallerHashOverride"_liv; + constexpr Utility::LocIndView s_AdminSettingsYaml_LocalArchiveMalwareScanOverride = "LocalArchiveMalwareScanOverride"_liv; // Attempts to read a single scalar value from the node. template<typename Value> @@ -194,7 +194,7 @@ namespace AppInstaller::Settings return result; } - std::string_view AdminSettingToString(AdminSetting setting) + Utility::LocIndView AdminSettingToString(AdminSetting setting) { switch (setting) { @@ -207,7 +207,7 @@ namespace AppInstaller::Settings case AdminSetting::LocalArchiveMalwareScanOverride: return s_AdminSettingsYaml_LocalArchiveMalwareScanOverride; default: - return "Unknown"sv; + return "Unknown"_liv; } } diff --git a/src/AppInstallerCommonCore/AppInstallerStrings.cpp b/src/AppInstallerCommonCore/AppInstallerStrings.cpp @@ -14,7 +14,6 @@ namespace AppInstaller::Utility using namespace std::string_view_literals; constexpr std::string_view s_SpaceChars = AICLI_SPACE_CHARS; constexpr std::wstring_view s_WideSpaceChars = L"" AICLI_SPACE_CHARS; - constexpr std::string_view s_MessageReplacementToken = "%1"sv; namespace { @@ -567,13 +566,6 @@ namespace AppInstaller::Utility return result; } - std::string FindAndReplaceMessageToken(std::string_view message, std::string_view value) - { - std::string result{ message }; - FindAndReplace(result, s_MessageReplacementToken, value); - return result; - } - // Follow the rules at https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file to replace // invalid characters in a candidate path part. // Additionally, based on https://docs.microsoft.com/en-us/windows/win32/fileio/filesystem-functionality-comparison#limits @@ -713,4 +705,21 @@ namespace AppInstaller::Utility return result; } + + LocIndString Join(LocIndView separator, const std::vector<LocIndString>& vector) + { + auto vectorSize = vector.size(); + if (vectorSize == 0) + { + return {}; + } + + std::ostringstream ssJoin; + ssJoin << vector[0]; + for (size_t i = 1; i < vectorSize; ++i) + { + ssJoin << separator << vector[i]; + } + return LocIndString{ ssJoin.str() }; + } } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerStrings.h b/src/AppInstallerCommonCore/Public/AppInstallerStrings.h @@ -6,6 +6,7 @@ #include <string> #include <string_view> #include <vector> +#include <winget/LocIndependent.h> namespace AppInstaller::Utility { @@ -168,9 +169,6 @@ namespace AppInstaller::Utility // Expands environment variables within the input. std::wstring ExpandEnvironmentVariables(const std::wstring& input); - // Replace message predefined token - std::string FindAndReplaceMessageToken(std::string_view message, std::string_view value); - // Converts the candidate path part into one suitable for the actual file system std::string MakeSuitablePathPart(std::string_view candidate); @@ -230,4 +228,17 @@ namespace AppInstaller::Utility // Converts the given hexadecimal string into bytes. std::vector<uint8_t> ParseFromHexString(const std::string& value, size_t byteCount = 0); + + // Join a string vector using the provided separator. + LocIndString Join(LocIndView separator, const std::vector<LocIndString>& vector); + + // Format an input string by replacing placeholders {index} with provided values at corresponding indices. + // Note: After upgrading to C++20, this function should be deprecated in favor of std::format. + template <typename ... T> + std::string Format(std::string inputStr, T ... args) + { + int index = 0; + (FindAndReplace(inputStr, "{" + std::to_string(index++) + "}", (std::ostringstream() << args).str()),...); + return inputStr; + } } diff --git a/src/AppInstallerCommonCore/Public/winget/AdminSettings.h b/src/AppInstallerCommonCore/Public/winget/AdminSettings.h @@ -19,7 +19,7 @@ namespace AppInstaller::Settings AdminSetting StringToAdminSetting(std::string_view in); - std::string_view AdminSettingToString(AdminSetting setting); + Utility::LocIndView AdminSettingToString(AdminSetting setting); bool EnableAdminSetting(AdminSetting setting); diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -4,6 +4,7 @@ #include <vector> #include <string> #include <type_traits> +#include "AppInstallerStrings.h" namespace AppInstaller::Settings { @@ -57,13 +58,13 @@ namespace AppInstaller::Settings static std::vector<ExperimentalFeature> GetAllFeatures(); std::string_view Name() const { return m_name; } - std::string_view JsonName() const { return m_jsonName; } + Utility::LocIndView JsonName() const { return m_jsonName; } std::string_view Link() const { return m_link; } Feature GetFeature() const { return m_feature; } private: std::string_view m_name; - std::string_view m_jsonName; + Utility::LocIndView m_jsonName; std::string_view m_link; Feature m_feature; }; diff --git a/src/AppInstallerCommonCore/Public/winget/Resources.h b/src/AppInstallerCommonCore/Public/winget/Resources.h @@ -5,6 +5,7 @@ #include <string> #include <vector> +#include "AppInstallerStrings.h" using namespace std::string_view_literals; @@ -20,8 +21,21 @@ namespace AppInstaller struct StringId : public std::wstring_view { explicit constexpr StringId(std::wstring_view id) : std::wstring_view(id) {} + + // Sets the placeholder values in the resolved string id. + // Example: out << myStringId(placeholderVal1, placeholderVal2, ...) + template<typename ...T> + Utility::LocIndString operator()(T ... args) const; + + private: + // Resolve the string ID to its corresponding localized string + // without replacing placeholders. + std::string Resolve() const; }; + // Output resource identifier as localized string. + std::ostream& operator<<(std::ostream& out, StringId si); + // Resource string identifiers. struct String { @@ -58,5 +72,58 @@ namespace AppInstaller // Resource data is valid as long as the binary is loaded. std::pair<const BYTE*, size_t> GetResourceAsBytes(int resourceName, int resourceType); std::pair<const BYTE*, size_t> GetResourceAsBytes(PCWSTR resourceName, PCWSTR resourceType); + + // A localized string + struct LocString : public Utility::LocIndString + { + LocString() = default; + + LocString(StringResource::StringId id) : Utility::LocIndString(id()) {} + LocString(Utility::LocIndString locIndString) : Utility::LocIndString(std::move(locIndString)) {} + + LocString(const LocString&) = default; + LocString& operator=(const LocString&) = default; + + LocString(LocString&&) = default; + LocString& operator=(LocString&&) = default; + }; + } + + namespace details + { + // List of approved types for output, others are potentially not localized. + template <typename T> + struct IsApprovedForOutput + { + static constexpr bool value = std::is_arithmetic<T>::value; + }; + +#define WINGET_CREATE_ISAPPROVEDFOROUTPUT_SPECIALIZATION(_t_) \ + template <> \ + struct IsApprovedForOutput<_t_> \ + { \ + static constexpr bool value = true; \ + } + + // It is assumed that single char values need not be localized, as they are matched + // ordinally or they are punctuation / other. + WINGET_CREATE_ISAPPROVEDFOROUTPUT_SPECIALIZATION(char); + // Localized strings (and from an Id for one for convenience). + WINGET_CREATE_ISAPPROVEDFOROUTPUT_SPECIALIZATION(StringResource::StringId); + WINGET_CREATE_ISAPPROVEDFOROUTPUT_SPECIALIZATION(Resource::LocString); + // Strings explicitly declared as localization independent. + WINGET_CREATE_ISAPPROVEDFOROUTPUT_SPECIALIZATION(Utility::LocIndView); + WINGET_CREATE_ISAPPROVEDFOROUTPUT_SPECIALIZATION(Utility::LocIndString); + // Normalized strings come from user data and should therefore already by localized + // by how they are chosen (or there is no localized version). + WINGET_CREATE_ISAPPROVEDFOROUTPUT_SPECIALIZATION(Utility::NormalizedString); + } + + template<typename ... T> + Utility::LocIndString StringResource::StringId::operator()(T ... args) const + { + static_assert((details::IsApprovedForOutput<std::decay_t<T>>::value && ...), "This type may not be localized, see comment for more information"); + return Utility::LocIndString{ Utility::Format(Resolve(), std::forward<T>(args)...) }; } } + diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -199,8 +199,8 @@ namespace AppInstaller::Settings Message(message), Path(settingPath), Data(settingValue), IsFieldWarning(isField) {} StringResource::StringId Message; - std::string Path; - std::string Data; + Utility::LocIndString Path; + Utility::LocIndString Data; bool IsFieldWarning = true; }; diff --git a/src/AppInstallerCommonCore/Resources.cpp b/src/AppInstallerCommonCore/Resources.cpp @@ -4,55 +4,120 @@ #include "winget/Resources.h" #include "Public/AppInstallerLogging.h" #include "Public/AppInstallerStrings.h" +#include "Public/AppInstallerErrors.h" +#include "Public/AppInstallerTelemetry.h" -namespace AppInstaller::Resource +namespace AppInstaller { - namespace + namespace Resource { - std::pair<void*, size_t> GetResourceData(PCWSTR resourceName, PCWSTR resourceType) + namespace { - HMODULE resourceModule = nullptr; - GetModuleHandleExW( - GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, - reinterpret_cast<PCWSTR>(GetResourceData), - &resourceModule); - THROW_LAST_ERROR_IF_NULL(resourceModule); + std::pair<void*, size_t> GetResourceData(PCWSTR resourceName, PCWSTR resourceType) + { + HMODULE resourceModule = nullptr; + GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast<PCWSTR>(GetResourceData), + &resourceModule); + THROW_LAST_ERROR_IF_NULL(resourceModule); - HRSRC resourceInfoHandle = FindResourceW(resourceModule, resourceName, resourceType); - THROW_LAST_ERROR_IF_NULL(resourceInfoHandle); + HRSRC resourceInfoHandle = FindResourceW(resourceModule, resourceName, resourceType); + THROW_LAST_ERROR_IF_NULL(resourceInfoHandle); - HGLOBAL resourceMemoryHandle = LoadResource(resourceModule, resourceInfoHandle); - THROW_LAST_ERROR_IF_NULL(resourceMemoryHandle); + HGLOBAL resourceMemoryHandle = LoadResource(resourceModule, resourceInfoHandle); + THROW_LAST_ERROR_IF_NULL(resourceMemoryHandle); - DWORD resourceSize = SizeofResource(resourceModule, resourceInfoHandle); - THROW_LAST_ERROR_IF(resourceSize == 0); + DWORD resourceSize = SizeofResource(resourceModule, resourceInfoHandle); + THROW_LAST_ERROR_IF(resourceSize == 0); - void* resourceContent = LockResource(resourceMemoryHandle); - THROW_HR_IF_NULL(E_UNEXPECTED, resourceContent); + void* resourceContent = LockResource(resourceMemoryHandle); + THROW_HR_IF_NULL(E_UNEXPECTED, resourceContent); - return std::make_pair(resourceContent, static_cast<size_t>(resourceSize)); + return std::make_pair(resourceContent, static_cast<size_t>(resourceSize)); + } } - } - std::string_view GetResourceAsString(int resourceName, int resourceType) - { - return GetResourceAsString(MAKEINTRESOURCE(resourceName), MAKEINTRESOURCE(resourceType)); - } + std::string_view GetResourceAsString(int resourceName, int resourceType) + { + return GetResourceAsString(MAKEINTRESOURCE(resourceName), MAKEINTRESOURCE(resourceType)); + } - std::string_view GetResourceAsString(PCWSTR resourceName, PCWSTR resourceType) - { - auto resourceData = GetResourceData(resourceName, resourceType); - return { reinterpret_cast<char*>(resourceData.first), resourceData.second }; - } + std::string_view GetResourceAsString(PCWSTR resourceName, PCWSTR resourceType) + { + auto resourceData = GetResourceData(resourceName, resourceType); + return { reinterpret_cast<char*>(resourceData.first), resourceData.second }; + } - std::pair<const BYTE*, size_t> GetResourceAsBytes(int resourceName, int resourceType) - { - return GetResourceAsBytes(MAKEINTRESOURCE(resourceName), MAKEINTRESOURCE(resourceType)); + std::pair<const BYTE*, size_t> GetResourceAsBytes(int resourceName, int resourceType) + { + return GetResourceAsBytes(MAKEINTRESOURCE(resourceName), MAKEINTRESOURCE(resourceType)); + } + + std::pair<const BYTE*, size_t> GetResourceAsBytes(PCWSTR resourceName, PCWSTR resourceType) + { + auto resourceData = GetResourceData(resourceName, resourceType); + return std::make_pair(reinterpret_cast<BYTE*>(resourceData.first), resourceData.second); + } + + // Utility class to load resources + struct Loader + { + // Gets the singleton instance of the resource loader. + static const Loader& Instance() + { + static Loader instance; + return instance; + } + + // Gets the string resource value. + std::string ResolveString(std::wstring_view resKey) const + { + if (m_wingetLoader) + { + return Utility::ConvertToUTF8(m_wingetLoader.GetString(resKey)); + } + + // Loader failed to load resource file, print the resource key instead. + return Utility::ConvertToUTF8(resKey); + } + + private: + winrt::Windows::ApplicationModel::Resources::ResourceLoader m_wingetLoader; + + Loader() : m_wingetLoader(nullptr) + { + try + { + // The default constructor of ResourceLoader throws a winrt::hresult_error exception + // when resource.pri is not found. ResourceLoader::GetForViewIndependentUse also throws + // a winrt::hresult_error but for reasons unknown it only gets caught when running on the + // debugger. Running without a debugger will result in a crash that not even adding a + // catch all will fix. To provide a good error message we call the default constructor + // before calling GetForViewIndependentUse. + m_wingetLoader = winrt::Windows::ApplicationModel::Resources::ResourceLoader(); + m_wingetLoader = winrt::Windows::ApplicationModel::Resources::ResourceLoader::GetForViewIndependentUse(L"winget"); + } + catch (const winrt::hresult_error& hre) + { + // This message cannot be localized. + AICLI_LOG(CLI, Error, << "Failure loading resource file with error: " << hre.code()); + m_wingetLoader = nullptr; + } + } + }; } - std::pair<const BYTE*, size_t> GetResourceAsBytes(PCWSTR resourceName, PCWSTR resourceType) + namespace StringResource { - auto resourceData = GetResourceData(resourceName, resourceType); - return std::make_pair(reinterpret_cast<BYTE*>(resourceData.first), resourceData.second); + std::string StringId::Resolve() const + { + return Resource::Loader::Instance().ResolveString(*this); + } + + std::ostream& operator<<(std::ostream& out, StringId si) + { + return (out << Resource::LocString{ si }); + } } }