terça-feira, 15 de outubro de 2013

Globalização dinâmica usando ResourceDirectory em WPF

Há alguns dias eu me deparei com seguinte feature em uma aplicação:
- Implementar o recurso de globalização (vários idiomas). A princípio, Inglês e Espanhol.
Até aqui, tudo bem, só criar os Resources para cada idioma.
Até que surgiu o segundo feature:
- Permitir mudar o idioma, refletindo automaticamente a alteração em toda interface.
Até onde sei, a única forma de conseguir isso é setando o CurrentCulture da Thread em execução antes da chamada da função InitializeComponent de cada interface. Mas não seria dinâmico, a janela precisaria ser fechada e reaberta.
Pesquisando um pouco eu encontrei alguns artigos sobre o uso de ResourceDictionary, MarkupExtensions para se atingir esse dinamismo.
Baseado nessas informações eu montei um pequeno engine que permite traduzir qualquer FrameworkElement (ex: Window, UserControl, Page) dinamicamente.

Criando o engine

Como a classe é extremamente simples e seu conteúdo é self-explanatory, segue abaixo o código fonte na íntegra.
public static void Translate(FrameworkElement element)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.IO;
using System.Reflection;
using System.Globalization;

namespace Language
{
    public static class LanguageCore
    {
        private static List RegisteredElements = new List();
        private static Dictionary CachedResourceDictionary = new Dictionary();
        private static string ResourcePath = "\\Resources";
        private static string GlobalDictionary = "Global";

        public static void SetResourcePath(string resourcePath)
        {
            ResourcePath = resourcePath;
        }

        public static void SetGlobalDictionary(string globalDictionary)
        {
            GlobalDictionary = globalDictionary;
        }

        public static void Translate(FrameworkElement element)
        {
            var dictionaryName = element.GetType().Name;;
            var dict = GetResourceDictionary(dictionaryName);
            if (dict != null)
            {
                SetElementResourceDictionary(element, dict);
                RegisterElement(element);
            }
        }

        public static string GetLanguage()
        {
            var lang = (string)Properties.Settings.Default["Language"];
            if (lang == String.Empty)
                return Thread.CurrentThread.CurrentCulture.Name;
            else
                return lang;
        }

        public static void SetLanguage(string language)
        {
            if (GetLanguage() == language) return;

            SaveLanguage(language);
            CallInitializeLanguageAllElements();
        }

        public static ResourceDictionary GetGlobalDictionary()
        {
            return GetResourceDictionary(GlobalDictionary);
        }

        private static ResourceDictionary GetResourceDictionary(string resourceName)
        {
            var fileName = GetResourceDictionaryFileName(resourceName);
            
            ResourceDictionary dict = new ResourceDictionary();
            if (CachedResourceDictionary.ContainsKey(fileName))
                dict = CachedResourceDictionary[fileName];
            else
            {
                try
                {
                    dict.Source = new Uri(fileName, UriKind.Relative);
                    CachedResourceDictionary.Add(fileName, dict);
                }
                catch
                {
                    return null;
                }
            }
            return dict;
        }

        private static string GetResourceDictionaryFileName(string resourceName)
        {
            var elementName = resourceName;
            var currentLanguage = GetLanguage();
            return Path.Combine(ResourcePath, currentLanguage, elementName + ".xaml"); ;
        }

        private static void RegisterElement(FrameworkElement element)
        {
            if (!RegisteredElements.Contains(element))
            {
                RegisteredElements.Add(element);
                element.Unloaded += element_Unloaded;
            }
        }

        private static void UnregisterElement(FrameworkElement element)
        {
            if (RegisteredElements.Contains(element))
                RegisteredElements.Remove(element);
        }

        private static void element_Unloaded(object sender, RoutedEventArgs e)
        {
            UnregisterElement((FrameworkElement)sender);
        }

        private static void CallInitializeLanguageAllElements()
        {
            foreach (FrameworkElement e in RegisteredElements)
                Translate(e);
        }

        private static void SetElementResourceDictionary(FrameworkElement element, ResourceDictionary dict)
        {
            foreach (DictionaryEntry res in dict)
            {
                if (element.Resources.Contains(res.Key))
                    element.Resources[res.Key] = dict[res.Key];
                else
                    element.Resources.Add(res.Key, dict[res.Key]);
            }
        }

        private static void SaveLanguage(string Language)
        {
            Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(Language);
            Properties.Settings.Default["Language"] = Language;
            Properties.Settings.Default.Save();
        }
    }
}

Utilizando o engine

Chame a função Translate em cada interface que será traduzida. Eu costumo colocar no construtor, logo após o InitializeComponent.
Ex:
namespace demo
{
    /// 
    /// Interaction logic for MainWindow.xaml
    /// 
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            LanguageCore.Translate(this);
        }
    }
}
Para que o engine "encontre" os resources de cada idioma, no projeto WPF crie a seguinte estrutura de pastas:
\Resources\en-US (para resources em inglês)
\Resources\pt-BR (para resources em português)
Adicione um ResourceDictionary com o mesmo nome da interface, por exemplo:
\Resources\pt-BR\MainWindow.xaml.
No XAML da interface adicione o resource padrão (também será o resource de fallback).
    
        
    
Para ver em design time como a interface ficará em outro idioma, basta alterar o idioma padrão. Adicione o texto que será utilizado na interface no ResourceDictionary.

    Teste de aplicação

Utilize o resource criado através do DynamicResource.

Para alterar o idioma utilize a função SetLanguage.
private void ingles_Click(object sender, RoutedEventArgs e)
{
    LanguageCore.SetLanguage("en-US");
}
Eu implementei um recurso que disponibiliza um resource global, onde eu uso em outras áreas do sistema, diretamente no código fonte. Para utilizar este recurso é necessário criar um ResourceDictionary chamado Global.xaml em cada pasta de cada idioma.

    Ocorreu um erro inesperado de comunicação com o aparelho./s:String>

Utilizando o resource:
    MessageBox.Show((string)LanguageCore.GetGlobalDictionary()["ErroComunicacao"], TITULO_APLICACAO, MessageBoxButton.OK, MessageBoxImage.Error);

Algumas melhorias implementadas

- Mecanismo de "cache" dos ResourceDictionray.
- Ao se alterar o idioma, o engine fará a tradução de todas as interfaces registradas.
- Remoção do registro da interfaces no evento Unloaded.

Inicialmente eu utilizava o método de adicionar o ResourceDictionary ao MergedDictionaries do FrameworkElement em tradução, mas em alguns casos mais complexos a interface não era traduzida, por isso resolvi iterar os resources, o processo demanda um maior processamento, mas funcionou em todos os casos.

Além de utilizar string é possível utilizar outros tipos de resource, por exemplo, a bandeira do idioma atual.


    Ocorreu um erro inesperado de comunicação com o aparelho./s:String>
    

Na interface, utilize da seguinte forma.

God bless!

Nenhum comentário:

Postar um comentário