Comment traduire un site Asp.net MVC ?

 

La traduction d’un site en plusieurs langues est une tache difficile, et les façons de faire sont nombreuses, je vais proposer ici une methode qui permet de traduire “à la volée” un site web réalisé avec la technologie asp.net MVC.

Le problème principal pour un traducteur, est d’obtenir le contexte de traduction (la page dans laquelle se trouve le texte et son ensemble), la traduction mot à mot engendre systematiquement des aproximations. un autre problème est le format d’echange avec le traducteur, puis la réintroduction des textes traduits. Bref la galère…

Le problème principal pour un webdesigner est qu’il doit editer des fichiers de ressources, bien localiser les textes dans les pages puis trouver leurs correspondances dans des fichier généralement en xml. Bref la galère…

Pour resoudre les principaux problèmes du traducteur, je propose qu’il puisse traduire directement dans la page comme un wiki. Ce qui supprime pratiquement le problème du webdesigner et de ses fichiers, il reste néanmoins la definitions des textes dans les pages a mettre en place, pour cela je propose l’ecriture suivante :

Il s’agit de la page index.aspx par defaut lorsque l’on crée un nouveau projet de type asp.net mvc

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="indexTitle" ContentPlaceHolderID="TitleContent" runat="server">
Home Page
</asp:Content>

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
<h2><%= Html.Encode(ViewData["Message"]) %></h2>
<p>
<%=Html.Localize("Content1","To learn more about ASP.NET MVC visit")%><a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.
</p>
</asp:Content>



Concernant le fichier index.aspx, l’idée est de sauvegarder un fichier par language avec le nom suivant :



index.aspx.fr.resx



index.aspx.en.resx



ect…



le contenu des fichiers est le suivant :




<?xml version="1.0" encoding="utf-8"?>
<localizations>
<localization key="Title"><![CDATA[Mon Application MVC]]></localization>
<localization key="Content1"><![CDATA[Pour en apprendre d'avantage a propos d'ASP.Net MVC allez sur]]></localization>
</localizations>



On retrouve l’identifiant “Content1”



Le but de la methode d’extension Localize(..) est de reconnaitre le language en cours et de retrouver le bon terme.



voici le détail de cette methode :




public static string Localize(this HtmlHelper helper, string contentKey, string defaultContent)
{
string translationLink = null;
var localizationService = new Services.LocalizationService();
var view = helper.ViewContext.View as System.Web.Mvc.WebFormView;
var path = view.ViewPath.Replace("~", "");
if (helper.ViewContext.HttpContext.Request["translate"] == "1")
{
translationLink = helper.RouteLink("translate"
, new { controller = "Localization"
, action = "LocalizeContent"
, id = string.Empty
, path = path
, key = contentKey
});
}
if (view != null)
{
var localization = localizationService.GetContentLocalization(path, contentKey);
if (localization != null)
{
return localization.Value + "&nbsp;" + translationLink + "&nbsp;";
}
}

return defaultContent + "&nbsp;" + translationLink + "&nbsp;";
}



Dans un premier temps elle va instancier le service de traduction, recuperer le nom du fichier en cours et interroger le service de traduction pour savoir s’il existe un terme pour le fichier en cours avec l’identifiant contentKey , si c’est le cas le terme est retourné, sinon on utilise le terme defaultContent.



On peut voir aussi que l’on verifie l’existence d’un parametre translate=1 dans l’url, c’est ce parametre qui permet de traduire les pages à la volée, s’il est la, on ajoute un lien pour la traduction du terme a coté du terme a traduire, de telle sorte que le traducteur retrouve son contexte.



Voici le service de traduction :




public class LocalizationService : ILocalizationService
{
public LocalizationService()
{

}

private List<Models.ContentLocalization> m_LocalizationList;
private static object m_Lock = new object();

public IList<Models.ContentLocalization> LocalizationList
{
get
{
if (m_LocalizationList == null)
{
lock (m_Lock)
{
if (m_LocalizationList == null)
{
m_LocalizationList = new List<Models.ContentLocalization>();
var currentDirectory = System.Web.HttpContext.Current.Server.MapPath("/");
var fileList = from file in System.IO.Directory.GetFiles(currentDirectory, "*.resx", System.IO.SearchOption.AllDirectories)
select file;

foreach (var file in fileList)
{
XDocument doc = null;
string content = null;
System.IO.StreamReader fs = null;
try
{
fs = System.IO.File.OpenText(file);
content = fs.ReadToEnd();
doc = XDocument.Parse(content);
}
catch
{
continue;
}
finally
{
if (fs != null)
{
fs.Close();
}
}

var path = System.IO.Path.GetFileName(file);
var language = path.Split('.')[2];
path = file.Replace(string.Format(".{0}.resx", language), "");
path = path.Replace(currentDirectory, "");
path = '/' + path.Replace('\\', '/');
m_LocalizationList.AddRange(from x in doc.Root.Elements()
select new Models.ContentLocalization()
{
Key = x.Attribute(XName.Get("key")).Value,
Value = x.Value,
Path = path,
Language = language,
});

}
}
}

}
return m_LocalizationList;
}
}

public static string DefaultLanguage
{
get
{
var language = (string)System.Web.HttpContext.Current.Session["language"];
if (language == null)
{
language = "fr";
}
return language;
}
set
{
System.Web.HttpContext.Current.Session["language"] = value;
}
}

#region ILocalizationService Members

/// <summary>
/// Gets the content localization.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="key">The key.</param>
/// <returns></returns>
public Models.ContentLocalization GetContentLocalization(string path, string key)
{
return LocalizationList.FirstOrDefault(i => i.Path.Equals(path, StringComparison.InvariantCultureIgnoreCase)
&& i.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)
&& i.Language.Equals(LocalizationService.DefaultLanguage, StringComparison.InvariantCultureIgnoreCase));
}

public IList<Models.ContentLocalization> GetContentLocalizationList(string path, string key)
{
var list = LocalizationList.Where(i => i.Path.Equals(path, StringComparison.InvariantCultureIgnoreCase)
&& i.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)).ToList();

foreach (var language in GetLanguageList())
{
var item = list.SingleOrDefault(i => i.Language.Equals(language, StringComparison.InvariantCultureIgnoreCase));
if (item == null)
{
list.Add(new Models.ContentLocalization()
{
Key = key,
Path = path,
Language = language,
Value = null,
});
}
}

return list;
}

/// <summary>
/// Sauvegarde une liste de traduction.
///
/// le principe est de sauvegarder un fichier par langue pour une page ou un controle donné
///
/// par exemple si le path est :
///
/// /views/home/index.aspx
///
/// la liste des traductions est fr, en alors, il y a creation ou append de 2 fichiers
///
/// /views/home/index.aspx.fr.resx
/// /views/home/index.aspx.en.resx
///
/// </summary>
/// <param name="list">The list.</param>
public void Save(IList<Models.ContentLocalization> list)
{
var rootPath = System.Web.HttpContext.Current.Server.MapPath("/");

foreach (var localization in list)
{
if (string.IsNullOrEmpty( localization.Value))
{
continue;
}
var path = System.IO.Path.Combine(rootPath, localization.Path.Trim('
/').Replace('/', '\\'));
path = string.Format("{0}.{1}.resx", path, localization.Language);

XDocument doc = null;
if (System.IO.File.Exists(path))
{
using (var fs = System.IO.File.OpenText(path))
{
doc = XDocument.Parse(fs.ReadToEnd());
fs.Close();
}
var match = doc.XPathSelectElement(string.Format("//localization[@key='
{0}']", localization.Key));
if (match != null)
{
match.RemoveAll();
match.Add(new XCData(localization.Value));
match.Add(new XAttribute("
key", localization.Key));
}
else
{
var elem = new XElement("
localization", new XCData(localization.Value));
elem.Add(new XAttribute("
key", localization.Key));
doc.Root.Add(elem);
}
}
else
{
doc = new XDocument(new XDeclaration("
1.0", "UTF-8", null));
var root = new XElement("
localizations");
var elem = new XElement("
localization", new XCData(localization.Value));
elem.Add(new XAttribute("
key", localization.Key));
root.Add(elem);
doc.Add(root);
}
doc.Save(path);
}
m_LocalizationList = null;
}

public IEnumerable<string> GetLanguageList()
{
yield return "
fr";
yield return "
en";
yield return "
es";
yield break;
}

#endregion
}







il implemente l’interface suivante :




public interface ILocalizationService
{
/// <summary>
/// Retourne une traduction en fonction du chemin de la vue et de la clé de traduction
/// </summary>
/// <param name="path">The path.</param>
/// <param name="key">The key.</param>
/// <returns></returns>
Models.ContentLocalization GetContentLocalization(string path, string key);
/// <summary>
/// Retourne la liste de toutes les traductions possible d'un contenu
/// </summary>
/// <param name="path">The path.</param>
/// <param name="key">The key.</param>
/// <returns></returns>
IList<Models.ContentLocalization> GetContentLocalizationList(string path, string key);

/// <summary>
/// Sauvegarde une liste de traduction.
/// </summary>
/// <param name="list">The list.</param>
void Save(IList<Models.ContentLocalization> list);

/// <summary>
/// Liste des language de traduction
/// </summary>
/// <returns></returns>
IEnumerable<string> GetLanguageList();
}





Comme je l’ai évoqué , si le parametre translate=1 est present, des liens son générés dans la page, ces liens utilisent le controlleur de traduction dont voici le code :




public class LocalizationController : Controller
{
public LocalizationController()
{
this.LocalizationService = new Services.LocalizationService();
}

protected Services.ILocalizationService LocalizationService { get; set; }

public ViewResult LocalizeContent(string path, string key)
{
var list = LocalizationService.GetContentLocalizationList(path, key);
ViewData.Model = list;
ViewData["Key"] = key;
ViewData["Path"] = path;
return View();
}

public ViewResult SaveLocalizedContent(FormCollection form)
{
string key = form["Key"];
string path = form["Path"];
var list = LocalizationService.GetContentLocalizationList(path, key);
var languages = form["lg"].Split(',');
var values = form["value"].Split(',');

for (int i = 0; i < languages.Length; i++)
{
var language = languages[i];
var value = values[i];
var localization = list.Single(l => l.Language.Equals(language, StringComparison.InvariantCultureIgnoreCase));
localization.Value = value;
}

LocalizationService.Save(list);

ViewData.Model = list;
ViewData["Key"] = key;
ViewData["Path"] = path;

return View("LocalizeContent");
}

}





Je n’ai pas évoqué la securité, mais bien sur il ne faut pas utiliser cette solution telle qu’elle, en effet il ne faut pas qu’un internaute puisse retraduire le site, mais on peut s’appuyer facilement sur les membership asp.net, ajouter un attribut “Authorize” sur les methodes du controlleur de traduction.



Pour la demo, il faut que le user sous lequel tourne le site web ai les droits d’ecriture dans le repertoire et sous-repertoire views.



pour voir plus en détail cette solution , voici le code source :


Aucun commentaire: