Skip to content

Route every lookup through a replaceable message formatter #30

Closed

Description

@rent-a-developer

Feature 01 of the 11 features of ResXLocalization 2.0.0, parent issue #29. "Feature NN" in this
issue means an entry of the numbered feature list in the parent issue.

Why

In 1.x a resource value that cannot be formatted - Broken {0, or File {0} of {1} with one argument -
fails in three different ways: Get(key, args) throws FormatException, the Avalonia binding reports
an error, and the WPF text goes empty. One bad translation in one language can throw out of a view
model, and nothing reports it in a way an application can log. Later features (named arguments, ICU
MessageFormat) also need one place where formatting happens, and applications want one place where
they can post-process every translated text.

What changes

// Resource "Malformed" = "Broken {0"
// 2.0 call form; MessageArguments is introduced in feature 02.
Localizer.Current.Get("Malformed", MessageArguments.FromValues(1));
// 1.x (as Get("Malformed", 1)): throws FormatException; 2.0: "!Malformed!" + LookupFailed

Localizer.Current.LookupFailed += (_, e) =>
    logger.LogWarning(e.Failure.Exception, "Lookup of {Key} in {Culture} failed: {Reason}", e.Failure.Key, e.Failure.Culture, e.Failure.Reason);

Localizer.Current.MessageFormatter = new MyFormatter();   // replaceable; the default is string.Format
  • New: IMessageFormatter, ILocalizer.MessageFormatter, ILocalizer.LookupFailed with
    LookupFailedEventArgs, and MessageArgument (one argument of a lookup: its position, an
    optional name and its value; a lookup passes a MessageArgument[]).
  • Every lookup, in code and in XAML, with or without arguments, goes through the same formatter, so it
    can serve as one hook for all translated texts; the default output is identical to 1.x for
    every successful format with at least one argument and for every lookup without arguments (an empty
    argument array now counts as no arguments, see below).
  • New ILocalizer.RefreshTriggeringRevision: changes whenever the culture, the formatter, the
    failure text (LookupFailureTextBuilder, below) or the registered resource
    managers change, so bound text can re-resolve
    on every such change, not only on a language switch.
  • The default formatter does not format a value without arguments: Get(key, args) with an empty array
    no longer formats, like Get(key) and XAML without arguments.

Breaking changes

  • Formatting failures return the !key! failure text and raise LookupFailed instead of throwing;
    in XAML they render the failure text.
  • Get(key, args) with an empty array returns the resource value unformatted ({{x}} stays {{x}}).
  • MissingTranslationFormat (a format string) is replaced by LookupFailureTextBuilder, a
    Func<LookupFailure, string> that produces the failure text - the text shown instead of a translation,
    by default !key! - for a missing key and for a formatting failure. LookupFailure carries the reason
    (LookupFailureReason.TranslationNotFound or .MessageFormatFailed), the key, the lookup's culture,
    its resource manager (null for search-all), the lookup's arguments and, for a formatting failure, the
    pattern and the exception: LookupFailureTextBuilder = failure => $"[{failure.Key}]". The documentation calls this text
    the "failure text" instead of the "sentinel".
  • TranslationNotFound and TranslationNotFoundEventArgs are replaced by LookupFailed and
    LookupFailedEventArgs. The new event reports every failed lookup - a missing key and, new, a
    formatting failure - and carries the same LookupFailure that LookupFailureTextBuilder receives.
  • Custom ILocalizer implementations must add the new members.

Acceptance criteria

  • A formatting failure returns the failure text and raises LookupFailed with reason, key, culture,
    resource manager, arguments, pattern and exception, without throwing from Get or a binding unless
    a consumer event handler throws or LookupFailureTextBuilder throws or returns null.
  • A missing key raises LookupFailed with reason TranslationNotFound; the failure text is never formatted.
  • Every resolved value reaches the formatter, also from Get(key), the indexer, the enum lookups
    and XAML without arguments; the default formatter returns such values unchanged.
  • The default formatter's successful output is unchanged from 1.x.
  • RefreshTriggeringRevision advances on every real change of culture, formatter,
    lookup-failure text builder or registered managers, and never on a no-op.
  • The changelog and the new migration guide describe every break.

Details

  • Depends on: nothing.

Metadata

Metadata

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions