前言
最近做了阿拉伯语的语言适配。记录一些比较麻烦的点。其实我也没太明白具体和其他语言有什么不同,唯一听懂的就是他们的语序是从右往左的(比如“你好!”,他们的阅读顺序是“!好你”)大概是这样子,具体原理也有很多大神在解释。但可能我没天赋,看的懵懵懂懂索性不去过分理解了。直接用按大神分享的插件就好了。
插件
用了前辈们都推荐的插件Arabic Support for Unity,免费的。没什么可说的。就一句调用代码,demo里面都有。
AssetStore插件地址
使用&问题
这个插件能满足一般的翻译。引用了命名空间的话。一句就够了。
string result = ArabicFixer.Fix(text, false, false);
这能解决一部分问题。
但不巧的是我们的项目需要翻译的比较复杂。因为里面出现了比较多的特殊字符。比如某个约定为切分标记的特殊符号,或者替换标记({}),或者换行符(\n),或者颜色串(<color=#{color}>{str})而且有的是在项目是拼接的颜色串(<color=#{color}>{str})有的是直接在配置表里面配置的颜色串。(这个真是有点醉,因为你不能在输出多语言的时候直接翻译,还需要在项目具体位置拼接完再单独翻译。)
这种的话用这个翻译的就要炸了。
我尝试在翻译的插件里做一些忽略特殊字符之类操作,但因为没理解透彻。反复尝试都有问题。
只能在外部做判断。
以下贴图都以中文为例方便理解。
下面贴两个比较常用的特殊字符处理方式。
/// <summary>
/// 包含换行特殊字符(\n)的处理,优先级高。主要针对多行文字
/// </summary>
/// <param name="Str"></param>
/// <returns></returns>
public static string ArabicWithRow(string Str)
{
Str = Str.Replace(" ", "");
string[] Holder = Str.Split('\n');
string newStr = "";
for (int i = 0; i < Holder.Length; i++)
{
if (Holder[i].Contains("color"))
{
newStr += ArabicWithColor(Holder[i]);
}
else
{
newStr += ArabicFixer.Fix(Holder[i], false, false);
}
if (i != Holder.Length-1)
{
newStr += "\n";
}
}
return newStr;
}
/// <summary>
/// 包含颜色特殊字符的处理优先级低
/// </summary>
/// <param name="Str"></param>
/// <returns></returns>
public static string ArabicWithColor(string Str)
{
Str = Str.Replace(" ", "");
//根据<>切分
string[] OldAry = Str.Split(new char[] { '<', '>' });
//颜色栈
Stack<string> ColorData = new Stack<string>();
string tempString = "";
//颜色部分都用*代替(切分符号)
for (int i = 0; i < OldAry.Length; i++)
{
if (OldAry[i].Contains("color"))
{
tempString += "*";
if (OldAry[i].Contains("#"))
{
ColorData.Push(OldAry[i]);
}
}
else
{
tempString += OldAry[i];
}
}
//翻译字符
tempString = ArabicFixer.Fix(tempString, false, false);
//通过*切分(切分符号)
string[] arry = tempString.Split('*');
string newstring = "";
//拼接
for (int i = 0; i < arry.Length; i++)
{
if (ColorData.Count != 0)
{
if (i % 2 == 0)
{
newstring += (arry[i] + "<" + ColorData.Pop() + ">");
}
else
{
newstring += (arry[i] + "</color>");
}
}
else
{
if (i == arry.Length - 1)
{
newstring += (arry[i]);
}
else
{
newstring += (arry[i] + "</color>");
}
}
}
return newstring;
}
效果如图:
总结
其实插件对一些特殊字符都是支持的,像\n或者“*”这种都是支持的。但有时候多个特殊字符连续在一起就有可能出错。
而颜色这种(<color=#{color}>{str})不是很规则的字符。直接翻译必错。
所以如果有自己自定义的符号的话,可能需要单独做处理。
不过像这种外面拼接的话还是比较麻烦。有大神能直接改成支持这种字符就好了。
额外注意
切分符号不能出现在被切分的字符串中。
我在做字符拼接时用*
切分。是因为我的项目字符串不包含*
。如果出现需要切分的字符串里出现*
,比如获得 狗崽子*1 这种,就会出问题。(所以标记切分符号的位置大家需要替换成自己项目里字符不包含的字符)
有人建议我在切分前做判断,比如包含*
就用@
来切分,那么同时包含@和*
呢。同时包含@#¥%……&*
这些呢,或者判断不包含*
就用*
切分,那么包含了怎么办?再判断有没有@
,再判断有没有@#¥%&*()
显然不太合适。
还建议大家约定好使用的特殊字符。
以上。
2020.8.18
新增单行字符过长导致自动换行的处理。
即:你输入的字符不包含换行。但字符长度超过Text的单行长度产生的自动换行。
单行字符过长自动换行会导致阿拉伯语先后顺序颠倒。
包含颜色富文本的话不要通过替换富文本位置处理(因为及其麻烦)通过标记拼接的方式相对容易。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using ArabicSupport;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine.UIElements;
//该脚本的使用基于上文插件处理过的阿拉伯语。挂在在对应text上即可。
public class TextFixer : MonoBehaviour
{
//The UI text
Text myText;
//used in cutting the lines
int startIndex;
int endIndex;
int length;
//*************************
static string[] FixedText = new string[30];
string[] Holder = new string[5];
string TextHolder;
void Start()
{
//invoke避免与其他文字处理先后位置冲突。
Invoke("WaitFixedText", 0.1f);
}
public void WaitFixedText()
{
myText = gameObject.GetComponent<Text>();
string tempText = "";
if (myText.text != null)
{
tempText = myText.text; //查找Key
}
Holder = tempText.Split('\n');
FixText();
}
public void FixText()
{
//只有一个字段的话最后一行的话不用换行。
if (Holder.Length == 1)
{
int templinesCount = 0;
myText.text = Holder[0];
Canvas.ForceUpdateCanvases();
for (int k = 0; k < FixedText.Length; k++)
{
FixedText[k] = "";
}
for (int k = 0; k < myText.cachedTextGenerator.lines.Count; k++)
{
startIndex = myText.cachedTextGenerator.lines[k].startCharIdx;
endIndex = (k == myText.cachedTextGenerator.lines.Count - 1) ? myText.text.Length
: myText.cachedTextGenerator.lines[k + 1].startCharIdx;
length = endIndex - startIndex;
FixedText[k] = myText.text.Substring(startIndex, length);
//在每行补上被截断的color标签
AddColorTag(k);
templinesCount = k;
}
myText.text = "";
for (int k = FixedText.Length - 1; k >= 0; k--)
{
if (FixedText[k] != "" && FixedText[k] != "\n" && FixedText[k] != null)
{
if (templinesCount == 0)
{
TextHolder += FixedText[k];
}
else
{
if (templinesCount != 0)
{
TextHolder += FixedText[k] + "\n";
templinesCount--;
}
else
{
TextHolder += FixedText[k];
}
}
}
}
}
else
{
//包含换行符的多行处理
for (int i = 0; i < Holder.Length; i++)
{
int templinesCount = 0;
myText.text = Holder[i];
Canvas.ForceUpdateCanvases();
for (int k = 0; k < FixedText.Length; k++)
{
FixedText[k] = "";
}
for (int k = 0; k < myText.cachedTextGenerator.lines.Count; k++)
{
startIndex = myText.cachedTextGenerator.lines[k].startCharIdx;
endIndex = (k == myText.cachedTextGenerator.lines.Count - 1) ? myText.text.Length
: myText.cachedTextGenerator.lines[k + 1].startCharIdx;
length = endIndex - startIndex;
FixedText[k] = myText.text.Substring(startIndex, length);
//在每行补上被截断的color标签
AddColorTag(k);
templinesCount = k;
}
myText.text = "";
//如果是最后一个字段的最后一行的话不用换行。
if (i == Holder.Length - 1)
{
for (int k = FixedText.Length - 1; k >= 0; k--)
{
if (FixedText[k] != "" && FixedText[k] != "\n" && FixedText[k] != null)
{
if (templinesCount == 0)
{
TextHolder += FixedText[k];
}
else
{
if (templinesCount != 0)
{
TextHolder += FixedText[k] + "\n";
templinesCount--;
}
else
{
TextHolder += FixedText[k];
}
}
}
}
}
else
{
for (int k = FixedText.Length - 1; k >= 0; k--)
{
if (FixedText[k] != "" && FixedText[k] != "\n" && FixedText[k] != null)
{
TextHolder += FixedText[k] + "\n";
}
}
}
}
}
myText.text = TextHolder;
//yield return new WaitForEndOfFrame();
}
/// <summary>
/// 临时颜色
/// </summary>
public static string tempcolor = "";
/// <summary>
/// 是否能够组成整数组颜色
/// </summary>
public static int IsOneGroup = 0;
static void AddColorTag(int k)
{
//通过本行中color左标签和右标签的数量判断需要在本行的那一端添加color标签
int leftCount = Regex.Matches(FixedText[k], "<color").Count;
int rightCount = Regex.Matches(FixedText[k], "</color>").Count;
//判断最右侧是否缺少</color>标签
int index1 = -1;
int index2 = -2;
index1 = FixedText[k].LastIndexOf("<color");
Debug.Log("index1:" + index1);
if (index1 != -1)
{
//判断index1后面还有没有</color>
index2 = FixedText[k].IndexOf("</color>", index1);
Debug.Log("index2:" + index2);
}
//左标签多于右标签 则在本行末尾添加</color>
if (leftCount > rightCount)
{
IsOneGroup++;
FixedText[k] += "</color>";
//保存颜色减少遍历次数
for (int i = 10; i < 20; i++)
{
tempcolor = FixedText[k].Substring(index1, i);
if (tempcolor.EndsWith(">"))
{
break;
}
}
}
//右标签多于左标签 则获取到上一行的最后一个左标签 添加到本行开头
else if (rightCount > leftCount)
{
IsOneGroup++;
FixedText[k] = tempcolor + FixedText[k];
}
//当存在</color><color=...>的情况 两边都添加
else if (leftCount == rightCount)
{
if (leftCount > 0 & index2 == -1)
{
FixedText[k] = tempcolor + FixedText[k];
for (int i =10; i < 20; i++)
{
tempcolor = FixedText[k].Substring(FixedText[k].LastIndexOf("<color"), i);
if (tempcolor.EndsWith(">"))
{
break;
}
}
FixedText[k] += "</color>";
}
else
{
if (IsOneGroup%2!=0)
{
FixedText[k] = tempcolor + FixedText[k];
FixedText[k] += "</color>";
}
}
}
}
}
以上脚本处理包含颜色多文本
效果示例 仅测试多行换行处理。阿拉伯语需先翻译再进行这个换行的操作。
处理前:语序a——>e
处理后:e——>a
以上。
2020.8.28 补充。
在上次做了多行处理之后,我们忽略了一个问题。就是阿拉伯语虽然是反向读的。但在行的阅读上还是从上往下的。
即上图我们虽然处理成e——>a的顺序了。但仍存在一个问题。即第一行的长度短,而最后一行的长度长。
而且你文本中出现小括号可能会出现以下情况。
如果你出现中括号可能出现以下状况。
那么这些都怎么处理呢!!!
最终处理后显示的文字即为如下效果。阿语要右对齐。
但为什么这里但仍存在 第一行的长度短,而最后一行的长度长的问题呢。
这个是因为阿拉伯语和中文的单个字符所占的字节长度不同导致的。
如果是用阿语演示的话;
以上。
最后:其实如果最初文本规范的话以上解决方案完全满足需求。 现在我处理极其费劲是项目初期并未考虑做阿语。导致各种奇奇怪怪的字符都可能出现。我最终的解决方案里处理了一些我们文本里包含的字符。也预留了处理特殊字符的位置。然后封装了一个方法。这样在给text赋值前调用一下就可以避免有些动态显示的字符不能被处理或者被处理多次的问题。
以下提供一下思路
2020.9.10补充。
因为这个处理流程,过于麻烦了。虽然基本达到了效果,但效果并不是很理想。因为后续发现项目配置表里的特殊字符越来越多。导致每个新字符的出现匹配起来都极其麻烦。而且换行很有可能把一个完整的词切分开。
归根结底是因为TextFixer 是一个Arabic Support for Unity的一个衍生插件Arabic Line Fixer给的处理方案,然后我一直按照这个方案一直往下处理。
最终感觉不妥,又重新换了一种更为合适的方案。
即阿拉伯语单词之间是根据空格切分。通过计算拼接阿拉伯单词的长度来确认合适的换行位置。避免把一个完整的词切开,然后把富文本补上。
这种处理方案的效果目前是最好的。
如下图示效果
2020.10.16补充。
没想到还会继续进行补充。
这里涉及一个翻译前赋值和翻译后赋值的问题。
以这个词为例。
直接复制粘贴进去。Unity显示的已经不正常了。
我们可以仔细对比我们写进test的aaa的值和在视图位置的test脚本中aaa的值。
即我们读的值在写在脚本里的时候已经产生了变化。只是我们在代码中看不出来。
这是为什么我们需要处理阿拉伯语的原因。
我们翻译前,给text赋的值在Unity里必然有显示问题。但如果我翻译后copy这个值,或者传到服务器储存,那么我再拿的这个值就是正常的。
但如果你用阿拉伯语输入值(inputfield)保存的话,应该显示的也是有问题的。在编辑器测试是显示错误的。
所以可能在用户输入阿拉伯语的时候也需要进行一个操作。(待验证。)