Подтвердить что ты не робот

Лучший способ реализовать многоязычность/глобализацию в крупном проекте .NET

В скором времени я буду работать над большим проектом С# и хотел бы начать с многоязычной поддержки с самого начала. У меня была игра, и я могу заставить ее работать, используя отдельный файл ресурсов для каждого языка, а затем использовать диспетчер ресурсов для загрузки строк.

Есть ли другие хорошие подходы, которые я мог бы изучить?

4b9b3361

Ответ 1

Используйте отдельный проект с ресурсами

Я могу сказать это по собственному опыту, имея текущее решение с 12 24 проектами, которое включает API, MVC, Библиотеки проектов (основные функции), WPF, UWP и Xamarin. Стоит прочитать этот длинный пост, так как я думаю, что это лучший способ сделать это. С помощью инструментов VS их легко экспортировать и импортировать для отправки в бюро переводов или просмотра другими людьми.

ОБНОВЛЕНИЕ 02/2018: По-прежнему сильны, преобразование его в стандартную библиотеку .NET позволяет даже использовать его в .NET Framework и NET Core. Я добавил дополнительный раздел для преобразования его в JSON, чтобы, например, Angular мог его использовать.

ОБНОВЛЕНИЕ 2019: В дальнейшем с Xamarin, это все еще работает на всех платформах. Например. Xamarin.Forms также рекомендует использовать файлы resx. (Я еще не разрабатывал приложение в Xamarin.Forms, но документация, которая является подробным, чтобы только начать, охватывает его: Документация Xamarin.Forms). Точно так же, как преобразование его в JSON, мы также можем преобразовать его в XML файл для Xamarin.Android.

ОБНОВЛЕНИЕ 2019 (2): При обновлении до UWP из WPF я обнаружил, что в UWP они предпочитают использовать другой тип файла .resw, который с точки зрения содержания идентичен, но использование другое. Я нашел другой способ сделать это, который, на мой взгляд, работает лучше, чем стандартное решение.

Итак, давайте перейдем к этому.

Pro

  • Сильно набран почти везде.
  • В WPF вам не нужно иметь дело с ResourceDirectories.
  • Насколько я тестировал, поддерживается для ASP.NET, библиотек классов, WPF, Xamarin,.NET Core,.NET Standard.
  • Никаких дополнительных сторонних библиотек не требуется.
  • Поддерживает отступление культуры: en-US → en.
  • Не только бэкэнд, работает также в XAML для WPF и Xamarin.Forms, в .cshtml для MVC.
  • Легко манипулируйте языком, изменяя Thread.CurrentThread.CurrentCulture
  • Поисковые системы могут сканировать на разных языках, а пользователь может отправлять или сохранять URL-адреса для конкретных языков.

Con

  • WPF XAML иногда глючит, недавно добавленные строки не отображаются напрямую. Перестройка - это временное исправление (vs2015).
  • UWP XAML не отображает предложения Intellisense и не отображает текст во время разработки.
  • Скажи мне.

Настройка

Создайте языковой проект в своем решении, присвойте ему имя, например MyProject.Language. Добавьте в него папку с именем Resources и в этой папке создайте два файла Resources (.resx). Один называется Resources.resx, а другой - Resources.en.resx (или .en-GB.resx для конкретных целей). В моей реализации я использую язык NL (голландский) в качестве языка по умолчанию, так что это входит в мой первый файл, а английский - во второй файл.

Настройка должна выглядеть следующим образом:

language setup project

Свойства для Resources.resx должны быть: properties

Убедитесь, что для пространства имен настраиваемого инструмента задано пространство имен вашего проекта. Причина этого в том, что в WPF нельзя ссылаться на Resources внутри XAML.

А внутри файла ресурса установите для модификатора доступа значение Public:

access modifier

Использование в другом проекте

Ссылка на ваш проект: Щелкните правой кнопкой мыши на References → Add Reference → Prjects\Solutions.

Использовать пространство имен в файле: using MyProject.Language;

Используйте это так в back-end: string someText = Resources.orderGeneralError;   Если есть что-то еще, называемое "Ресурсы", просто поместите все пространство имен.

Использование в MVC

В MVC вы можете делать все, что хотите, чтобы установить язык, но я использовал параметризованные URL, которые можно настроить следующим образом:

RouteConfig.cs Ниже других отображений

routes.MapRoute(
    name: "Locolized",
    url: "{lang}/{controller}/{action}/{id}",
    constraints: new { lang = @"(\w{2})|(\w{2}-\w{2})" },   // en or en-US
    defaults: new { controller = "shop", action = "index", id = UrlParameter.Optional }
);

FilterConfig.cs (возможно, потребуется добавить, если так, добавьте FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); к методу Application_start() в Global.asax

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new ErrorHandler.AiHandleErrorAttribute());
        //filters.Add(new HandleErrorAttribute());
        filters.Add(new LocalizationAttribute("nl-NL"), 0);
    }
}

LocalizationAttribute

public class LocalizationAttribute : ActionFilterAttribute
{
    private string _DefaultLanguage = "nl-NL";
    private string[] allowedLanguages = { "nl", "en" };

    public LocalizationAttribute(string defaultLanguage)
    {
        _DefaultLanguage = defaultLanguage;
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        string lang = (string) filterContext.RouteData.Values["lang"] ?? _DefaultLanguage;
        LanguageHelper.SetLanguage(lang);
    }
}

LanguageHelper просто устанавливает информацию о культуре.

//fixed number and date format for now, this can be improved.
public static void SetLanguage(LanguageEnum language)
{
    string lang = "";
    switch (language)
    {
        case LanguageEnum.NL:
            lang = "nl-NL";
            break;
        case LanguageEnum.EN:
            lang = "en-GB";
            break;
        case LanguageEnum.DE:
            lang = "de-DE";
            break;
    }
    try
    {
        NumberFormatInfo numberInfo = CultureInfo.CreateSpecificCulture("nl-NL").NumberFormat;
        CultureInfo info = new CultureInfo(lang);
        info.NumberFormat = numberInfo;
        //later, we will if-else the language here
        info.DateTimeFormat.DateSeparator = "/";
        info.DateTimeFormat.ShortDatePattern = "dd/MM/yyyy";
        Thread.CurrentThread.CurrentUICulture = info;
        Thread.CurrentThread.CurrentCulture = info;
    }
    catch (Exception)
    {

    }
}

Использование в .cshtml

@using MyProject.Language;
<h3>@Resources.w_home_header</h3>

или если вы не хотите определять использование, просто заполните все пространство имен ИЛИ вы можете определить пространство имен в /Views/web.config:

<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="System.Web.Mvc.WebViewPage">
  <namespaces>
    ...
    <add namespace="MyProject.Language" />
  </namespaces>
</pages>
</system.web.webPages.razor>

Это учебное пособие по реализации mvc: Потрясающий учебный блог

Использование в библиотеках классов для моделей

Внутреннее использование - то же самое, но только пример для использования в атрибутах

using MyProject.Language;
namespace MyProject.Core.Models
{
    public class RegisterViewModel
    {
        [Required(ErrorMessageResourceName = "accountEmailRequired", ErrorMessageResourceType = typeof(Resources))]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; }
    }
}

Если у вас есть Reshaper, он автоматически проверит, существует ли данное имя ресурса. Если вы предпочитаете безопасность типов, вы можете использовать шаблоны T4 для создания перечисления

Использование в WPF.

Конечно, добавьте ссылку на ваше пространство имен MyProject.Language, мы знаем, как ее использовать в бэк-энде.

В XAML, внутри заголовка Window или UserControl, добавьте ссылку на пространство имен с именем lang следующим образом:

<UserControl x:Class="Babywatcher.App.Windows.Views.LoginView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:MyProject.App.Windows.Views"
              xmlns:lang="clr-namespace:MyProject.Language;assembly=MyProject.Language" <!--this one-->
             mc:Ignorable="d" 
            d:DesignHeight="210" d:DesignWidth="300">

Затем внутри ярлыка:

    <Label x:Name="lblHeader" Content="{x:Static lang:Resources.w_home_header}" TextBlock.FontSize="20" HorizontalAlignment="Center"/>

Поскольку он строго типизирован, вы уверены, что строка ресурса существует. Возможно, вам придется иногда перекомпилировать проект во время установки, WPF иногда глючит с новыми пространствами имен.

Еще одна вещь для WPF, установите язык внутри App.xaml.cs. Вы можете сделать свою собственную реализацию (выбрать во время установки) или позволить системе принять решение.

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        SetLanguageDictionary();
    }

    private void SetLanguageDictionary()
    {
        switch (Thread.CurrentThread.CurrentCulture.ToString())
        {
            case "nl-NL":
                MyProject.Language.Resources.Culture = new System.Globalization.CultureInfo("nl-NL");
                break;
            case "en-GB":
                MyProject.Language.Resources.Culture = new System.Globalization.CultureInfo("en-GB");
                break;
            default://default english because there can be so many different system language, we rather fallback on english in this case.
                MyProject.Language.Resources.Culture = new System.Globalization.CultureInfo("en-GB");
                break;
        }

    }
}

Использование в UWP

В UWP Microsoft использует это решение, что означает, что вам нужно будет создавать новые файлы ресурсов. Кроме того, вы также не можете повторно использовать текст, потому что они хотят, чтобы вы установили x:Uid вашего элемента управления в XAML как ключ ваших ресурсов. И в ваших ресурсах вы должны сделать Example.Text, чтобы заполнить текст TextBlock. Мне совсем не понравилось это решение, потому что я хочу повторно использовать свои файлы ресурсов. В конце концов я пришел к следующему решению. Я только что узнал об этом сегодня (2019-09-26), поэтому я мог бы вернуться с чем-то еще, если окажется, что это не работает должным образом.

Добавьте это в свой проект:

using Windows.UI.Xaml.Resources;

public class MyXamlResourceLoader : CustomXamlResourceLoader
{
    protected override object GetResource(string resourceId, string objectType, string propertyName, string propertyType)
    {
        return MyProject.Language.Resources.ResourceManager.GetString(resourceId);
    }
}

Добавьте это к App.xaml.cs в конструкторе:

CustomXamlResourceLoader.Current = new MyXamlResourceLoader();

В любом месте приложения вы можете изменить язык:

ApplicationLanguages.PrimaryLanguageOverride = "nl";
Frame.Navigate(this.GetType());

Последняя строка необходима для обновления пользовательского интерфейса. Пока я еще работаю над этим проектом, я заметил, что мне нужно сделать это 2 раза. Я мог бы в конечном итоге выбрать язык в первый раз, когда пользователь запускает. Но так как он будет распространяться через Windows Store, язык обычно равен системному языку.

Затем используйте в XAML:

<TextBlock Text="{CustomResource ExampleResourceKey}"></TextBlock>

Использование его в Angular (преобразовать в JSON)

В наши дни более распространено иметь фреймворк, такой как Angular, в сочетании с компонентами, то есть без cshtml. Переводы хранятся в файлах json, я не собираюсь рассказывать о том, как это работает, но если вы хотите преобразовать это в файл JSON, это довольно просто, я использую шаблонный скрипт T4, который преобразует файл ресурсов в файл json. Я рекомендую установить редактор T4, чтобы прочитать синтаксис и правильно его использовать, потому что вам нужно внести некоторые изменения.

Единственное, на что следует обратить внимание: невозможно сгенерировать данные, скопировать их, очистить данные и сгенерировать их для другого языка. Таким образом, вы должны скопировать приведенный ниже код столько раз, сколько у вас языков, и изменить запись перед тем, как "//выберите язык здесь". В настоящее время нет времени, чтобы исправить это, но, вероятно, будет обновляться позже (если интересно).

Путь: MyProject.Language/T4/CreateWebshopLocalizationEN.tt

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Windows.Forms" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Resources" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.ComponentModel.Design" #>
<#@ output extension=".json" #>
<#


var fileNameNl = "../Resources/Resources.resx";
var fileNameEn = "../Resources/Resources.en.resx";
var fileNameDe = "../Resources/Resources.de.resx";
var fileNameTr = "../Resources/Resources.tr.resx";

var fileResultName = "../T4/CreateWebshopLocalizationEN.json";//choose language here
var fileResultPath = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", fileResultName);
//var fileDestinationPath = "../../MyProject.Web/ClientApp/app/i18n/";

var fileNameDestNl = "nl.json";
var fileNameDestEn = "en.json";
var fileNameDestDe = "de.json";
var fileNameDestTr = "tr.json";

var pathBaseDestination = Directory.GetParent(Directory.GetParent(this.Host.ResolvePath("")).ToString()).ToString();

string[] fileNamesResx = new string[] {fileNameEn }; //choose language here
string[] fileNamesDest = new string[] {fileNameDestEn }; //choose language here

for(int x = 0; x < fileNamesResx.Length; x++)
{
    var currentFileNameResx = fileNamesResx[x];
    var currentFileNameDest = fileNamesDest[x];
    var currentPathResx = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", currentFileNameResx);
    var currentPathDest =pathBaseDestination + "/MyProject.Web/ClientApp/app/i18n/" + currentFileNameDest;
    using(var reader = new ResXResourceReader(currentPathResx))
    {
        reader.UseResXDataNodes = true;
#>
        {
<#
            foreach(DictionaryEntry entry in reader)
            {
                var name = entry.Key;
                var node = (ResXDataNode)entry.Value;
                var value = node.GetValue((ITypeResolutionService) null); 
                 if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\n", "");
                 if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\r", "");
#>
            "<#=name#>": "<#=value#>",
<#


            }
#>
        "WEBSHOP_LASTELEMENT": "just ignore this, for testing purpose"
        }
<#
    }
    File.Copy(fileResultPath, currentPathDest, true);
}


#>

Используйте в Xamarin.Android

Как объяснялось выше в обновлениях, я использую тот же метод, что и в Angular/JSON. Но Android использует файлы XML, поэтому я написал файл T4, который генерирует эти файлы XML.

Путь: MyProject.Language/T4/CreateAppLocalizationEN.tt

#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Windows.Forms" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Resources" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.ComponentModel.Design" #>
<#@ output extension=".xml" #>
<#
var fileName = "../Resources/Resources.en.resx";
var fileResultName = "../T4/CreateAppLocalizationEN.xml";
var fileResultRexPath = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", fileName);
var fileResultPath = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", fileResultName);

    var fileNameDest = "strings.xml";

    var pathBaseDestination = Directory.GetParent(Directory.GetParent(this.Host.ResolvePath("")).ToString()).ToString();

    var currentPathDest =pathBaseDestination + "/MyProject.App.AndroidApp/Resources/values-en/" + fileNameDest;

    using(var reader = new ResXResourceReader(fileResultRexPath))
    {
        reader.UseResXDataNodes = true;
        #>
        <resources>
        <#

                foreach(DictionaryEntry entry in reader)
                {
                    var name = entry.Key;
                    //if(!name.ToString().Contains("WEBSHOP_") && !name.ToString().Contains("DASHBOARD_"))//only include keys with these prefixes, or the country ones.
                    //{
                    //  if(name.ToString().Length != 2)
                    //  {
                    //      continue;
                    //  }
                    //}
                    var node = (ResXDataNode)entry.Value;
                    var value = node.GetValue((ITypeResolutionService) null); 
                     if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\n", "");
                     if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\r", "");
                     if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("&", "&amp;");
                     if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("<<", "");
                     //if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("'", "\'");
#>
              <string name="<#=name#>">"<#=value#>"</string>
<#      
                }
#>
            <string name="WEBSHOP_LASTELEMENT">just ignore this</string>
<#
        #>
        </resources>
        <#
        File.Copy(fileResultPath, currentPathDest, true);
    }

#>

Android работает с папками values-xx, поэтому выше для английского языка в папке values-en. Но вы также должны сгенерировать значение по умолчанию, которое помещается в папку values. Просто скопируйте шаблон T4 выше и измените папку в приведенном выше коде.

Теперь вы можете использовать один файл ресурсов для всех ваших проектов. Это позволяет очень легко экспортировать все в отдельный документ и позволить кому-то перевести его и импортировать снова.

Отдельное спасибо этому удивительному расширению VS, которое прекрасно работает с файлами resx. Подумайте о пожертвовании ему за его потрясающую работу (я не имею к этому никакого отношения, мне просто нравится расширение).

Ответ 2

Я видел проекты, реализованные с использованием разных подходов, каждый из которых имеет свои достоинства и недостатки.

  • Один сделал это в файле конфигурации (а не в моем любимом)
  • Один из них использовал базу данных - это работало очень хорошо, но было больно в том, что вы знаете, что нужно поддерживать.
  • Один из используемых файлов ресурсов так, как вы предлагаете, и я должен сказать, что это был мой любимый подход.
  • Самый простой из них - использование файла include, содержащего строки - уродливые.

Я бы сказал, что метод ресурсов, который вы выбрали, имеет большой смысл. Было бы интересно увидеть ответы других людей, так как я часто задаюсь вопросом, есть ли лучший способ делать такие вещи. Я видел множество ресурсов, которые указывают на метод использования ресурсов, включая один прямо здесь, на SO.

Ответ 3

Я не думаю, что есть "лучший способ". Это действительно будет зависеть от технологий и типа приложения, которое вы создаете.

Веб-приложения могут хранить информацию в базе данных, как предлагали другие авторы, но я рекомендую использовать отдельные файлы ресурсов. То есть файлы ресурсов отделены от вашего источника. Отдельные файлы ресурсов снижают конкуренцию за одни и те же файлы, и по мере роста вашего проекта вы можете обнаружить, что локализация будет осуществляться отдельно от бизнес-логики. (Программисты и переводчики).

Гуру Microsoft WinForm и WPF рекомендуют использовать отдельные сборки ресурсов, настроенные для каждой локали.

Способность WPF масштабировать элементы пользовательского интерфейса в соответствии с содержимым снижает требуемую работу над макетом, например: (японские слова намного короче английских).

Если вы рассматриваете WPF: я предлагаю прочитать эту статью о msdn. Чтобы быть правдивым, я нашел инструменты локализации WPF: msbuild, locbaml (и, возможно, таблицу Excel) утомительными, но они работают.

Кое-что только немного связанное: общая проблема, с которой я сталкиваюсь, - это интеграция устаревших систем, которые отправляют сообщения об ошибках (обычно на английском языке), а не коды ошибок. Это вынуждает либо вносить изменения в унаследованные системы, либо отображать строки бекенда в мои собственные коды ошибок, а затем в локализованные строки... yech. Коды ошибок являются локализацией друга

Ответ 4

+1 База данных

Формы в вашем приложении могут даже перевести себя на лету, если в базу данных будут внесены исправления.

Мы использовали систему, в которой все элементы управления были сопоставлены в XML файле (по одной форме) для идентификаторов языковых ресурсов, но все идентификаторы были в базе данных.

В принципе, вместо того, чтобы каждый элемент управления удерживал идентификатор (реализовывая интерфейс или используя свойство тега в VB6), мы использовали тот факт, что в .NET дерево управления легко обнаруживалось с помощью отражения. Процесс, когда загруженная форма создаст XML файл, если он отсутствует. Файл XML сопоставляет элементы управления с идентификаторами ресурсов, поэтому это просто необходимо заполнить и сопоставить с базой данных. Это означало, что не нужно было менять скомпилированный двоичный файл, если что-то не было помечено, или если его нужно разделить на другой идентификатор (некоторые слова на английском языке, которые могут использоваться как имена, так и глаголы, возможно, необходимо перевести на два разных слова в словаре и не будут повторно использоваться, но вы не сможете обнаружить это при первоначальном назначении идентификаторов). Но дело в том, что весь процесс перевода становится полностью независимым от вашего двоичного файла (каждая форма должна наследовать от базовой формы, которая знает, как перевести себя и все ее элементы управления).

Только те приложения, где приложение становится более привлекательным, - это когда используется фаза с точками вставки.

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

Ответ 5

Я бы пошел с несколькими файлами ресурсов. Это не так сложно настроить. На самом деле я недавно ответил на аналогичный вопрос о настройке файлов ресурсов на основе глобального языка в сочетании с файлами ресурсов языка языка.

Локализация в Visual Studio 2008

Я бы подумал, что лучший подход, по крайней мере, для разработки WinForm.

Ответ 6

Вы можете использовать коммерческие инструменты, такие как Sisulizer. Он создаст сборку спутников для каждого языка. Единственное, на что вы должны обратить внимание, это не запутывать имена классов форм (если вы используете обфускатор).

Ответ 7

Я искал, и я нашел это:

Если вы используете WPF или Silverlight, ваш aproach может использовать WPF LocalizationExtension по многим причинам.

Открытый исходный код IT Это бесплатно (и будет бесплатно) находится в реальном состоянии stabel

В приложении Windows вы можете сделать следующее:

public partial class App : Application  
{  
     public App()  
     {             
     }  

     protected override void OnStartup(StartupEventArgs e)  
     {  
         Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("de-DE"); ;  
         Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("de-DE"); ;  

          FrameworkElement.LanguageProperty.OverrideMetadata(  
              typeof(FrameworkElement),  
              new FrameworkPropertyMetadata(  
                  XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));  
          base.OnStartup(e);  
    }  
} 

И я думаю, что на Wep-странице aproach может быть тем же самым.

Удачи!

Ответ 8

В большинстве проектов с открытым исходным кодом для этой цели используйте GetText. Я не знаю, как и когда он когда-либо использовался в проекте .Net раньше.

Ответ 9

Стандартные файлы ресурсов проще. Однако, если у вас есть данные, зависящие от языка, такие как таблицы поиска, вам придется управлять двумя наборами ресурсов.

Я этого не делал, но в своем следующем проекте я бы использовал поставщика ресурсов базы данных. Я нашел, как это сделать в MSDN:

http://msdn.microsoft.com/en-us/library/aa905797.aspx

Я также нашел эту реализацию:

поставщик DBResource

Ответ 10

Мы используем настраиваемый поставщик для поддержки нескольких языков и помещаем все тексты в таблицу базы данных. Он работает хорошо, за исключением того, что мы иногда сталкиваемся с проблемами кеширования при обновлении текстов в базе данных без обновления веб-приложения.