5

我目前正在参与编写现有(Delphi)基于桌面的软件产品的 ASP.NET MVC 4 Web 版本(使用 Razor 视图引擎),目前允许客户(企业)完全自定义其实例中的所有文本应用程序,既可以对其进行本地化,也可以根据其特定环境对其进行自定义。

例如条款-

  • 我的任务
  • 产品
  • 工作流程
  • 设计

可能全部更改为业务中使用的个别术语。

目前,这种定制只是在存储在应用程序数据库中的文本字符串中完成,并在 Delphi 数据库中的每个表单加载时进行比较和加载。即,表单上的每个字符串都与数据库英语字符串进行比较,如果可用,则在表单上呈现基于所选语言环境的替换。我觉得这既不是可扩展的,也不是特别高效的。

我个人也不喜欢在本地化方法中进行自定义的想法,即最终客户可以更改应用程序中的每个字符串——这可能会导致文本一致性方面的支持问题,以及指令不正确的混乱更改或未更新。应用程序中有很多字符串,除了将它们本地化到用户的语言环境(本地语言和/或格式约定)之外,可能不应该更改这些字符串。

我个人宁愿坚持使用 ASP.NET API 和约定来本地化应用程序的 Web 版本,使用 RESX 资源文件和资源键而不是字符串匹配。这比字符串匹配灵活得多,其中字符串可能具有不同的上下文或大小写,并且不能简单地整体更改(有许多英语单词在不同的上下文中可能具有不同的含义,并且在其他情况下可能不会映射到相同的含义集语言),关键是避免往返数据库以获取获取页面所需的字符串,并且还允许使用围绕标准 RESX 文件的大量工具轻松翻译。这也意味着不需要为未来的开发人员维护或记录自定义实现。

然而,这确实给我们如何处理这些自定义条款带来了问题。

我目前认为我们应该为这些术语创建一个单独的 RESX 文件,其中列出了给定语言环境的默认值。然后我会创建一个新的数据库表,类似于

CREATE TABLE [dbo].[WEB_CUSTOM_TERMS] 
    (
        [TERM_ID] int identity primary key,
        [COMPANY_ID] int NOT NULL, -- Present for legacy reasons
        [LOCALE] varchar(8) NOT NULL,
        [TERM_KEY] varchar(40) NOT NULL,
        [TERM] nvarchar(50) -- Intentionally short, this is to be used for single words or short phrases
    );

这可能会在需要时读入 Dictionary<string, string> 并由 IIS 缓存以提供查找,而不会延迟连接到 SQL 服务器和执行查询。

public static class DatabaseTerms
{
    private static string DictionaryKey
    {
       get { return string.Format("CustomTermsDictionary-{0}", UserCulture); }
    }

    private static string UserCulture
    {
        get { return System.Threading.Thread.CurrentThread.CurrentCulture.Name; }
    }

    public static Dictionary<string, string> TermsDictionary
    {
        get
        {
            if (HttpContext.Current.Cache[DictionaryKey] != null)
            {
                var databaseTerms = HttpContext.Current.Cache[DictionaryKey] as Dictionary<string, string>;

                if (databaseTerms != null)
                {
                    return databaseTerms;
                }
            }

            var membershipProvider = Membership.Provider as CustomMembershipProvider;

            int? companyId = null;

            if (membershipProvider != null)
            {
                companyId = CustomMembershipProvider.CompanyId;
            }

            using (var context = new VisionEntities())
            {
                var databaseTerms = (from term in context.CustomTerms
                                     where (companyId == null || term.CompanyId == companyId) &&
                                     (term.Locale == UserCulture)
                                     orderby term.Key
                                     select term).ToDictionary(t => t.Key, t => t.Text);

                HttpContext.Current.Cache.Insert(DictionaryKey, databaseTerms, null, DateTime.MaxValue,
                    new TimeSpan(0, 30, 0), CacheItemPriority.BelowNormal, null);

                return databaseTerms;
            }
        }
        set
        {
            if (HttpContext.Current.Cache[DictionaryKey] != null)
            {
                HttpContext.Current.Cache.Remove(DictionaryKey);
            }
            HttpContext.Current.Cache.Insert(DictionaryKey, value, null, DateTime.Now.AddHours(8),
                  new TimeSpan(0, 30, 0), CacheItemPriority.BelowNormal, null);
        }
    }
}

然后我可以有一个公开公共属性的类,根据这个字典值RESX文件中的值返回一个字符串——以不为空的为准。就像是-

public static class CustomTerm
{
    public static string Product
    {
        get
        {
            return (DatabaseTerms.TermsDictionary.ContainsKey("Product") ?
                DatabaseTerms.TermsDictionary["Product"] : CustomTermsResources.Product);
        }
    }
}

如果需要,可以使用字符串格式将它们添加到更大的本地化字符串中,或​​者单独用作菜单的标签等。

这种方法的主要缺点是需要提前预测最终客户可能希望定制哪些条款,但我确实认为这可能是两全其美的。

这看起来是一种可行的方法吗?其他开发人员是如何解决这个问题的?

提前致谢。

4

4 回答 4

3

我曾经设计过一个 MVC 应用程序,可以更改任何字符串。在我的情况下,它是为了处理其他语言,但可以想象你可以仅仅为了审美目的而改变任何东西。并且该系统有可能被推销给其他商店,他们很可能将相同的东西称为不同的名称(您说“延期付款”,我说“租赁付款”等)

警告:这个解决方案与全球化和本地化无关(例如,从左到右、单词/动词排序——它只需要做它所做的事情!)

它还考虑了美式英语 (en-US) 与英式英语 (en-GB) 与澳大利亚英语 (en-AU) 的可能性。

最后,在数据库中创建了一个 Locale 表:

_id   _localeName   _idRoot
---------------------------
 1     en-GB        null
 2     en-US        1
 3     en-AU        2

请注意 US 和 AU 如何有效地将 en-GB 作为其父级。 因此,在我们的翻译表中, en-GB具有可以在应用程序中使用的所有可能的字符串:

_id   _idCulture   _from           _to
--------------------------------------
1      1           msgyes          Yes
2      1           msgno           No
3      1           msgcolour       Colour
4      2           msgcolour       Color

现在,在应用程序初始化期间,有一个指定文化的配置标志,在我的例子中恰好是 en-AU。系统查找文化树(en-AU 源自 en-GB),并将所有翻译自下而上加载到字典缓存中。因此,任何 en-AU 特定的翻译都会覆盖 GB 的翻译。

因此,在您的情况下进行描述-无论如何,您的数据库中都会有所有翻译,这是您的默认设置。当客户希望自定义文本时,他们基本上会获得一个新节点(或在我的示例中为派生文化),然后您再次构建缓存。他们自定义的任何术语都会覆盖默认值。您不再需要担心完成了哪些条款,它可以正常工作。

于 2013-07-10T15:42:23.003 回答
2

The requirement you have is not very common. I have worked in projects where localization is done purely using satellite assemblies and in projects where localization is done purely using database tables. In .NET, the recommended approach is RESX files compiled into satellite assemblies. It is a real good approach if you adopt it fully.

Your requirements are some where in between. Though the approach you plan to take, at this point sounds good on paper, I have a feeling over the course of time, maintenance will be difficult, since some of the strings will be in RESX and some will be in database. Even if the distribution is 90% - 10%, people will have difficulty figuring out where it takes the strings when you have all the strings loaded up in production. You will get queries from your users why a particular string is not showing up correctly and at that time it can get difficult for a developer (other than you) to figure out. You will be the best judge for your needs but I believe either you embrace RESX approach fully (which is not possible in your case) or go the full database route. If I have every thing in tables, all I need to do is to run a query for a given profile and I will see all the strings. This will be easier to support.

Even with database, you can follow a RESX-style approach of storing the full string against a key for a culture. The older approach of storing word by word is definitely not a good solution and will not work for different languages, since only sentences can be translated and not individual words. Your idea of caching is definitely needed for performance. So, basically having every thing in a bunch of tables, caching the same in memory and pulling the strings from cache based on the current culture is something I will go for. Of course, my opinion is based on what I could understand by reading your question :).

Also, check this out.

于 2013-07-14T08:21:31.513 回答
2

我们的应用程序中有类似的设置,我们允许某些模块具有自定义名称以适应客户品牌。

这个解决方案的第一步是我们在运行时了解我们的客户端上下文,并将其填充到 HttpContext.Items 中。

对于那些可以自定义的项目,我们引入了包含基本键的资源文件。如果企业想要定制,我们在密钥名称前添加一个前缀(即Client_key)

所有这一切都立即到位,它是一个简单的合并以获取自定义或默认值。

Resx 文件片段

<data name="TotalLeads" xml:space="preserve">
  <value>Total Leads</value>
</data>
<data name="Client_TotalLeads" xml:space="preserve">
  <value>Total Prospects</value>
</data>

处理自定义和基础资源之间切换的类

public static class CustomEnterpriseResource
{
    public static string GetString(string key)
    {
        return GetString(key, Thread.CurrentThread.CurrentUICulture);
    }

    public static string GetString(string key, string languageCode)
    {
        return GetString(key, new CultureInfo(languageCode));
    }

    public static string GetString(string key, CultureInfo cultureInfo)
    {
        var customKey = ((EnterpriseContext)HttpContext.Current.Items[EnterpriseContext.EnterpriseContextKey]).ResourcePrefix + key;

        return Resources.Enterprise.ResourceManager.GetString(customKey, cultureInfo) 
            ?? Resources.Enterprise.ResourceManager.GetString(key, cultureInfo);
    }
}

此外,为了协助查看我们为此创建了一个 html 助手。

public static class EnterpriseResourceHelper
{
    /// <summary>
    /// Gets a customizable resource
    /// </summary>
    /// <param name="helper">htmlHelper</param>
    /// <param name="key">Key of the resource</param>
    /// <returns>Either enterprise customized resource or base resource for current culture.</returns>
    public static string EnterpriseResource(this HtmlHelper helper, string key)
    {
        return CustomEnterpriseResource.GetString(key);
    }
}
于 2013-07-15T23:14:28.383 回答
0

非常有趣的问题,谢谢你提出来。

我以非常不同的方式本地化了应用程序,您的情况非常具体。让我们从一切都归结为本地化 UI 的标签/标题这一事实开始。因此,这些元素必须成为可本地化的。在许多平台(如 WinForms、ASP.NET)上,它们在设计上是可本地化的,所需要的只是扩展资源管理模型。我想说,如果您正在为这样的平台写作,这是最自然的本地化方式。

在 ASP.NET MVC 的情况下,即使它构建在 ASP.NET 引擎之上,我们也不建议使用 ASP.NET 的服务器端标签,因此该解决方案不起作用。为什么我在上面提供它是为了让我在下面描述的解决方案更加清晰。

第 1 步 - 模块化

所有标签和标题都是应用程序某些特定屏幕的一部分。由于屏幕是它们的组成部分,因此在描述本地化资源时,我经常将其用于此确切目的。顺便说一句,这就是为什么我们每个屏幕都有一个 resx 文件用于应用程序。因此,我们在这里遵循一致的标准。

为了表达模块化,定义与每个屏幕相对应的类,并在其上定义与屏幕上每个可本地化标签或标题相对应的属性。

伪示例:

class ProductPageResources
{
    public string PageTitle { get; set; }

    public string ProductNameLabel { get; set; }
}

第 2 步 - 本地化

在设计应用程序屏幕时,请坚持使用上面定义的模块化资源类。使用模块化资源类中的本地化字符串来显示标签和标题。如果需要在屏幕上添加标签,不要忘记也为模块化资源类添加新属性。由于它是 ASP.NET MVC,我们需要将资源与模型一起传递。从概念上讲,这不是必需的,但这样做可以让我们灵活地在未来替换资源实现(例如,从 MS SQL 到其他源)。

使用示例:

@{
    ViewBag.Title = string.format(Model.Resources.PageTitle, Model.Product.Name);
}
...
<label>@Model.Resources.ProductNameLabel</label>

请注意,资源类属性返回当前区域性的本地化字符串,或者如果未找到则返回后备值(如下所述)。为了使默认值显示为值,我通过迭代属性并在属性为空时为其分配默认值来准备资源对象(因为未找到覆盖)。

第 3 步 - 定制

[你在这里找到了非常好的描述性术语,所以我将使用它。]

我个人认为资源管理不应该是数据驱动的。主要原因是它不够动态。回想一下,我们有模块化的类,当我们需要在屏幕上显示新的东西时,我们就开始向它添加属性。另一方面,如果您向数据库中添加某些内容,它就不会出现在屏幕上。

因此,我们在这里有一个强类型的本地化,这是本地化事物的非常自然的方式。其余的来自这个结论。

在您的自定义/资源管理屏幕上,您可以使用反射来检测所有模块化资源类并在屏幕上显示它们的属性以进行自定义。要查找资源类,可以将它们放在同一个命名空间下,也可以用一些属性标记它们,以便在程序集中找到它们。无论哪种方式都有效。

为了使模块化资源类对显示更友好,您可以使用属性来分配应该在屏幕上显示的描述,而不是它们的 Pascal-Case 名称。

所以,我们的模块化资源类变成了这样:

[Description("Product Page")]
class ProductPageResources
{
    [Description("Page Title")]
    [DefaultValue("Product Details: {0}")
    public string PageTitle { get; set; }

    [Description("Product Name (label)")]
    [DefaultValue("Name:")]
    public string ProductNameLabel { get; set; }
}

基本上,在自定义屏幕上,我们将看到产品页面的默认值,以及每个可用的本地化值。对于最后一部分,您可以枚举应用程序的所有活动区域性并再次从属性中提取值。或者,您可以根据实现使用其他方式。

这似乎是一种广泛的反思,但毕竟 Visual Studio 做了一些非常相似的事情,它允许我们在特殊的编辑器中编辑资源文件。底线是你有一个精确的工作框架。

于 2013-07-16T16:41:28.757 回答