如何维护 3 个网站之间的语言?

How can I maintain Language between 3 websites?

我设置了三个网站。我们称它们为用户门户、管理门户、登录门户。

管理员和用户门户都将使用登录门户进行身份验证,但是两者将提供不同的内容。我的问题是这样的。如果我在用户门户上并将语言从英语更改为西班牙语,那么当我访问管理门户或登录门户时,它应该以西班牙语显示所有内容。然后,如果我在登录门户上将我的语言切换为法语,那么管理门户和用户门户都应该显示法语。

基本上,无论我在哪个网站上切换语言,我都希望其他两个网站知道该更改并采取相应行动。现在我正在使用 .NET Core 并且我正在使用本机方式进行本地化。这意味着我已经设置了资源文件并使用 cookie 来存储当前语言。

我知道我不能编辑跨域 cookie,所以我有点不知道如何才能做到这一点。我想到的解决方案是,当你在一个网站上更改一种语言时,你对另外两个网站做一个 post 的表格来保持它们的更新,这感觉相当混乱。如果我还要添加第 4 个门户,情况也会变得更糟。

我想到的第二个解决方案是将值保存在数据库中,然后编写中间件来拦截每个请求并检查数据库并设置语言。这也感觉很不对劲,因为我在每次发送请求时都会向我的数据库添加更多流量。

有没有更好的方法可以做到这一点?

考虑到您将 3 个 Web 应用程序部署到不同的域名,我会建议第二种解决方案。你能做什么-

  1. 您可以使用带有内存缓存的中间件,例如aerospike/cassandra/mongodb 或者您可以使用托管 firebase
  2. 优化语言值读取调用。在浏览器上,当用户关注 window 或选项卡时触发语言读取调用。 Window focus() 事件。

为您的 cookie 创建一个通用的 url,这可以通过从共享 CDN 调用一些东西来完成,我使用一个私有 CDN 来共享 css、图像和其他资源。使用 URL 中的 cookie,所有网站都可以共享它。

你经常看到这种技术,它带有带图像的 cookie 的“赞”按钮。

我有一个 cdn.domain.Com 和 www.domain.com, mobile.domain.com 和 api.domain.com 后三个使用 cdn 以便它尽可能地从现金中提取并且还允许我从一个共同的来源更新所有网站。这不限于子域或子域,任何域都可以

有了语言标识符后,您就可以使用多种本地化策略之一加载静态文本 Microsoft shows how to do this here

在数据库中维护语言可能有点重,我喜欢ResXManager it allows you to maintain several languages at the same time as well as export & import that you can use to have native speakers fix the "google translations". You can use Excel,比数据库更容易共享…

希望这比下面的评论更容易。如果您需要数据库代码,请告诉我,我会在此处 post。

我提到了 "database" 解决方案的代码

首先,此方法使用我为此创建的 TagHelper

它是如何工作的, 第 1 步:站点使用标签助手呈现视图。 第 2 步:标签助手被中间件注意到并被执行。标签助手然后进入注入的存储库并获得正确的内部 html。 对我来说 html 是。

<h5 language-key="CTrader-C1-H5">The C Trading with the C1 Algorithm</h5>
<p language-key="CTrader-C1">
              The C1 Trader trades against the trend and allows you to..
</p>

第 3 步:根据用户角色我加载 JavaScript 允许在线编辑页面文本的文件

第 4 步:更新模板 _ViewImports.cshtml 以加载 TagHelpers,在我的例子中是:

@using CATS.Web.Shared.Repositories
@using CATS.Web.Shared.Infrastructure.TagHelpers
@using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.AspNetCore.Identity

@addTagHelper "*, CATS.Web.Shared"
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

标签助手定义如下:

[HtmlTargetElement("p",Attributes = CatsLanguageKey)]
[HtmlTargetElement("span", Attributes = CatsLanguageKey)]
[HtmlTargetElement("a", Attributes = CatsLanguageKey)]
[HtmlTargetElement("li", Attributes = CatsLanguageKey)]
[HtmlTargetElement("h1", Attributes = CatsLanguageKey)]
[HtmlTargetElement("h2", Attributes = CatsLanguageKey)]
[HtmlTargetElement("h3", Attributes = CatsLanguageKey)]
[HtmlTargetElement("h4", Attributes = CatsLanguageKey)]
[HtmlTargetElement("div", Attributes = CatsLanguageKey)]
public class LanguageTagHelper: TagHelper
{
    private const string CatsLanguageKey= "language-key";

    private readonly ILanguageRepository _repository;
    private readonly ClaimsPrincipal _user;
    private readonly IMemoryCache _memoryCache;

    public LanguageTagHelper(ILanguageRepository repository, IHttpContextAccessor context, IMemoryCache memoryCache)
    {
        _repository = repository;
        _user = context.HttpContext.User;
        _memoryCache = memoryCache;
    }

    [HtmlAttributeName(CatsLanguageKey)]
    public string Key { get; set; }



    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {

        var childContent = await output.GetChildContentAsync();
        if (!childContent.IsEmptyOrWhiteSpace)
        {
            var textItem = _repository.GetHtml(Key, childContent.GetContent().Trim());
            if (_user.Identity.IsAuthenticated && _user.IsInRole(MagicStrings.ROLE_TEXTER))
            {
                output.Attributes.Add("data-language-target", textItem.Language);
                output.Attributes.Add("data-language-key", textItem.Key);
                var html = new HtmlString(textItem.Text);
                output.Content.SetHtmlContent(html);

                _memoryCache.Remove(Key);
            }
            else
            {
                string text = string.Empty;
                if (!_memoryCache.TryGetValue(Key, out text))
                {
                    text = Regex.Replace(textItem.Text, @">\s+<", "><", RegexOptions.Compiled | RegexOptions.Multiline);
                    text = Regex.Replace(text, @"<!--(?!\s*(?:\[if [^\]]+]|<!|>))(?:(?!-->)(.|\n))*-->", "", RegexOptions.Compiled | RegexOptions.Multiline);
                    text = Regex.Replace(text, @"^\s+", "", RegexOptions.Compiled | RegexOptions.Multiline);
                    text = Regex.Replace(text, @"\r\n?|\n", "", RegexOptions.Compiled | RegexOptions.Multiline);
                    text = Regex.Replace(text, @"\s+", " ", RegexOptions.Compiled | RegexOptions.Multiline);
                    _memoryCache.Set(Key, text, new MemoryCacheEntryOptions() { Priority= CacheItemPriority.Low, SlidingExpiration= new TimeSpan(hours:1,minutes:0,seconds:0) });
                }
                var html = new HtmlString(text);
                output.Content.SetHtmlContent(html);

            }


        }
    }
}

在上面的代码中,我还确保我删除了兑现的文本,以防用户是文本发送者,否则他永远不会看到他更新的文本。

将打开创建和打开在线文本编辑器的 java 脚本如下所示。

var languageUrl="";
var languageResetUrl = "";


function ResetPageText(key){
//    var element = $("[data-language-key='" + key + "']");
//    var key = element.data("language-key");
    var args = {
        __RequestVerificationToken: gettoken(),
        textKey: key
    };
    $.post(languageResetUrl, args, function (data, textStatus, jqXHR) {
        location.reload();
    });


}
function SavePageText(key)
{
    var element = $("[data-language-key='" + key + "']");

    var lkey = element.data("language-key");
    var language = element.data("language-target");
    var text = $("#editor_" + lkey).val();


    var model = {
        __RequestVerificationToken: gt(),
        textKey: key,
        textLanguage: language,
        textValue: $.trim(text),
        salt: st()
    };

    if(text.length===0){
        alert('Text did not contain any characters, translation not saved');
        return;
    }

    $.ajax({
        url: languageUrl,
        method: 'POST',
        data: model,
        contentType: 'application/x-www-form-urlencoded',
        headers: { 'X-XSRF-TOKEN': model.__RequestVerificationToken, 'X-Cats-Salt': model.salt },
        success: function (data, textStatus, jqXHR) {
            var sender = $("[data-language-key='" + data.key + "']");
            sender.html(data.text);

            $("#dlg").fadeOut('slow', function () {
                $("#dlg").html("");
            });
        }
    });



}

function UpdatePreview(key)
{
    var preview = $("#2a" + key + "_pv");
    preview.html($("#editor_" + key).val());
}


function ShowEditor(element)
{
    var _key = $(element).data("language-key");
    var _language = $(element).data("language-target");
    var _value = $(element).html();
    var _data = { Key: _key, Language: _language, Value: _value };
    var form = [
        "<div class='modal-container' id='d" + _key + ">"
        , "  <div class='modal-dialog'>"
        , "     <div class='modal-content'>"
        , "         <div class='modal-header'>"
        , "             <button type='button' class='close' onclick='CancelEdit(\"" + _key + "\")'><span class='white'>&times;</span></button>"
        , "             <h4 class='modal-title'>Update text element \"" + _key+"\" in language \"" + _language + "\"</h4>"
        , "          </div >"
        , "          <div class='modal-body'>"
        , "          <div id='exTab1' >"
        , "             <ul class='nav nav-pills'>"
        , "                 <li class='active'><a href='#1a" + _key +"' data-toggle='tab'>HTML</a></li>"
        , "                 <li><a href='#2a" + _key +"' data-toggle='tab' onclick='UpdatePreview(\""+_key+"\")'>Preview</a></li>"
        ,"              <ul>"
        , "             <div class='tab-content clearfix'>"
        , "                 <div class='tab-pane active' id='1a" + _key+"'>"
        , "                     <textarea class='editBox pad5' name='textValue' id='editor_" + _key + "' spellcheck='true' lang='" + _language + "'>" + _value + "</textarea >"       
        , "                 </div>"
        , "                 <div class='tab-pane' id='2a" + _key + "'>"
        , "                     <div class='preview' id='2a" + _key + "_pv'>"
        , _value
        , "                     </div>"
        , "                 </div>"
        , "             </div>"
        , "         </div>"    
        , "         <div class='modal-footer'>"
        , "             <button type='button'  class='btn btn-default glyphicon glyphicon-repeat pad5' title='reset text to original' onclick='ResetPageText(\"" + _key + "\")'>Reset</button> "
        , "             <button type='button'  class='btn btn-default glyphicon glyphicon-ok pad5' title='Save changes' onclick='SavePageText(\"" + _key + "\")'>Save</button>"

        , "         </div>"  
        , "     </div>"    
        , "  </div>"
        ,"</div>"
    ].join("\n");


    $("#dlg").html(form);
    $("#dlg").fadeIn();
}


function CancelEdit(id)
{
    $("#dlg").fadeOut(400, 'swing', function () {
        $("#dlg").html("");
    });

}

Element.prototype.remove = function () {
    this.parentElement.removeChild(this);
};

NodeList.prototype.remove = HTMLCollection.prototype.remove = function () {
    for (var i = this.length - 1; i >= 0; i--) {
        if (this[i] && this[i].parentElement) {
            this[i].parentElement.removeChild(this[i]);
        }
    }
};


$(document).ready(function () {

    $('[data-language-key]')
        .on("click",
        function (){
            $(this).on("dblclick", ShowEditor(this));
    });

});

我通过将 "Salt" 添加到我的 header 来确保 JavaScript 的安全,确保只有具有正确 salt 和正确 IP 的正确用户才能更新站点的文本以避免“第三派对”更新 ;-)

我只像这样在我的 _layour.cshtml 共享页面中加载脚本,mahig 字符串只是一个 class 带有魔法字符串,就像我曾经在用户处于特定角色,因此他必须在激活之前登录。

<Roles app-role="@MagicStrings.ROLE_TEXTER">

    <script src="~/js/Translate.js" type="text/javascript"></script>
    <script>
        languageUrl      = '@Url.Action(action: "Update", controller: "PageText")';
        languageResetUrl = '@Url.Action(action: "Reset", controller: "PageText")';
        currentUrl       = '@Context.Request.Path';
    </script>

</Roles>

我的语言由语言库管理。代码为:

public class LanguageRepository : BaseRepository, ILanguageRepository
{

    private readonly IMemoryCache _memoryCache;
    private readonly IHttpContextAccessor _context;


    public LanguageRepository(AppDbContext _db, IHttpContextAccessor context
        , IMemoryCache memoryCache) 
        : base(_db)
    {
        _memoryCache = memoryCache;
        _context = context;
    }


    private string Url
    {
        get {
            return _context.HttpContext.Request.Path;
        }
    }


    /// <summary>
    /// Gets the language that the user is using.
    /// </summary>
    /// <value>
    /// The language.
    /// </value>
    private string Language
    {
        get {

            var code = System.Threading.Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName.ToLower();
            if (new[] { "en", "de", "fr", "nl" }.Contains(code))
                return code;
            else
                return "en";
        }
    }
    public TextItem GetHtml(string key, string defaultIfNull)
    {
        var cashKey = string.Concat("tc_",Language ,"_", key);
        if (!_memoryCache.TryGetValue<TextItem>(cashKey, out TextItem result))
        {
            result = new TextItem(key: key, language: Language);
            using (var cmd = db.Database.GetDbConnection().CreateCommand())
            {
                if (cmd.Connection.State != System.Data.ConnectionState.Open)
                    cmd.Connection.Open();
                cmd.CommandText = "dbo.GetPageText";
                cmd.CommandType = System.Data.CommandType.StoredProcedure;
                cmd.Parameters.Add(new SqlParameter("@Key", System.Data.SqlDbType.VarChar) { Value = key, Size = 150 });
                cmd.Parameters.Add(new SqlParameter("@Language", System.Data.SqlDbType.VarChar) { Value = Language, Size = 3 });
                cmd.Parameters.Add(new SqlParameter("@DefaultIfNull", System.Data.SqlDbType.NVarChar) { Value = defaultIfNull, Size = 4000 });
                cmd.Parameters.Add(new SqlParameter("@Url", System.Data.SqlDbType.VarChar) { Value = Url, Size = 150 });
                result.Text = cmd.ExecuteScalar().ToString();
            }

            _memoryCache.Set<TextItem>(cashKey, result, MemoryOptions);
        }
        return result;
    }



    public bool SetHtml(string key, string value)
    {
        var cashKey = string.Concat("tc_", Language, "_", key);
        TextItem result= new TextItem(key, Language) { Text= value };
        _memoryCache.Set<TextItem>(cashKey, result, MemoryOptions);

        using (var cmd = db.Database.GetDbConnection().CreateCommand())
        {
            if (cmd.Connection.State != System.Data.ConnectionState.Open)
                cmd.Connection.Open();

            cmd.CommandText = "dbo.SetPageText";
            cmd.CommandType = System.Data.CommandType.StoredProcedure;
            cmd.Parameters.Add(new SqlParameter("@Key", System.Data.SqlDbType.VarChar) { Value = key, Size = 150 });
            cmd.Parameters.Add(new SqlParameter("@Language", System.Data.SqlDbType.VarChar) { Value = Language, Size = 3 });
            cmd.Parameters.Add(new SqlParameter("@Value", System.Data.SqlDbType.NVarChar) { Value = value, Size = 4000 });
            return cmd.ExecuteNonQuery()!=0;
        }           

    }
    public async Task<bool> SetHtmlAsync(string key, string value)
    {
        var cashKey = string.Concat("tc_", Language, "_", key);
        TextItem result= new TextItem(key, Language) { Text= value };
        _memoryCache.Remove(cashKey);
        _memoryCache.Set<TextItem>(cashKey, result, MemoryOptions);

        using (var cmd = db.Database.GetDbConnection().CreateCommand())
        {
            if (cmd.Connection.State != System.Data.ConnectionState.Open)
                cmd.Connection.Open();

            cmd.CommandText = "dbo.SetPageText";
            cmd.CommandType = System.Data.CommandType.StoredProcedure;
            cmd.Parameters.Add(new SqlParameter("@Key", System.Data.SqlDbType.VarChar) { Value = key, Size = 150 });
            cmd.Parameters.Add(new SqlParameter("@Language", System.Data.SqlDbType.VarChar) { Value = Language, Size = 3 });
            cmd.Parameters.Add(new SqlParameter("@Value", System.Data.SqlDbType.NVarChar) { Value = value, Size = 4000 });
            return await cmd.ExecuteNonQueryAsync()!=0;
        }           

    }

    public async Task<bool> DeleteHtmlAsync(string key)
    {
        var cashKey = string.Concat("tc_", Language, "_", key);
        _memoryCache.Remove(cashKey);

        using (var cmd = db.Database.GetDbConnection().CreateCommand())
        {
            if (cmd.Connection.State != System.Data.ConnectionState.Open)
                cmd.Connection.Open();

            cmd.CommandText = "dbo.DeletePageText";
            cmd.CommandType = System.Data.CommandType.StoredProcedure;
            cmd.Parameters.Add(new SqlParameter("@Key", System.Data.SqlDbType.VarChar) { Value = key, Size = 150 });
            return await cmd.ExecuteNonQueryAsync() != 0;
        }

    }

    private MemoryCacheEntryOptions MemoryOptions=> new MemoryCacheEntryOptions() { Priority = CacheItemPriority.High, SlidingExpiration = DateTime.Now.AddHours(6) - DateTime.Now };

}

将用户的语言保存在数据库中。

这样一来,您只需在一个位置检查语言,而无需 cookie。无论您在哪个站点,后端只需要查看数据库、检查语言并执行所需操作(重新加载页面,运行 一些 javascript 将句子切换为新的语言,或任何最方便的语言)。

如果您希望在所有网站上实时进行语言转换,即如果您在一个 window 的管理门户中更改语言,并在另一个 window 中打开登录门户] 并希望语言在 两个 站点上更新,您需要在每个站点上使用 setInterval() 创建一个投票,调用 API 来检查用户的语言(存储在数据库中)并采取相应的行动。

如果所有三个门户都是同一基域下的子域,则使用 cookie。如果 cookie 值是为基域创建的,那么所有子域都可以访问它。例如,阅读:Share cookie between subdomain and domain

如果门户完全位于不同的域中,那么您需要使用 URL 或数据库方法。在我看来,方法因您可以对用例或技术执行的操作而有所不同。

我的第一选择是 URL - 使语言代码成为 URL 的一部分。例如,许多网站都这样做:www.domain.com/en-us/page.html。这个 URL 里面有语言和国家代码。在页面加载期间,这些代码将被接受并且页面呈现在所选的语言环境中。当您更改语言时,将用户重定向到具有正确语言环境的 URL。在门户之间切换时,将区域设置保持在 URL,假设所有门户都具有相似的 URL 结构。

我不太喜欢的方法是数据库。这种方法也有一些需要注意的注意事项。即,避免从外部门户读取包含语言选择的数据库。即,如果语言选择由用户门户管理,则管理门户将需要向用户门户询问此值,而不是直接读取数据库。我建议从架构的角度(微服务、限界上下文,随便你怎么说)考虑这种复杂性。因此,数据库方法涉及更多,因此由于这种复杂性而不太受欢迎。我想我不需要为数据库方法做更多的阐述,因为你明白了。

您的答案应该在您当前的设计中。

我假设在您的门户中有一个门户决定用户选择is/was什么语言?

考虑这种情况,如果用户访问了您 User-Portal 中的深层 link,您可能会将他转移到 Login-Portal,然后再返回到 User-Portal。在此转移过程中,您如何维护您的语言选择?


  1. 很可能是通过 cookie;因为您不希望他登录 - 这意味着您拥有全局 cookie,并且您的 sub-domain 应该能够 write/update 语言 cookie。

  2. 如果您使用的是 oAuth,则语言选择可以共享为查询字符串或 request-header 或令牌内的声明(很奇怪 - 但我见过这样的系统)

在上述两种情况下,您可能希望让您的 decision-making 门户了解已更改的 user-preference 以供将来使用,并继续将所选语言作为 global-cookie 或查询字符串或 request-header.

如果使用令牌,只需请求一个新令牌 - 需要一些编码。