【发布时间】:2022-01-21 04:29:39
【问题描述】:
我有这个显示通用消息的组件:
<span>@message</span>
消息由id 标识,来自资源文件中的字符串表(多种语言)。消息的示例是:
"Hello {user}! Welcome to {site}!"
所以在基本情况下,我只需解析字符串并将{user} 替换为“John Doe”,将{site} 替换为“MySiteName”。结果设置为message,然后正确(且安全)地呈现。
但我实际上想做的是用我创建的 component 替换 {site},该组件显示具有特殊字体和样式的站点名称。我还有其他情况,我想用 components 替换特殊的 {markings}。
您将如何解决这个问题?有没有办法将组件“插入”到字符串中,然后“安全地”插入字符串以进行渲染?我说“安全”是因为最终字符串的某些部分可能来自数据库并且本质上是不安全的(例如用户名),因此插入带有 @((MarkupString)message) 之类的字符串似乎并不安全。
编辑: 感谢 MrC aka Shaun Curtis,这个最终解决方案受到了极大的启发。我将他的答案标记为最佳答案。
所以我最终选择了一个范围服务,它从资源文件中获取字符串,解析它们并返回它从组件的静态表中获取的 RenderFragments 列表。我使用动态对象在需要时将特定参数发送到 RenderFragments。
我现在基本上可以通过这种集中机制获取我的应用程序的所有文本。
以下是资源文件字符串表中的条目示例:
Name: "welcome"; Value: "Welcome to {site:name} {0}!"
这是它在组件中的使用方式:
<h3><Localizer Key="notif:welcome" Data="@(new List<string>() { NotifModel.UserNames.First })"/></h3>
您可以在下面看到简化的组件和服务代码。为简单起见,我明确省略了验证和错误检查代码。
@using MySite.Client.Services.Localizer
@inject ILocalizerService Loc
@foreach (var fragment in _fragments)
{
@fragment.Renderer(fragment.Item)
}
@code
{
private List<ILocalizerService.Fragment> _fragments;
public enum RendererTypes
{
Default,
SiteName,
SiteLink,
}
public static Dictionary<RendererTypes, RenderFragment<dynamic>> Renderers = new Dictionary<RendererTypes, RenderFragment<dynamic>>()
{
// NOTE: For each of the following items, do NOT insert a space between the end of the markup and the closing curly brace otherwise it will be rendered !!!
// Like here ↓↓
{ RendererTypes.Default, (model) => @<span>@(model as string)</span>},
{ RendererTypes.SiteName, (model) => @<MySiteNameComponent />},
{ RendererTypes.SiteLink, (model) => @<a href="@model.LinkUrl">@model.LinkTxt</a>}
};
[Parameter]
public string Key { get; set; }
[Parameter]
public List<string> Data { get; set; }
protected override void OnParametersSet()
{
_fragments = Loc.GetFragments(Key, Data);
}
}
interface ILocalizerService
{
public struct Fragment
{
public Fragment(RenderFragment<dynamic> renderer)
: this(renderer, default)
{
}
public Fragment(RenderFragment<dynamic> renderer, dynamic item)
{
Renderer = renderer;
Item = item;
}
public RenderFragment<dynamic> Renderer { get; set; }
public dynamic Item { get; set; }
}
List<Fragment> GetFragments(string key, List<string> parameters);
}
internal sealed class LocalizerService : ILocalizerService
{
private readonly Dictionary<string, IStringLocalizer> _strLoc = new Dictionary<string, IStringLocalizer>();
public LocalizerService(IStringLocalizer<MySite.Shared.Resources.App> appLoc,
IStringLocalizer<MySite.Shared.Resources.Connection> connLoc,
IStringLocalizer<MySite.Shared.Resources.Notifications> notifLoc)
{
// Keep string localizers
_strLoc.Add("app", appLoc);
_strLoc.Add("conn", connLoc);
_strLoc.Add("notif", notifLoc);
}
public List<Fragment> GetFragments(string key, List<string> parameters)
{
var list = new List<Fragment>();
GetFragments(list, key, parameters);
return list;
}
private void GetFragments(List<Fragment> list, string key, List<string> parameters)
{
// First, get key tokens
var tokens = key.Split(':');
// Analyze first token
switch (tokens[0])
{
case "site":
// Format : {site:...}
ProcessSite(list, tokens, parameters);
break;
default:
// Format : {0|1|2|...}
if (uint.TryParse(tokens[0], out var paramIndex))
{
ProcessParam(list, paramIndex, parameters);
}
// Format : {app|conn|notif|...}
else if (_strLoc.ContainsKey(tokens[0]))
{
ProcessStringLocalizer(list, tokens, parameters);
}
break;
}
}
private void ProcessSite(List<Fragment> list, string[] tokens, List<string> parameters)
{
// Analyze second token
switch (tokens[1])
{
case "name":
// Format {site:name}
// Add name component
list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.SiteName]));
break;
case "link":
// Format {site:link:...}
ProcessLink(list, tokens, parameters);
break;
}
}
private void ProcessLink(List<Fragment> list, string[] tokens, List<string> parameters)
{
// Analyze third token
switch (tokens[2])
{
case "user":
// Format: {site:link:user:...}
ProcessLinkUser(list, tokens, parameters);
break;
}
}
private void ProcessLinkUser(List<Fragment> list, string[] tokens, List<string> parameters)
{
// Check length
var length = tokens.Length;
if (length >= 4)
{
string linkUrl;
string linkTxt;
// URL
// Format: {site:link:user:0|1|2|...}
// Retrieve handle from param
if (!uint.TryParse(tokens[3], out var paramIndex))
{
throw new ApplicationException("Invalid token!");
}
var userHandle = GetParam(paramIndex, parameters);
linkUrl = $"/user/{userHandle}";
// Text
if (length >= 5)
{
if (tokens[4].Equals("t"))
{
// Format: {site:link:user:0|1|2|...:t}
// Use token directly as text
linkTxt = tokens[4];
}
else if (uint.TryParse(tokens[4], out paramIndex))
{
// Format: {site:link:user:0|1|2|...:0|1|2|...}
// Use specified param as text
linkTxt = GetParam(paramIndex, parameters);
}
}
else
{
// Format: {site:link:user:0|1|2|...}
// Use handle as text
linkTxt = userHandle;
}
// Add link component
list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.SiteLink], new { LinkUrl = linkUrl, LinkTxt = linkTxt }));
}
}
private void ProcessParam(List<Fragment> list, uint paramIndex, List<string> parameters)
{
// Add text component
list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.Default], GetParam(paramIndex, parameters)));
}
private string GetParam(uint paramIndex, List<string> parameters)
{
// Proceed
if (paramIndex < parameters.Length)
{
return parameters[paramIndex];
}
}
private void ProcessStringLocalizer(List<Fragment> list, string[] tokens, List<string> parameters)
{
// Format {loc:str}
// Retrieve string localizer
var strLoc = _strLoc[tokens[0]];
// Retrieve string
var str = strLoc[tokens[1]].Value;
// Split the string in parts to see if it needs formatting
// NOTE: str is in the form "...xxx {key0} yyy {key1} zzz...".
// This means that once split, the keys are always at odd indexes (even if {key} starts or ends the string)
var strParts = str.Split('{', '}');
for (int i = 0; i < strParts.Length; i += 2)
{
// Get parts
var evenPart = strParts[i];
var oddPart = ((i + 1) < strParts.Length) ? strParts[i + 1] : null;
// Even parts are always regular text. If not null or empty, we add directly
if (!string.IsNullOrEmpty(evenPart))
{
list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.Default], evenPart));
}
// Odd parts are always keys. If not null or empty, get fragments recursively
if (!string.IsNullOrEmpty(oddPart))
{
GetFragments(list, oddPart, parameters);
}
}
}
}
【问题讨论】:
-
在我整理答案之前检查一下:“用组件替换 {site}”中的组件是 Blazor 组件吗?
-
“安全”问题建议您将 HTML 存储在后端并进行渲染。请详细说明输入/输出的范围。
-
@MrCakaShaunCurtis 是的,blazor 组件。例如
-
@HenkHolterman 不,我没有在后端存储 HTML。只需用户提供字符串,例如他们的名字。我提到安全性只是因为我担心如果我的问题的解决方案意味着使用“MarkupString”(或类似的东西),那么用户提供的字符串最终可能会在未经验证的情况下被渲染。例如,如果用户说他们的名字是“”,只是想确保可能的解决方案考虑到我显然不希望它成为实际的 HTML 的事实!
-
好的,那只是显示文本。例如“用户”字段应该有多“可配置”?
标签: localization blazor web-component webassembly