Локализация .NET приложения, а в частности ASP.NET WebForms

Привет Хабрадруг!
Я, конечно, понимаю, что тема это уже заезжена до дыр, но попытаюсь описать мое видение локализации приложения и ее реализацию. И так, что мы хотим получить в итоге: Систему быстрой локализации .NET приложения, а в частности WebForms, на стадии пост разработки. По пунктам:

1. Разработка приложения: минимум внимания локализации.
2. Завершение разработки: минимум времени на локализацию.
И ещё надо, что бы это все было максимально удобно и просто.

Если коротко, то все будет выглядеть так:

У нас будет метод, что то на подобии gettext, в который мы будем передавать какой-то текст, или ключ + текст, а метод в свою очередь нам вернет локализованную строку или же то, что было в него передано. А также будет утилита, которая пропарсит *.cs, *.aspx, *.ascx файлы в поисках этого самого метода, получит оттуда ключи и текст, посмотрит или данные тексты и ключи уже имеются в ресурсах приложения. Потом мы воспользуемся google translate, что бы хоть худо бедно, да перевести новые тексты. И напоследок отредактируем переводы и сохраним это все назад в ресурсы. Готово.
Явно, что похожий, или может даже лучший механизм уже разработан, но полазив в интернете, увидел только мега тяжёлые и к тому-же платные решения.



Итак, приступим к реализации. Начнем со вспомогательного класса, a для server-side контролов c ExpressionBuilder-a:

public static class L
{
  private static readonly Regex KeyRepairRegex = new Regex("[^\\w\\d]", RegexOptions.Compiled);
  
  public static string Run(string value)
  {
    var key = GenerateKey(value);
    var localized = Resources.ResourceManager.GetString(key);
    return string.IsNullOrEmpty(localized) ? value : localized;
  }

  public static string Run(string key, string value)
  {
    var localized = Resources.ResourceManager.GetString(key);
    return string.IsNullOrEmpty(localized) ? value : localized;
  }

  public static string Run(string key, string value, params object[] format)
  {
    var localized = Run(key, value);

    if (format != null && format.Length > 0)
    {
      try
      {
        localized = string.Format(localized, format);
      }
      catch
      {
        
      }
    }
    return localized;
  }

  private static string GenerateKey(string value)
  {
    if (string.IsNullOrEmpty(value)) return value;

    value = value.Length > 23 ? value.Substring(0, 23) + "__" : value;    
    value = KeyRepairRegex.Replace(value, "_");
    if (char.IsUpper(value[0])) value = "_" + value;
    value = value.ToLowerInvariant();

    return value;
  }
}


Теперь, во время разработки, в местах, где нам понадобится локализация пишем:

<%= L.Run("Hello, Mr.Everybody") %>


Класс L специально не находиться ни в каком namespace, что бы не приходилось в *.ascx файлах делать его импорт.

<%@ Import Namespace ="Example.Namespace" %>


Здесь есть, как видно из кода, не мало важный метод GenerateKey. Если мы передаем только текст, без ключа, то этот метод сгенерирует ключ и именно этот ключ мы будем искать в ресурсах. Ключ в ресурсах может содержать только символы [a-z0-9_]. И наша утилита будет использовать этот же метод для генерации ключей. Сейчас еще рассмотрим ExpressionBuilder и приведу более конкретный пример всей схемы. Для чего же нам нужен этот самый Builder?

Конструкция <%=%> в серверных контролах приведет к ошибке, а для <%# %> надо дополнительно вызывать DataBind метод, что не очень удобно. Нам нужен класс унаследованный от ExpressionBuilder.

public class LExpressionBuilder: ExpressionBuilder
  {
    public override CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
    {
      var thisType = new CodeTypeReferenceExpression(GetType());      
      var expression = new CodePrimitiveExpression(entry.Expression.Trim());
      const string evaluationMethod = "Localize";
      return new CodeMethodInvokeExpression(thisType, evaluationMethod, new CodeExpression[] { expression });
    }

    public static string Localize(string expression)
    {
      return L.Run(expression);
    }

    public override object EvaluateExpression(object target, BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
    {
      return Localize(entry.Expression);
    }

    public override bool SupportsEvaluate
    {
      get { return true; }
    }
  }


В результате пишем:

<input typ='submit' id='btnSubmit' runat='server' value="<%$ L:A people free to choose will always choose peace %>"/>


Первая часть готова. Переходим к утилите. Это обыкновенное WinForms приложение.

Диаграмма Классов

Как видно из диаграммы, код довольно прямолинейный. Но главное — простой. Мы загружаем нужный нам Solution или проект, указываем в классе Settings путь к нашему файлу ресурсов. И ...«поехали»! Solution класс загрузит все файлы. CodeParser загрузит все строки и ключи в класс Translation, который в свою очередь, загрузит все переводы с ресурсов и покажет все это в GridView. Теперь можно переводить вручную или через google, можна сохранить оригинальные строки в текстовый файл или что то еще реализовать, к примеру, сохранение в xml.

Assembla Subversion(Регистрация не обязательна)

Утилита эта написана для себя нa скорую руку, по принципу: «лишь бы работало», поэтому попрошу гуру не судить строго. Если будет у кого то интерес, могу довести это дело до, хотя бы, альфа версии.

Что ещё хочется сделать:

CodeParser выдирая строки из файлов запоминает их позиции. А так как большинство локализированных строк идут у меня изначально без ключей, то на их генерацию уходит несколько лишних наносекунд, поэтому утилита должна после вставлять в файлы ключи к строкам.

Но и без этого все работает очень шустро, хотя буду рад выслушать любую критику или пожелания.

Спасибо за внимание.

* Source code was highlighted with Source Code Highlighter.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 2

    0
    а два пользователя с разными языками смогут одновременно работать?
      0
      Конечно. Эта вся схема в сущности ни чем не отличается, если бы мы просто писали Resources.SomeLocalizedString. .Net framework сам посмотрит, что за локализация у клиента и отдаст ему нужный перевод. Мы можем также разрешить пользователю выбирать язык и его выбор сохранить в куки, а потом проверять:

      
      protected override void InitializeCulture()
              {
                  
                  var culture = string.Empty;
                  if (Request.Cookies["language"] != null)
                      culture = Request.Cookies["language"].Value;
      
                  if (IsPostBack && Request.Params.Get("__EVENTTARGET") == "language")
                  {
                      if (Request.Params.Get("__EVENTARGUMENT") == "ru")
                          culture = "ru-RU";
                      if (Request.Params.Get("__EVENTARGUMENT") == "en")
                          culture = "en-US";
      
                      Response.Cookies.Set(new HttpCookie("language",culture){Expires = DateTime.Now.AddYears(1)});
                  }
      
                  if (!string.IsNullOrEmpty(culture))
                  {
                      Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(culture);
                      Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture);
                  }
                  base.InitializeCulture();
              }
      

    Only users with full accounts can post comments. Log in, please.