From 15260c2c957423fd4595be494f2514a58a00d677 Mon Sep 17 00:00:00 2001 From: Paul Licameli Date: Tue, 3 Dec 2019 11:34:50 -0500 Subject: [PATCH] TranslatableString can store a context and format arguments... ... Format arguments are substituted into the translation of the msgid, which may not be known at the time the format arguments are captured (because locale may change). This allows TranslatableString with arguments to be constructed at static initialization time. There is also a special "verbatim" or null context which makes no translations of msgids. There is not yet any use of other contexts besides default or null. --- include/audacity/ComponentInterface.h | 8 ++-- include/audacity/Types.h | 54 ++++++++++++++++++++++++--- src/Internat.cpp | 32 +++++++++++++++- src/Internat.h | 2 +- src/PluginManager.cpp | 2 +- src/effects/Equalization.cpp | 8 ++-- src/effects/nyquist/Nyquist.cpp | 22 ++++++----- src/prefs/QualityPrefs.cpp | 8 +--- src/widgets/NumericTextCtrl.cpp | 2 +- 9 files changed, 103 insertions(+), 35 deletions(-) diff --git a/include/audacity/ComponentInterface.h b/include/audacity/ComponentInterface.h index f96c07db0..3625d9bd0 100644 --- a/include/audacity/ComponentInterface.h +++ b/include/audacity/ComponentInterface.h @@ -68,12 +68,12 @@ public: // Allows implicit construction from an internal string re-used as a msgid ComponentInterfaceSymbol( const wxString &internal ) - : mInternal{ internal }, mMsgid{ internal } + : mInternal{ internal }, mMsgid{ internal, {} } {} // Allows implicit construction from an internal string re-used as a msgid ComponentInterfaceSymbol( const wxChar *msgid ) - : mInternal{ msgid }, mMsgid{ msgid } + : mInternal{ msgid }, mMsgid{ msgid, {} } {} // Two-argument version distinguishes internal from translatable string @@ -87,7 +87,7 @@ public: const wxString &Internal() const { return mInternal; } const TranslatableString &Msgid() const { return mMsgid; } - const wxString &Translation() const { return mMsgid.Translation(); } + const wxString Translation() const { return mMsgid.Translation(); } bool empty() const { return mInternal.empty(); } @@ -135,7 +135,7 @@ public: virtual wxString GetDescription() = 0; // non-virtual convenience function - const wxString& GetTranslatedName(); + const wxString GetTranslatedName(); // Parameters, if defined. false means no defined parameters. virtual bool DefineParams( ShuttleParams & WXUNUSED(S) ){ return false;}; diff --git a/include/audacity/Types.h b/include/audacity/Types.h index f11816d84..fd121fba7 100644 --- a/include/audacity/Types.h +++ b/include/audacity/Types.h @@ -43,6 +43,7 @@ #define __AUDACITY_TYPES_H__ #include +#include #include #include #include // for wxASSERT @@ -292,8 +293,8 @@ using CommandID = TaggedIdentifier< CommandIdTag, false >; using CommandIDs = std::vector; -// Holds a msgid for the translation catalog (and in future, might also hold a -// second, disambiguating context string) +// Holds a msgid for the translation catalog and may also hold a context string +// and a formatter closure that captures formatting arguments // // Two different wxString accessors -- one for the msgid itself, another for // the user-visible translation. The msgid should be used only in unusual cases @@ -302,10 +303,28 @@ using CommandIDs = std::vector; // Implicit conversions to and from wxString are intentionally disabled class TranslatableString : private wxString { public: + // A dual-purpose function + // Given the empty string, return a context string + // Given the translation of the msgid into the current locale, substitute + // format arguments into it + using Formatter = std::function< wxString(const wxString &) >; + + // This special formatter causes msgids to be used verbatim, not looked up + // in any catalog + static const Formatter NullContextFormatter; + TranslatableString() {} - explicit TranslatableString(const wxString &str) : wxString{ str } {} - explicit TranslatableString(const wxChar *str) : wxString{ str } {} + // Supply {} for the second argument to cause lookup of the msgid with + // empty context string + explicit TranslatableString( + const wxString &str, Formatter formatter = NullContextFormatter) + : wxString{ str }, mFormatter{ std::move(formatter) } {} + // Supply {} for the second argument to cause lookup of the msgid with + // empty context string + explicit TranslatableString( + const wxChar *str, Formatter formatter = NullContextFormatter) + : wxString{ str }, mFormatter{ std::move(formatter) } {} using wxString::empty; @@ -316,7 +335,7 @@ public: // This is a deliberately ugly-looking function name. Use with caution. Identifier MSGID() const { return Identifier{ *this }; } - const wxString &Translation() const; + wxString Translation() const; friend bool operator == ( const TranslatableString &x, const TranslatableString &y) @@ -326,8 +345,31 @@ public: const TranslatableString &x, const TranslatableString &y) { return !(x == y); } - // Future: may also store a domain and context, as if for dpgettext() + // Returns true if context is NullContextFormatter + bool IsVerbatim() const; + + // Capture variadic format arguments (by copy). The subsitution is + // computed later in a call to Translate() after msgid is looked up in the + // translation catalog. + template + TranslatableString&& Format( Args&&... args ) && + { + wxString context; + if ( this->mFormatter ) + context = this->mFormatter({}); + this->mFormatter = [context, args...](const wxString &str){ + if (str.empty()) + return context; + else + return wxString::Format( str, args... ); + }; + return std::move( *this ); + } + friend std::hash< TranslatableString >; +private: + + Formatter mFormatter; }; using TranslatableStrings = std::vector; diff --git a/src/Internat.cpp b/src/Internat.cpp index e1cbd72fc..b3f18b296 100644 --- a/src/Internat.cpp +++ b/src/Internat.cpp @@ -291,7 +291,35 @@ std::vector< Identifier > Identifier::split( wxChar separator ) const return { strings.begin(), strings.end() }; } -const wxString &TranslatableString::Translation() const +static const wxChar *const NullContextName = wxT("*"); + +const TranslatableString::Formatter +TranslatableString::NullContextFormatter { + [](const wxString & str) -> wxString { + if (str.empty()) + return NullContextName; + else + return str; + } +}; + +bool TranslatableString::IsVerbatim() const { - return wxGetTranslation(*this); + return mFormatter && mFormatter({}) == NullContextName; +} + +wxString TranslatableString::Translation() const +{ + wxString context; + if ( mFormatter ) + context = mFormatter({}); + + wxString result = (context == NullContextName) + ? *this + : wxGetTranslation( *this, {}, context ); + + if ( mFormatter ) + result = mFormatter( result ); + + return result; } diff --git a/src/Internat.h b/src/Internat.h index b57abe600..2a6f95028 100644 --- a/src/Internat.h +++ b/src/Internat.h @@ -29,7 +29,7 @@ extern AUDACITY_DLL_API const wxString& GetCustomSubstitution(const wxString& st #define _TS( s ) GetCustomSubstitution( s ) // Marks strings for extraction only... use .Translate() to translate. -#define XO(s) (TranslatableString{ wxT(s) }) +#define XO(s) (TranslatableString{ wxT(s), {} }) #ifdef _ #undef _ diff --git a/src/PluginManager.cpp b/src/PluginManager.cpp index 09e5c62ca..444ca7abe 100644 --- a/src/PluginManager.cpp +++ b/src/PluginManager.cpp @@ -3229,7 +3229,7 @@ int PluginManager::b64decode(const wxString &in, void *out) // These are defined out-of-line here, to keep ComponentInterface free of other // #include directives. -const wxString& ComponentInterface::GetTranslatedName() +const wxString ComponentInterface::GetTranslatedName() { return GetSymbol().Translation(); } diff --git a/src/effects/Equalization.cpp b/src/effects/Equalization.cpp index 96b2d76e3..f84ebb693 100644 --- a/src/effects/Equalization.cpp +++ b/src/effects/Equalization.cpp @@ -881,11 +881,11 @@ void EffectEqualization::PopulateOrExchange(ShuttleGui & S) mEQVals[i] = 0.; //S.SetSizerProportion(1); S.Prop(1) - .Name( TranslatableString{ + .Name( kThirdOct[i] < 1000. - ? wxString::Format(_("%d Hz"), (int)kThirdOct[i]) - : wxString::Format(_("%g kHz"), kThirdOct[i]/1000.) - } ) + ? XO("%d Hz").Format( (int)kThirdOct[i] ) + : XO("%g kHz").Format( kThirdOct[i]/1000. ) + ) .ConnectRoot( wxEVT_ERASE_BACKGROUND, &EffectEqualization::OnErase) .Position(wxEXPAND) diff --git a/src/effects/nyquist/Nyquist.cpp b/src/effects/nyquist/Nyquist.cpp index 8003746db..e84f4024e 100644 --- a/src/effects/nyquist/Nyquist.cpp +++ b/src/effects/nyquist/Nyquist.cpp @@ -186,6 +186,8 @@ NyquistEffect::NyquistEffect(const wxString &fName) } mFileName = fName; + // Use the file name verbatim as effect name. + // This is only a default name, overridden if we find a $name line: mName = TranslatableString{ mFileName.GetName() }; mFileModified = mFileName.GetModificationTime(); ParseFile(); @@ -223,7 +225,7 @@ VendorSymbol NyquistEffect::GetVendor() return XO("Audacity"); } - return TranslatableString{ mAuthor }; + return mAuthor; } wxString NyquistEffect::GetVersion() @@ -1576,9 +1578,10 @@ std::vector NyquistEffect::ParseChoice(const wxString & text) for (auto &choice : choices) { auto label = UnQuote(choice, true, &extra); if (extra.empty()) - results.push_back( { label } ); + results.push_back( TranslatableString{ label, {} } ); else - results.push_back( { extra, TranslatableString{ label } } ); + results.push_back( + { extra, TranslatableString{ label, {} } } ); } } else { @@ -1891,17 +1894,17 @@ bool NyquistEffect::Parse( // later looked up will lack the ... and will not be found. if (name.EndsWith(wxT("..."))) name = name.RemoveLast(3); - mName = TranslatableString{ name }; + mName = TranslatableString{ name, {} }; return true; } if (len >= 2 && tokens[0] == wxT("action")) { - mAction = TranslatableString{ UnQuote(tokens[1]) }; + mAction = TranslatableString{ UnQuote(tokens[1]), {} }; return true; } if (len >= 2 && tokens[0] == wxT("info")) { - mInfo = TranslatableString{ UnQuote(tokens[1]) }; + mInfo = TranslatableString{ UnQuote(tokens[1]), {} }; return true; } @@ -1951,18 +1954,19 @@ bool NyquistEffect::Parse( #endif if (len >= 2 && tokens[0] == wxT("author")) { - mAuthor = TranslatableString{ UnQuote(tokens[1]) }; + mAuthor = TranslatableString{ UnQuote(tokens[1]), {} }; return true; } if (len >= 2 && tokens[0] == wxT("release")) { // Value must be quoted if the release version string contains spaces. - mReleaseVersion = TranslatableString{ UnQuote(tokens[1]) }; + mReleaseVersion = + TranslatableString{ UnQuote(tokens[1]), {} }; return true; } if (len >= 2 && tokens[0] == wxT("copyright")) { - mCopyright = TranslatableString{ UnQuote(tokens[1]) }; + mCopyright = TranslatableString{ UnQuote(tokens[1]), {} }; return true; } diff --git a/src/prefs/QualityPrefs.cpp b/src/prefs/QualityPrefs.cpp index fa9e9a8a0..df0ad28c2 100644 --- a/src/prefs/QualityPrefs.cpp +++ b/src/prefs/QualityPrefs.cpp @@ -124,13 +124,7 @@ void QualityPrefs::GetNamesAndLabels() for (int i = 0; i < AudioIOBase::NumStandardRates; i++) { int iRate = AudioIOBase::StandardRates[i]; mSampleRateLabels.push_back(iRate); - mSampleRateNames.push_back( - // Composing strings for the choice control - // Note: the format string is localized, then substituted, - // and then the result is treated as if it were a msgid - // but really isn't in the translation catalog - /* i18n-hint Hertz, a unit of frequency */ - TranslatableString{ wxString::Format(_("%i Hz"), iRate) } ); + mSampleRateNames.push_back( XO("%i Hz").Format( iRate ) ); } mSampleRateNames.push_back(XO("Other...")); diff --git a/src/widgets/NumericTextCtrl.cpp b/src/widgets/NumericTextCtrl.cpp index e98e010c7..bc0529d64 100644 --- a/src/widgets/NumericTextCtrl.cpp +++ b/src/widgets/NumericTextCtrl.cpp @@ -709,7 +709,7 @@ NumericConverter::NumericConverter(Type type, void NumericConverter::ParseFormatString( const TranslatableString & untranslatedFormat) { - auto &format = untranslatedFormat.Translation(); + auto format = untranslatedFormat.Translation(); mPrefix = wxT(""); mFields.clear();