多语言支持
在之前的例子中,我们看到的应用界面,都是中文的。作为演示开发流程的角度,这是不错的。但如果你的应用不仅仅针对中文市场发布,或者你的用户群中会有外国人,那么就需要考虑多语言支持。由于这个功能是基础性的,所以我单独用一个小节来做说明。
多语言支持的方案
多语言支持包含了两大方面
- 应用定义的多语言支持。这将影响到用户看到的应用的基本信息,尤其是名称信息。这个是通过为manifest(清单文件)提供多语言来实现。
- 应用界面的多语言支持。例如用户打开的选项卡应用,或者机器人的消息等。这个是需要开发人员自己进行实现,不同的开发平台有不同的实现方案。
另外,在实现多语言支持的具体方案上,建议设置英文为默认语言,然后再根据具体需要支持的其他语言列表定义资源文件(例如简体中文),这样的话,除了已定义的语言之外,其他所有没有定义资源文件的语言,都将以英文来显示,这样可以比较省力气。
如何检测用户的语言
Teams客户端本身就支持多语言,以上提到的两个方面,”应用定义“ 会自动根据当前的客户端语言进行切换,然后 ”应用界面“ 的部分,我也是建议你同样进行处理。
这个语言一旦修改,是会重启客户端的,所以对于选项卡应用来说,只需要在初始化时读取该设置即可,不需要每次都获取。通过Teams JS SDK,在 getContext
的回调中,检测 locale
这个属性即可。
对于机器人应用,它其实是一个远程服务,不在本地运行。你可以通过 turnContext.Activity.Locale
这个属性来得到当前发送消息的用户所使用的语言。
> 请注意,机器人的场景比较复杂,在不同的事件中,检测Locale的方式可能不一样,有些时候并没有提供这个信息,请做必要的异常处理。
详情请参考下面的文档介绍
应用定义的多语言支持
我们是在App Studio中定义Teams应用的,根据上述的原则,我建议你默认用英文定义,然后根据需要添加其他语言。
假设你先定义了如下这样一个应用,它包含了一个”个人选项卡“,一个”机器人“。
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.9/MicrosoftTeams.schema.json",
"manifestVersion": "1.9",
"version": "1.0.0",
"id": "a1b624ef-80f6-4dbe-9dd0-e3cf83248e8f",
"packageName": "xyz.code365.teams",
"developer": {
"name": "https://code365.xyz",
"websiteUrl": "https://code365.xyz",
"privacyUrl": "https://code365.xyz",
"termsOfUseUrl": "https://code365.xyz"
},
"icons": {
"color": "color.png",
"outline": "outline.png"
},
"name": {
"short": "all",
"full": "allbot"
},
"description": {
"short": "metionallbot",
"full": "Allow users to metion all people in a group chat."
},
"accentColor": "#FFFFFF",
"staticTabs": [
{
"entityId": "homepage",
"name": "Home",
"contentUrl": "https://teamsplatform.xizhang.com",
"scopes": [
"personal"
]
},
{
"entityId": "about",
"scopes": [
"personal"
]
}
],
"bots": [
{
"botId": "d7467536-9673-426c-8ea9-168fe9ba743a",
"scopes": [
"groupchat"
],
"commandLists": [
{
"scopes": [
"groupchat"
],
"commands": [
{
"title": "help",
"description": "Tell me about the bot"
}
]
}
],
"supportsFiles": false,
"isNotificationOnly": false
}
],
"permissions": [
"identity",
"messageTeamMembers"
],
"validDomains": [
"teamsplatform.code365.xyz"
]
}
请在App Studio中,定位到 ”Languages“ 这个页面,然后选择 ”Download template“ 按钮。
你将得到一个json文件,它会自动根据当前的manifest中可以进行多语言化的字段生成一些信息。本例我得到下面这样的定义。
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.9/MicrosoftTeams.Localization.schema.json",
"name.short": "",
"name.full": "",
"description.short": "",
"description.full": "",
"staticTabs[0].name": "",
"staticTabs[1].name": "",
"bots[0].commandLists[2].commands[0].title": "",
"bots[0].commandLists[2].commands[0].description": ""
}
我用中文定义如下的内容
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.9/MicrosoftTeams.Localization.schema.json",
"name.short": "所有人",
"name.full": "所有人",
"description.short": "在群聊中提醒所有人",
"description.full": "这个功能类似于是群公告,任何人可以通过@的语法,给其他人发出提醒。",
"staticTabs[0].name": "主页",
"staticTabs[1].name": "关于",
"bots[0].commandLists[2].commands[0].title": "帮助",
"bots[0].commandLists[2].commands[0].description": "告诉我怎么用"
}
然后给该文件命令为 zh.json
,将其导入到应用定义中去。
点击”Import“ 按钮后选择上面提到的文件,然后稍等就完成导入了。
这样,我们如果在不同语言版本的Teams客户端中去尝试安装这个应用,看到的应用信息就是不一样的,例如下图是中文环境的效果。
你需要把应用上传到公司的应用商店,或者发布到微软的官方商店才会有效果。如果你直接为自己上传应用,还是会以默认语言显示。
点击应用弹出的安装界面也是完全中文的
下图是英文环境的效果。
应用界面的多语言支持
下面我们来看一下应用界面如何实现多语言支持。
选项卡应用(Node.js)
我用一个简单的例子来演示。假设你有如下这样一个简单的选项卡应用,它会在首页上跟用户打招呼,并且提供了应用的基本说明。下图3个数字标出来的部分是可以做多语言处理的。至于 ares@code365.xyz
这个部分是用户的信息,因为一个用户很难有多个不同的名称(此处读取的upn,更多的时候会读取显示名,请参考单点登录实现方案
中的介绍),所以这里不做多语言处理。
我们接下来看一下实现这个界面的核心代码
render() {
const { t } = this.props;
const userName =
Object.keys(this.state.context).length > 0
? this.state.context["upn"]
: "";
return (
<div>
<h1>欢迎使用, {userName}!</h1>
<h3>这是应用标题</h3>
<p>这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,这是应用描述信息,</p>
</div>
);
}
我这里介绍的一个方案用到了一个比较流行的库 react-i18next
,它在Github上有多达6300个star,请参考 https://github.com/i18next/react-i18next 。
添加 i18next
npm install react-i18next i18next
创建语言资源文件
让我们在components目录下面创建一个子目录(i18n),然后按照语言再建立单独的目录,例如(en, zh),然后每个目录下面定义一个translation.json 文件,然后在里面定义我们需要的资源。
创建多语言配置
在 i18n
目录下面创建一个 config.ts 文件,这里会设置默认的语言。
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
// 定义资源
const resources = {
"en-us": {
translation: require("./en/translation.json")
},
"zh-cn": {
translation: require("./zh/translation.json")
}
} as const;
// 初始化
i18n
.use(initReactI18next).init({
fallbackLng: "en-us",
lowerCaseLng: true,
resources: resources
});
export default i18n;
在应用入口处导入上面的配置
通常是在 index.tsx 文件中import即可。例如
在应用组件中添加初始化和语言设置的逻辑
在App.tsx中,添加下面的第2行,第11行,第14-16行,在Teams完成初始化时,修改语言设置。
import * as microsoftTeams from "@microsoft/teams-js";
import { useTranslation } from "react-i18next";
import { BrowserRouter as Router, Route } from "react-router-dom";
import "./App.css";
import Privacy from "./Privacy";
import Tab from "./Tab";
import TabConfig from "./TabConfig";
import TermsOfUse from "./TermsOfUse";
function App() {
const { t, i18n } = useTranslation();
microsoftTeams.initialize();
microsoftTeams.getContext((context) => {
i18n.changeLanguage(context.locale);
})
return (
<Router>
<Route exact path="/privacy" component={Privacy} />
<Route exact path="/termsofuse" component={TermsOfUse} />
<Route exact path="/tab" component={Tab} />
<Route exact path="/config" component={TabConfig} />
</Router>
);
}
export default App;
在具体的Tab组件中引入资源定义
请注意下面的第5行,第27行,第36-38行,以及第43行。这里的关键语法就是 {t("xxxxxxx")}
,它可以读取到对应资源文件中的文本定义。
import React from "react";
import "./App.css";
import * as microsoftTeams from "@microsoft/teams-js";
import { withTranslation } from "react-i18next";
class Tab extends React.Component<any, any> {
constructor(props: any) {
super(props);
this.state = {
context: {},
};
}
componentDidMount() {
microsoftTeams.getContext((context: microsoftTeams.Context) => {
this.setState({
context: context,
});
});
}
render() {
const { t } = this.props;
const userName =
Object.keys(this.state.context).length > 0
? this.state.context["upn"]
: "";
return (
<div>
<h1>{t("greeting")},{userName}!</h1>
<h3>{t("title")}</h3>
<p>{t("description")}</p>
</div>
);
}
}
export default withTranslation()(Tab);
下面是中文环境效果
下面是英文环境效果
这个案例的完整代码,你可以通过 https://github.com/code365opensource/teamsapp-samples-tab-multilanguages 获取到。
机器人应用(C#)
下面来看看机器人的场景。这里的范例,请大家同样用C#来开发。请通过 dotnet new echobot -o echobotsample
这样的命令来创建范例项目。默认情况下生成的项目,关键的代码如下。
我们准备对上面的两个方法进行改造,以使得其支持多语言。其实实现的方式有很多,我这里分别介绍三种做法。
直接定义在代码文件中
首先是最简单的一种。大家试想一下,每个机器人真正需要跟用户交互的消息不会太多的,那么我可以把这些资源直接定义在代码文件中,以便实现更加高效的编程,以及加快运行速度。
private Dictionary<string, string> en = new Dictionary<string, string>()
{
{"Echo","Echo:{0}"},
{"Welcome","Hello and welcome!"}
};
private Dictionary<string, string> zh = new Dictionary<string, string>()
{
{"Echo","回音:{0}"},
{"Welcome","欢迎!"}
};
private string GetLocalizedText(string key, string locale)
{
if (locale.ToLower().StartsWith("zh"))
{
return zh[key];
}
else
return en[key];
}
我们分别用一个字典定义了两种语言的字符串定义,然后写了一个简单的方法来读取它们。接下来尝试修改一下原有的两个方法。
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
var replyText = string.Format(GetLocalizedText("Echo", turnContext.Activity.Locale), turnContext.Activity.Text);
await turnContext.SendActivityAsync(MessageFactory.Text(replyText, replyText), cancellationToken);
}
protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
var welcomeText = GetLocalizedText("Welcome", turnContext.Activity.GetLocale());
foreach (var member in membersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
await turnContext.SendActivityAsync(MessageFactory.Text(welcomeText, welcomeText), cancellationToken);
}
}
}
定义在单独的配置文件中
如果你所需要的语言资源比较多,或者你不喜欢在代码中用字典的方式来定义它们,这完全可以理解。你可以利用.NET Core支持的基于文件的配置系统,单独定义一个(或多个)资源文件,然后在程序启动时将其加载进来,这样做的好处还在于,这些资源的定义,是只加载一次,而不是每次机器人初始化时都构建一次,有时候可能会占用额外的内存。
你可以将这些资源定义在一个文件,也可以在多个文件中。
然后,我们需要在这个项目启动时就把这些定义作为配置信息加载进来,所以你需要修改一下 Program.cs
这个文件,增加如下的几行代码。
最后,在机器人的代码中,你可以默认读取到这些配置(因为运行时已经把配置信息注入到机器人的构造函数中),你通过下面的方式接收即可。
private IConfiguration Configuration;
public EchoBot(IConfiguration configuration)
{
this.Configuration = configuration;
}
顺便将 GetLocalizedText
这个方法稍作修改即可。
private string GetLocalizedText(string key, string locale)
{
var lng = locale.ToLower().StartsWith("zh") ? "zh" : "en";
return Configuration.GetValue<string>($"{lng}:{key}");
}
下图演示的是分别用中文和英文环境交互的效果。
用模板处理多语言卡片
在机器人与用户的交互过程中,你可以充分利用最新的“自适应卡片”的技术。你可以通过 这里 学习一些基本知识,并且在下面的网站设计卡片消息。
https://adaptivecards.io/designer/
请在完成卡片设计后,点击“Copy card payload” 按钮把卡片的定义复制到单独的文件中,并且根据不同语言,定义不同的版本。
本例中,我定义了一个卡片的两种语言版本,分别用一个json文件保存起来,放在了Resources目录下面的Cards目录。接下来我们需要添加一个包来解析和生成卡片。
dotnet add package AdaptiveCards
然后,用如下几行代码即可读取卡片定义,并且根据当前语言生成卡片,并发送给用户。
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
// 回复卡片消息
var lng = turnContext.Activity.Locale.ToLower().StartsWith("zh") ? "zh" : "en";
var carddef = System.IO.File.ReadAllText($"Resources/cards/samplecard.{lng}.json");
var cardParseResult = AdaptiveCard.FromJson(carddef);
var message = MessageFactory.Attachment(new Attachment() { Content = cardParseResult.Card, ContentType = AdaptiveCard.ContentType });
await turnContext.SendActivityAsync(message);
}
就是这么简单,测试的效果如下,请参考。
本例的完整代码,请通过下面的地址访问。
https://github.com/code365opensource/teamsapp-samples-bot-multilanguages