Montag, 29. November 2010

Complete generic multi-language with Ext.Net

This Blog moved to http://webapps-in-action.com/

This time, I provide you an example which shows a possible way of having multi language all over your Ext.Net Project. Not quite simple, but very generic. And of course still open for improvements.

I'll maybe describe it later in detail. Now in short:

Featuring: Correct CultureInfo in Threat, Automatic replacement within markup,
IDs as Enums, Complex Translation in Codebehind / Javascript with JSON and string.Format() port...



Default.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="MLang._Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Complete Multi-Language with Ext.NET</title>
</head>
<body>
    <ext:ResourceManager runat="server" ID="resMgr" />
    <form id="form" runat="server" style="padding: 10px">
    <ext:Panel ID="panel" runat="server" Title="_SimpleFormWithFieldSets" Width="350"
        Frame="true" ButtonAlign="Center">
        <Items>
            <ext:FieldSet runat="server" CheckboxToggle="true" Title="_UserInformation" AutoHeight="true"
                Collapsed="true" LabelWidth="75" Layout="Form">
                <Items>
                    <ext:TextField runat="server" FieldLabel="_Firstname" AllowBlank="false" AnchorHorizontal="100%" />
                    <ext:TextField runat="server" FieldLabel="_Lastname" AnchorHorizontal="100%" />
                    <ext:TextField runat="server" FieldLabel="_Company" AnchorHorizontal="100%" />
                    <ext:TextField runat="server" FieldLabel="_Email" AnchorHorizontal="100%" />
                </Items>
            </ext:FieldSet>
            <ext:FieldSet runat="server" CheckboxToggle="true" Title="_PhoneNumber" AutoHeight="true"
                LabelWidth="75" Layout="Form">
                <Items>
                    <ext:TextField runat="server" FieldLabel="_Home" AnchorHorizontal="100%" />
                    <ext:TextField runat="server" FieldLabel="_Business" AnchorHorizontal="100%" />
                    <ext:TextField runat="server" FieldLabel="_Mobile" AnchorHorizontal="100%" />
                    <ext:TextField runat="server" FieldLabel="_Fax" AnchorHorizontal="100%" />
                    <ext:TextField ID="txtMoney" runat="server" FieldLabel="_Money" AnchorHorizontal="100%" />
                    <ext:TextField ID="txtDate" runat="server" FieldLabel="_Date" AnchorHorizontal="100%" />
                    <ext:TextField ID="txtCodebehind" runat="server" AnchorHorizontal="100%" />
                </Items>
            </ext:FieldSet>
            <ext:Label runat="server" ID="labelText">
            </ext:Label>
        </Items>
        <Buttons>
            <ext:Button runat="server" Text="_Save" />
            <ext:Button runat="server" Text="_Cancel" />
            <ext:Button runat="server" Text="Javascript">
                <Listeners>
                    <Click Fn="LangWithinJS" />
                </Listeners>
            </ext:Button>
        </Buttons>
    </ext:Panel>
    <ext:Button runat="server" ID="changeLang" OnClick="changeLang_Click" AutoPostBack="true"
        Text="_ChangeLanguage">
    </ext:Button>
    </form>
    <ext:XScript runat="server" ID="xScript">
    <script type="text/javascript">                
        /* ----------------------------------------- */
        var LangWithinJS = function () {            
            alert(
                // THE LANGUAGE string.Format() MAGIC IN JS
                getLang('SomeText',getLang('Sentence'),getLang('Variables'))
             );                
        };
        /* ----------------------------------------- */        
        /* STRING FORMAT PORT - SHOULD BE PLACED IN STATIC.JS OR SIMILAR*/
        function getLang(id) {            
            var str, json = eval(#{language}.getValue());
            for (i = 0; i < json.length; i++) {
                if (json[i].Key == id) {
                    str = json[i].Value;
                    for (j = 0; j < arguments.length; j++) {
                        str = str.replace('{' + (j - 1) + '}', arguments[j]);
                    }
                    return str;
                }
            }
            return "#" + id + "#";
        }
        /* ----------------------------------------- */
    </script>
    </ext:XScript>
</body>
</html>

Default.aspx.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Globalization;
using Ext.Net;

namespace MLang
{
    public partial class _Default : BasePage
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            // TRANSLATION WITH ENUM / INTELLISENSE
            txtCodebehind.EmptyText = lang.translate(LangKey._Firstname);
            txtCodebehind.FieldLabel = lang.translate(LangKey._Firstname);

            // DEFAULT DATA TYPES IN C# IN CORRECT FORMAT
            txtDate.Text = DateTime.Now.ToString();
            txtMoney.Text = String.Format("{0:C}", 124404.34);

            // COMPLEX TEXT WITH VARIABLES
            labelText.Text = String.Format(lang.translate(LangKey.SomeText),
                                lang.translate(LangKey.Sentence),
                                lang.translate(LangKey.Variables));
        }
        
        // JUST A LANG SWITCH.
        protected void changeLang_Click(object sender, EventArgs e)
        {
            string currentLang = ((CultureInfo)Session["Language"]).Name;
            Session.Abandon();
            if (currentLang == "de-DE")
            {
                Response.Redirect("Default.aspx?l=en");
            }
            else
            {
                Response.Redirect("Default.aspx");
            }
        }
    }
}

BasePage.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Globalization;
using System.Threading;
using Ext.Net;

namespace MLang
{
    public class BasePage : System.Web.UI.Page
    {
        // JSON STRING KEEPER
        protected Ext.Net.Hidden language { get; set; }

        protected override void OnLoad(EventArgs e)
        {
            // LANGUAGE ALREADY LOADED?
            if (Session["Language"] == null)
            {
                // SWITCH ON QUERY STRING
                if (!string.IsNullOrEmpty(Request["l"]))
                {
                    Session["Language"] = new CultureInfo("en-US");
                }
                else
                {
                    // SET DEFAULT LANGUAGE AND 
                    Session["Language"] = new CultureInfo("de-DE");
                }
                Response.Redirect("Default.aspx");
            }
            Thread.CurrentThread.CurrentCulture = (CultureInfo)Session["Language"];
            Session.LCID = ((CultureInfo)Session["Language"]).LCID;

            // CURRENT STRINGS LOADED?
            if (lang == null)
            {
                // HARD CODED TRANSLATION, ONLY FOR UNDERSTANDING.. DB SEE BELOW
                // THE "_" IN ID IS FOR MARKUP REPLACEMENT
                Language translation = new Language();
                translation.Translate = new Dictionary<string, string>();
                if (((CultureInfo)Session["Language"]).Name == "de-DE")
                {
                    translation.Translate.Add("_Save", "Speichern");
                    translation.Translate.Add("_Firstname", "Vorname");
                    translation.Translate.Add("_Lastname", "Nachname");
                    translation.Translate.Add("_Company", "Firma");
                    translation.Translate.Add("_Email", "Email");
                    translation.Translate.Add("_UserInformation", "Benutzer Information");
                    translation.Translate.Add("_SimpleFormWithFieldSets", "Einfaches Formular mit Fieldsets");
                    translation.Translate.Add("_PhoneNumber", "Telefon");
                    translation.Translate.Add("_Home", "Privat");
                    translation.Translate.Add("_Business", "Geschäftlich");
                    translation.Translate.Add("_Mobile", "Mobil");
                    translation.Translate.Add("_Fax", "Fax");
                    translation.Translate.Add("_Cancel", "Abbrechen");
                    translation.Translate.Add("_Money", "Währung");
                    translation.Translate.Add("_Date", "Datum");
                    translation.Translate.Add("_ChangeLanguage", "Sprache wechseln");
                    translation.Translate.Add("SomeText", "Zwei {1} in einem {0}");
                    translation.Translate.Add("Variables", "variablen");
                    translation.Translate.Add("Sentence", "Satz");
                }
                else
                {
                    translation.Translate.Add("_Save", "Save");
                    translation.Translate.Add("_Firstname", "First name");
                    translation.Translate.Add("_Lastname", "Last name");
                    translation.Translate.Add("_Company", "Company");
                    translation.Translate.Add("_Email", "Email");
                    translation.Translate.Add("_UserInformation", "User Information");
                    translation.Translate.Add("_SimpleFormWithFieldSets", "Simple Form with Fieldsets");
                    translation.Translate.Add("_PhoneNumber", "Phone number");
                    translation.Translate.Add("_Home", "Home");
                    translation.Translate.Add("_Business", "Business");
                    translation.Translate.Add("_Mobile", "Mobile");
                    translation.Translate.Add("_Fax", "Fax");
                    translation.Translate.Add("_Cancel", "Cancel");
                    translation.Translate.Add("_Money", "Money");
                    translation.Translate.Add("_Date", "Date");
                    translation.Translate.Add("_ChangeLanguage", "Change language");
                    translation.Translate.Add("SomeText", "This is a {0} with two {1}");
                    translation.Translate.Add("Variables", "variables");
                    translation.Translate.Add("Sentence", "sentence");
                }

                // BETTER:
                // SIMPLE SAMPLE GET FROM DB BY de-DE or en-US

                // translation.Translate = (from l in dataContext.Translation
                //                where l.language == ((CultureInfo)Session["Language"]).Name
                //                select l).ToDictionary(item => item.key, item => item.value);

                Session["LanguageTranslation"] = translation;
            }
            
            // ADD HIDDEN FIELD WITH ALL TRANSLATION STRINGS FOR JAVASCRIPT
            // HERE YOU COULD ALSO OPTIMIZE TO LOAD ONLY NEEDED ONCES
            language = new Ext.Net.Hidden();
            language.ID = "language";
            language.Text = lang.Translate.ToJSON();
            this.Page.Controls.Add(language);
            
            // LOOP ALL CONTROLS AND REPLACE ID LABLES
            UIHelper.SetControlTexts(this, lang);

            // CONTINUE WITH BASE
            base.OnLoad(e);
        }

        // SHORTCUT 
        public Language lang
        {
            get { return (Language)Session["LanguageTranslation"]; }
        }
    }
}

Extensions.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;
using System.Text;
using System.Runtime.Serialization.Json;

namespace MLang
{
    public static class Extensions
    {
        public static string ToJSON<T>(this T obj)
        {
            MemoryStream stream = new MemoryStream();
            try
            {
                //serialize data to a stream, then to a JSON string
                DataContractJsonSerializer jsSerializer = new DataContractJsonSerializer(typeof(T));
                jsSerializer.WriteObject(stream, obj);
                return Encoding.UTF8.GetString(stream.ToArray());
            }
            finally
            {
                stream.Close();
                stream.Dispose();
            }
        }
    }
}

Language.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace MLang
{
    public enum LangKey
    {
        _Firstname,
        _Lastname,
        _Company,
        _Email,
        _UserInformation,
        _SimpleFormWithFieldSets,
        _PhoneNumber,
        _Home,
        _Business,
        _Mobile,
        _Fax,
        _Save,
        _Cancel,
        _Money,
        _Date,
        _ChangeLanguage,
        SomeText,
        Variables,
        Sentence
    }

    /// <summary>
    /// Generic Translation
    /// </summary>
    public class Language
    {
        public Dictionary<string, string> Translate { get; set; }

        /// <summary>
        /// Translates the specified language key.
        /// </summary>
        /// <param name="language">The language key.</param>
        /// <returns></returns>
        public string translate(LangKey language)
        {
            if (Translate.ContainsKey(language.ToString()))
            {
                return Translate[language.ToString()];
            }
            return "*" + language.ToString() + "*";
        }

        /// <summary>
        /// Translates the specified language key.
        /// </summary>
        /// <param name="language">The language key.</param>
        /// <returns></returns>
        public string translate(string language)
        {
            if (Translate.ContainsKey(language))
            {
                return Translate[language];
            }
            return "*" + language + "*";
        }
    }
}

UIHelper.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using Ext.Net;

namespace MLang
{
    public class UIHelper
    {
        private const string lPrefix = "_";

        /// <summary>
        /// Sets the control texts.
        /// </summary>
        /// <param name="parent">The parent.</param>
        /// <param name="lang">The lang.</param>
        public static void SetControlTexts(Control parent, Language lang)
        {
            foreach (Control c in parent.Controls)
            {
                var tf = c as TextField;
                if (tf != null)
                {
                    if (IsTranslatedText(lang, tf.EmptyText))
                    {
                        tf.EmptyText = lang.translate(tf.EmptyText);
                    }
                    if (IsTranslatedText(lang, tf.FieldLabel))
                    {
                        tf.FieldLabel = lang.translate(tf.FieldLabel);
                    }
                }
                var bt = c as Button;
                if (bt != null)
                {
                    if (IsTranslatedText(lang, bt.Text))
                    {
                        bt.Text = lang.translate(bt.Text);
                    }
                }
                var lc = c as LiteralControl;
                if (lc != null)
                {
                    if (IsTranslatedText(lang, lc.Text))
                        lc.Text = lang.translate(lc.Text);
                }
                var lb = c as Label;
                if (lb != null)
                {
                    if (IsTranslatedText(lang, lb.Text))
                        lb.Text = lang.translate(lb.Text);
                }
                var pnl = c as Panel;
                if (pnl != null)
                {
                    if (IsTranslatedText(lang, pnl.Title))
                        pnl.Title = lang.translate(pnl.Title);
                }
                var gridPnl = c as GridPanel;
                if (gridPnl != null)
                {
                    foreach (var col in gridPnl.ColumnModel.Columns)
                    {
                        if (IsTranslatedText(lang, col.Header))
                            col.Header = lang.translate(col.Header);
                        var icc = col as ImageCommandColumn;
                        if (icc != null)
                        {
                            if (IsTranslatedText(lang, icc.Tooltip))
                                icc.Tooltip = lang.translate(icc.Tooltip);
                        }

                    }
                }
                SetControlTexts(c, lang);
            }
        }
        /// <summary>
        /// Determines whether [is translated text] [the specified lang].
        /// </summary>
        /// <param name="lang">The lang.</param>
        /// <param name="text">The text.</param>
        /// <returns>
        ///  <c>true</c> if [is translated text] [the specified lang]; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsTranslatedText(Language lang, string text)
        {
            if ((!string.IsNullOrEmpty(text)) && (text.StartsWith(lPrefix)))
            {
                if (lang.translate(text) != null)
                {
                    return true;
                }
            }
            return false;
        }
    }
}

Update #1

Here the first extension. Including a site.js and correct language
JS Locale for ExtJS on every page.

1. Put this in BasePage.cs after "this.Page.Controls.Add(language);"
// INCLUDE SITE DYNAMIC
HtmlGenericControl IncludeSite = new HtmlGenericControl("script");
IncludeSite.Attributes.Add("type", "text/javascript");

IncludeSite.Attributes.Add("src", VirtualPathUtility.ToAbsolute("~/static/js/site.js"));
Page.Header.Controls.Add(IncludeSite);

// INCLUDE CORRECT LANG JS 
HtmlGenericControl IncludeLang = new HtmlGenericControl("script");
IncludeLang.Attributes.Add("type", "text/javascript");
switch (((CultureInfo)Session["Language"]).Name)
{
    case 'de-DE':
        IncludeLang.Attributes.Add("src", VirtualPathUtility.ToAbsolute("~/static/js/lang-de.js"));

        X.Js.Call("initLanguage", lang.dict);
        break;
    default:
        IncludeLang.Attributes.Add("src", VirtualPathUtility.ToAbsolute("~/static/js/lang-en.js"));
        X.Js.Call("initLanguage", lang.dict);
    break;
}
Page.Header.Controls.Add(IncludeLang);

2. Then move the JS function getLang(id) from Default.aspx into "~/static/js/site.js"

3. Finally add the ExtJS locale as your needs

//************************************************************************************
//*** GERMAN - languagecode de *******************************************************
//*** Ext.Net Standard Texts for Controls ********************************************
//*** This is a copy of the Ext.Js translations located in 
//*** <base>Ext.Net\Ext.Net\Build\Ext.Net\extjs\src\locale\ext-lang-<languagecode>.js 
//*** Project specific modifications can be made by "param.<TextCode>" 
//************************************************************************************
function initLanguage(param) {
    if (Ext.View) {
        Ext.View.prototype.emptyText = "";
    }

    if (Ext.grid.GridPanel) {
        Ext.grid.GridPanel.prototype.ddText = "{0} Zeile(n) ausgewählt";
    }
...

//************************************************************************************
//*** ENGLISH - languagecode en ******************************************************
//*** Ext.Net Standard Texts for Controls ********************************************
//*** This is a copy of the Ext.Js translations located in 
//*** <base>Ext.Net\Ext.Net\Build\Ext.Net\extjs\src\locale\ext-lang-<languagecode>.js 
//*** Project specific modifications can be made by "param.<TextCode>" 
//************************************************************************************
function initLanguage(param) {
    /*!
    * Ext JS Library 3.3.0
    * Copyright(c) 2006-2010 Ext JS, Inc.
    * licensing@extjs.com
    * http://www.extjs.com/license
    */
    /**
    * List compiled by mystix on the extjs.com forums.
    * Thank you Mystix!
    *
    * English Translations
    * updated to 2.2 by Condor (8 Aug 2008)
    */

    Ext.UpdateManager.defaults.indicatorText = '<div class="loading-indicator">Loading...</div>';

    if (Ext.data.Types) {
        Ext.data.Types.stripRe = /[\$,%]/g;
    }
...

Update #2

There is an easier way for multi language in the markup, using the default "App_GlobalResources".
This makes sense if you don't need everything loaded dyamically from db or other source.

Just make with Visual Studio a resource file for English (LocalizedText.resx) and then for French equivelent (LocalizedText.fr.resx) for example.

Each resource files contain a Name/Value pair, eg.:

Name=lang_DeleteItem
Value=Delete selected item 

Name=lang_DeleteItem
Value=Effacez l'article choisi 

Now, the only thing to do in mark up is to point link the resource

<ext:Button ID="WCAdd" 
runat="server" Icon="TableAdd" 
MinWidth="75" 
ToolTip="<%$ Resources: LocalizedText, lang_DeleteItem %>"></ext:Button>

On each page load, the value is substituted based on the Thread.CurrentThread.CurrentCulture setting.
Same simple thing with Code behind in C#:

Button.ToolTips = Resources.LocalizedText.ResourceManager.GetString("lang_DeleteItem");

This is the simplest way in ASP.NET. Thanks and credits for this hint goes to Doug Romans (USA).

Keine Kommentare:

Kommentar veröffentlichen