You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor本地化:实现IStringLocalizer以Azure Blob替代.resx文件

实现基于Azure Blob的IStringLocalizer替代Resx文件

整体思路

通过实现ASP.NET Core的IStringLocalizer和IStringLocalizerFactory接口,将Azure Blob存储的JSON格式键值对作为本地化数据源,替代传统的.resx文件。


步骤1:定义Blob存储结构

在Azure Blob容器中创建本地化资源目录(比如localization-resources/),每个语言对应一个JSON文件,命名规则为{资源组}.{文化代码}.json:

  • 示例文件:resources.en-US.json、resources.zh-CN.json、Home.zh-TW.json
  • JSON格式示例(resources.zh-CN.json):
{
    "Welcome": "欢迎",
    "HelloWorld": "你好,世界!",
    "Greeting": "你好,{0}!"
}

步骤2:实现IStringLocalizer

创建BlobStringLocalizer类,实现IStringLocalizer接口,负责从Blob加载翻译内容:

using Azure.Storage.Blobs;
using Microsoft.Extensions.Localization;
using System.Globalization;
using System.Text.Json;

public class BlobStringLocalizer : IStringLocalizer
{
    private readonly BlobContainerClient _containerClient;
    private readonly CultureInfo _culture;
    private readonly string _resourceBaseName;
    private Dictionary<string, string> _translations = new();

    public BlobStringLocalizer(BlobContainerClient containerClient, CultureInfo culture, string resourceBaseName = "resources")
    {
        _containerClient = containerClient;
        _culture = culture;
        _resourceBaseName = resourceBaseName;
        _ = LoadTranslationsAsync(); // 异步加载,避免阻塞构造函数
    }

    private async Task LoadTranslationsAsync()
    {
        // 优先加载当前文化的资源文件
        var blobName = $"localization-resources/{_resourceBaseName}.{_culture.Name}.json";
        var blobClient = _containerClient.GetBlobClient(blobName);

        if (await blobClient.ExistsAsync())
        {
            using var stream = await blobClient.OpenReadAsync();
            _translations = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream) ?? new();
            return;
        }

        // 当前文化不存在时,回退到父文化(比如zh-CN回退到zh)
        if (!string.IsNullOrEmpty(_culture.Parent.Name))
        {
            var parentBlob = _containerClient.GetBlobClient($"localization-resources/{_resourceBaseName}.{_culture.Parent.Name}.json");
            if (await parentBlob.ExistsAsync())
            {
                using var stream = await parentBlob.OpenReadAsync();
                _translations = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream) ?? new();
                return;
            }
        }

        // 最后回退到默认文化(en-US)
        var fallbackBlob = _containerClient.GetBlobClient($"localization-resources/{_resourceBaseName}.en-US.json");
        if (await fallbackBlob.ExistsAsync())
        {
            using var stream = await fallbackBlob.OpenReadAsync();
            _translations = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream) ?? new();
        }
    }

    public LocalizedString this[string name]
    {
        get
        {
            var exists = _translations.TryGetValue(name, out var value);
            return new LocalizedString(name, exists ? value : name, !exists);
        }
    }

    public LocalizedString this[string name, params object[] arguments]
    {
        get
        {
            var baseString = this[name];
            return new LocalizedString(name, string.Format(baseString.Value, arguments), baseString.ResourceNotFound);
        }
    }

    public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
    {
        foreach (var kvp in _translations)
        {
            yield return new LocalizedString(kvp.Key, kvp.Value, false);
        }

        // 若需要包含父文化资源,递归加载父文化的所有字符串
        if (includeParentCultures && !string.IsNullOrEmpty(_culture.Parent.Name))
        {
            var parentLocalizer = new BlobStringLocalizer(_containerClient, _culture.Parent, _resourceBaseName);
            foreach (var str in parentLocalizer.GetAllStrings(false))
            {
                if (!_translations.ContainsKey(str.Name))
                {
                    yield return str;
                }
            }
        }
    }

    public IStringLocalizer WithCulture(CultureInfo culture)
    {
        return new BlobStringLocalizer(_containerClient, culture, _resourceBaseName);
    }
}

步骤3:实现IStringLocalizerFactory

创建工厂类,负责根据请求的资源组和文化创建对应的BlobStringLocalizer实例:

using Microsoft.Extensions.Localization;
using System.Globalization;
using Azure.Storage.Blobs;

public class BlobStringLocalizerFactory : IStringLocalizerFactory
{
    private readonly BlobContainerClient _containerClient;

    public BlobStringLocalizerFactory(BlobContainerClient containerClient)
    {
        _containerClient = containerClient;
    }

    public IStringLocalizer Create(Type resourceSource)
    {
        // 以类名作为资源组名称(比如HomeController对应Home资源文件)
        var resourceBaseName = resourceSource.Name.Replace("Controller", "");
        return new BlobStringLocalizer(_containerClient, CultureInfo.CurrentUICulture, resourceBaseName);
    }

    public IStringLocalizer Create(string baseName, string location)
    {
        // 直接使用传入的baseName作为资源组名称
        return new BlobStringLocalizer(_containerClient, CultureInfo.CurrentUICulture, baseName);
    }
}

步骤4:注册服务到ASP.NET Core

在Program.cs中配置Azure Blob服务和本地化服务:

using Azure.Storage.Blobs;
using Microsoft.AspNetCore.Localization;
using System.Globalization;

var builder = WebApplication.CreateBuilder(args);

// 注册Azure Blob容器客户端
builder.Services.AddSingleton(sp =>
{
    var connectionString = builder.Configuration.GetConnectionString("AzureBlobStorage");
    // 替换为你的Blob容器名称
    return new BlobContainerClient(connectionString, "localization-container");
});

// 替换默认的本地化器工厂
builder.Services.AddSingleton<IStringLocalizerFactory, BlobStringLocalizerFactory>();

// 配置本地化支持(无需指定ResourcesPath,因为不用resx)
builder.Services.AddLocalization();

// 添加MVC并启用视图和数据注解本地化
builder.Services.AddControllersWithViews()
    .AddViewLocalization()
    .AddDataAnnotationsLocalization(options =>
    {
        options.DataAnnotationLocalizerProvider = (type, factory) =>
            factory.Create(type);
    });

// 配置请求本地化规则
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    var supportedCultures = new[]
    {
        new CultureInfo("en-US"),
        new CultureInfo("zh-CN"),
        new CultureInfo("zh-TW")
    };

    options.DefaultRequestCulture = new RequestCulture("en-US");
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;
});

var app = builder.Build();

// 启用请求本地化中间件(必须在路由之前)
app.UseRequestLocalization();

// 其他中间件配置
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

步骤5:在代码中使用本地化器

和使用默认.resx本地化的方式完全一致,比如在控制器中注入:

public class HomeController : Controller
{
    private readonly IStringLocalizer<HomeController> _localizer;

    public HomeController(IStringLocalizer<HomeController> localizer)
    {
        _localizer = localizer;
    }

    public IActionResult Index()
    {
        ViewData["WelcomeMessage"] = _localizer["Welcome"];
        ViewData["Greeting"] = _localizer["Greeting", "张三"];
        return View();
    }
}

优化建议

  1. 添加缓存:避免每次请求都加载Blob文件,可使用IMemoryCache或IDistributedCache缓存已加载的翻译内容,缓存键可以是{资源组}-{文化代码}。
  2. 异步初始化改进:可以使用IAsyncDisposable或自定义异步初始化逻辑,确保翻译内容加载完成后再处理请求。
  3. 资源热更新:监听Blob文件的变化,自动刷新缓存中的翻译内容,无需重启应用。
  4. 权限控制:如果Blob容器是私有的,建议使用Azure Managed Identity替代连接字符串,提升安全性。

内容的提问来源于stack exchange,提问作者Kasun Jalitha

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 20:24:33