Jednoduchý správce překladů v Metru/Modern UI


Jak jednoduše používat lokalizaci v Metru, resp. Modern/Windows 8 UI? Tentokrát i včetně vzorové aplikace…

Základem toho velmi jednoduchého řešení je systém Resources, resp. stringových překladů z Metra, resp. Modern/Windows 8 UI, takže je nutné mít v projektu adresář Strings, ve kterém se dál dělí jednotlivé jazyky do podadresářů podle zkratky jazyka (LanguageTag) a ty obsahují již jednotlivé Resources soubory s překlady (*.resw). Toto řešení vychází z předpokladu, že všechny jazyky by měly mít stejné soubory; v příložném a rozebíraném příkladu se jedná o dva soubory, resp. slovníky Titles.resw a Messages.resw pro 2 jazyky – češtinu a angličtinu… (Pozn.: dále je vhodné, aby stejné soubory obsahovali i shodné překlady, minimálně klíče a k tomu tedy i odpovídající překladové hodnoty). Pro identifikaci překladových souborů je použit Enum ELocalizedType, který obsahuje názvy slovníků (v našem případě tedy 2 hodnoty – Titles a Messages), přes které se dál se slovníky pracuje ve třídě LocalizationManager, popř. pro GUI v LocalizationConverteru. Pro usnadnění práce se v GUI navíc používá “helper” třída Translater, která je šitá na míru danému řešení…

Konkrétně by měl být tedy adresář Strings ve spouštěcím projektu na hlavní úrovni a struktura vypadá následovně:

Strings
– cs (popř. cs_CZ)
— Titles.resw
— Messages.resw
– en (popř. en_US apod.)
— Titles.resw
— Messages.resw

Pozn.: překladové Resources soubory jsou stejné jako v dalších technologiích z “XAMLové” rodiny .NET Frameworku, např. jako *.resx ve WPF a jedná se o princip klíč – hodnota – (popis) formou textu.

Definice výčtu ELocalizedType je tak(é) jasná:

public enum ELocalizedType
{
  Titles,
  Messages,
}

Třída LocalizationManager představuju hlavní část celého řešení:

public static class LocalizationManager
{
  private static readonly Dictionary<string resourcemap ,> s_ResourceLoadersCache = new Dictionary<string resourcemap ,>();
  private static ResourceContext s_Context;


  public static ResourceContext Context
  {
    get { return s_Context ?? (s_Context = new ResourceContext()); }
  }


  public static bool ChangeLang(string lang)
  {
    if (ApplicationLanguages.PrimaryLanguageOverride == lang)
      return false;
    ApplicationLanguages.PrimaryLanguageOverride = lang;
    Context.Languages = new[] { lang };
    return true;
  }

  public static IEnumerable<language> GetLanguages()
  {
    return ApplicationLanguages.Languages.Select(language => new Language(language));
  }

  public static ResourceMap GetResourceMap(string resourceFileName)
  {
    ResourceManager current = ResourceManager.Current;
    if (current == null)
      return null;
    ResourceMap mainResourceMap = current.MainResourceMap;
    if (mainResourceMap == null)
      return null;
    return mainResourceMap.GetSubtree(resourceFileName);
  }

  public static bool TryGetTranslate(string resourceFileName, string key, out string translate, string suffix = null, string prefix = null)
  {
    ResourceMap resourceMap;
    if (!s_ResourceLoadersCache.TryGetValue(resourceFileName, out resourceMap))
    {
      try
      {
        resourceMap = GetResourceMap(resourceFileName);
        s_ResourceLoadersCache.Add(resourceFileName, resourceMap);
      }
      catch (Exception ex)
      {
        Debug.WriteLine(string.Format("[LocalizationManager.TryGetTranslate] {0}@{1}.resw: {2}", key, resourceFileName, ex.Message));
        translate = null;
        return false;
      }
    }
    return TryGetTranslate(resourceMap, key, out translate, suffix, prefix);
  }

  public static string GetTranslate(string resourceFileName, string key, string suffix = null, string prefix = null)
  {
    string translate;
    return TryGetTranslate(resourceFileName, key, out translate, suffix, prefix) ? translate : key;
  }

  public static bool TryGetTranslate(ResourceMap resourceMap, string key, out string translate, string suffix = null, string prefix = null)
  {
    if (resourceMap == null)
      throw new ArgumentNullException("resourceMap");
    if (string.IsNullOrEmpty(key))
      throw new ArgumentNullException("key");
    ResourceCandidate candidate = resourceMap.GetValue(key, Context);
    if (candidate == null)
    {
      translate = null;
      return false;
    }
    else
    {
      translate = string.Format("{2}{0}{1}", candidate.ValueAsString, suffix, prefix);
      return true;
    }
  }

  public static string GetTranslate(ResourceMap resourceMap, string key, string suffix = null, string prefix = null)
  {
    string translate;
    return TryGetTranslate(resourceMap, key, out translate, suffix, prefix) ? translate : key;
  }
}

Zde se mj. nachází metoda GetLanguages pro získání kolekce všech aktivních jazyků v aplikaci a dále hlavně přetížené metody pro samotný překlad TryGetTranslate a GetTranslate podle zadaných parametrů. Pomocí metody ChangeLang je možné v aplikaci změnit jazyk, resp. programově nastavit aktivní jazyk přes ApplicationLanguages.PrimaryLanguageOverride a (aktuální překladový) Context, ale pak je ještě potřeba provést jeden krok v GUI z volaného místa a to aktualizovat rodičovskou stránku:

private void LanguagesComboBoxOnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
  string lang = u_LanguagesComboBox.SelectedValue as string;
  if (string.IsNullOrEmpty(lang))
    return;
  bool result = LocalizationManager.ChangeLang(lang);
  if (result)
    Frame.Navigate(GetType()); // reload UI
}

Jak už by řečeno, pro jednodušší použití v GUI je připravena pomocná třída Translater, ve které jsou definovány Attached Properties pro překlad podle typu požadovaného překladového slovníku přímo v XAMLu:

public class Translater : DependencyObject
{
  #region Title

  public static readonly DependencyProperty TitleProperty
    = DependencyProperty.RegisterAttached("Title", typeof(string), typeof(Translater), new PropertyMetadata(null));

  public static string GetTitle(UIElement element)
  {
    if (element == null)
      throw new ArgumentNullException("element");
    object value = element.GetValue(TitleProperty);
    return (value == null) ? null : value.ToString();
  }

  public static void SetTitle(UIElement element, string value)
  {
    if (element == null)
      throw new ArgumentNullException("element");
    if (string.IsNullOrEmpty(value))
      throw new ArgumentNullException("value");
    element.SetValue(TitleProperty, value);
    string translate = GetTitle(value, GetSuffix(element), GetPrefix(element));
    SetElementText(element, translate);
  }

  #endregion

  #region Message

  public static readonly DependencyProperty MessageProperty
    = DependencyProperty.RegisterAttached("Message", typeof(string), typeof(Translater), new PropertyMetadata(null));

  public static string GetMessage(UIElement element)
  {
    if (element == null)
      throw new ArgumentNullException("element");
    object value = element.GetValue(MessageProperty);
    return (value == null) ? null : value.ToString();
  }

  public static void SetMessage(UIElement element, string value)
  {
    if (element == null)
      throw new ArgumentNullException("element");
    if (string.IsNullOrEmpty(value))
      throw new ArgumentNullException("value");
    element.SetValue(MessageProperty, value);
    string translate = GetMessage(value, GetSuffix(element), GetPrefix(element));
    SetElementText(element, translate);
  }

  #endregion

  #region Shared

  private static void SetElementText(UIElement element, string translate)
  {
    TextBlock textBlock = element as TextBlock;
    if (textBlock == null)
    {
      ContentControl contentControl = element as ContentControl;
      if (contentControl == null)
        throw new NotImplementedException("Set Title Element type");
      string itemType = AutomationProperties.GetItemType(element);
      if (string.IsNullOrEmpty(itemType))
        contentControl.Content = translate;
      else
        AutomationProperties.SetName(contentControl, translate);
    }
    else
      textBlock.Text = translate;
    // todo: ošetřit další typy controlů
  }

  #endregion

  #region Suffix

  public static readonly DependencyProperty SuffixProperty
    = DependencyProperty.RegisterAttached("Suffix", typeof(string), typeof(Translater), new PropertyMetadata(null));

  public static string GetSuffix(UIElement element)
  {
    if (element == null)
      throw new ArgumentNullException("element");
    object value = element.GetValue(SuffixProperty);
    return (value == null) ? null : value.ToString();
  }

  public static void SetSuffix(UIElement element, string value)
  {
    if (element == null)
      throw new ArgumentNullException("element");
    element.SetValue(SuffixProperty, value);
  }

  #endregion

  #region Prefix

  public static readonly DependencyProperty PrefixProperty
    = DependencyProperty.RegisterAttached("Prefix", typeof(string), typeof(Translater), new PropertyMetadata(null));

  public static string GetPrefix(UIElement element)
  {
    if (element == null)
      throw new ArgumentNullException("element");
    object value = element.GetValue(PrefixProperty);
    return (value == null) ? null : value.ToString();
  }

  public static void SetPrefix(UIElement element, string value)
  {
    if (element == null)
      throw new ArgumentNullException("element");
    element.SetValue(PrefixProperty, value);
  }

  #endregion

  #region Other

  public static string GetTitle(string key, string suffix = null, string prefix = null)
  {
    return LocalizationManager.GetTranslate(ELocalizedType.Titles.ToString(), key, suffix, prefix);
  }

  public static string GetMessage(string key, string suffix = null, string prefix = null)
  {
    return LocalizationManager.GetTranslate(ELocalizedType.Messages.ToString(), key, suffix, prefix);
  }

  #endregion
}

Nastavování překladů tímto způsobem je zatím vyřešeno pouze pro TextBlocky a jejich property Text… (viz todo komentář výše)

Pozn.: i když nemám moc rád používání regionů kvůli snazší přehlednosti např. v kódu někoho jiného nebo rychlému přehledu třídy, tak zde je podle mě právě výjimka potvrzující pravidlo :)

A pro rozšířené použití v GUI je ještě připraven LocalizationConverter:

public class LocalizationConverter : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, string language)
  {
    if (value == null || parameter == null)
      return value;
    string localizationResources = parameter.ToString();
    ELocalizedType localizedType;
    if (!Enum.TryParse(localizationResources, true, out localizedType))
      throw new KeyNotFoundException(string.Format("Localized Type: ", localizationResources));
    string key = value.ToString();
    return LocalizationManager.GetTranslate(localizedType.ToString(), key); ;
  }

  public object ConvertBack(object value, Type targetType, object parameter, string language)
  {
    throw new NotImplementedException("Localization Converter Convert Back");
  }
}

Ten se nechá využít u všech “obsahových” controlů, např.:

<Page.Resources> 
  <Converters:LocalizationConverterx:Key="r_LocalizationConverter" /> 
</Page.Resources> 

<ComboBox ... 
          SelectedValue="{Binding Path=MyPropertyName, Converter={StaticResource r_LocalizationConverter}, ConverterParameter=Titles}" 
          ... /> 

<Button ... 
        Content="{Binding Path=MyPropertyName, Converter={StaticResource r_LocalizationConverter}, ConverterParameter=Titles}" 
        ... /> 

Na závěr je ještě potřeba dodat, že hlavní nevýhoda řešení nastává v případě, když potřebujete pro překládanou stránku použít NavigationCacheMode = Required, přičemž v takovém případě nefunguje online změna překladů a je vždy nutné provést restart aplikace… Nicméně pro základní použití toto řešení naprosto postačuje, tak se snad bude někomu hodit… :)

Vzorovou aplikaci můžete stahovat z mého SkyDrivu.

A ještě jedna poznámka na závěr, minimálně s úpravy změny jazyka (metoda ChangeLang) a aktualizace v GUI by mělo být možné toto řešení použít i v jiných derivacích XAMLu…

, , , , ,

Komentáře jsou uzavřeny.