Skip to content

Commit 0f4ad65

Browse files
author
coolbyte
committed
添加OpenCartTranslator,支持OpenCart4的语言包翻译
1 parent 5175fff commit 0f4ad65

File tree

5 files changed

+561
-39
lines changed

5 files changed

+561
-39
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
using System.Text;
2+
using System.Text.RegularExpressions;
3+
4+
namespace I18nResourceTranslator;
5+
6+
/// <summary>
7+
/// 支持open cart 4.0 的国际化语言文件进行翻译
8+
/// </summary>
9+
/// <remarks>适合翻译单个文件内容,对于整个OpenCart的语言包进行翻译,需要循环每个语言文件内容然后使用本类进行翻译</remarks>
10+
public class OpenCartTranslator : Translator
11+
{
12+
private class TranslationInfo
13+
{
14+
/// <summary>
15+
/// <see cref="Origin"/>所属行号
16+
/// </summary>
17+
public int LineNumber { get; set; }
18+
19+
/// <summary>
20+
/// <see cref="Origin"/>所属行号开始的索引
21+
/// </summary>
22+
public int StartIndexInLine { get; set; }
23+
24+
/// <summary>
25+
/// <see cref="Origin"/>所属行号结束的索引
26+
/// </summary>
27+
public int EndIndexInLine { get; set; }
28+
29+
/// <summary>
30+
/// <see cref="Origin"/>被括起的字符,单引号或双引号
31+
/// </summary>
32+
public char Quote { get; set; }
33+
34+
/// <summary>
35+
/// 原文
36+
/// </summary>
37+
public string Origin { get; set; } = default!;
38+
39+
/// <summary>
40+
/// 译文
41+
/// </summary>
42+
public string Translated { get; set; } = default!;
43+
}
44+
45+
private IList<TranslationInfo> TranslationInfos { get; }
46+
47+
/// <summary>
48+
/// 用来匹配open cart 语言文件中<code>$_['error_exception'] = 'Error Code(%s): %s in %s on line %s';</code>
49+
/// 的<code>Error Code(%s): %s in %s on line %s</code>值的正则表达式,
50+
/// 这个匹配到的值在value分组中,可以拿去翻译
51+
/// </summary>
52+
/// <remarks>该正则表达式匹配到成对的单引号或双引号中的内容,并且成对引号中的有相同引号的话不会被匹配,不懂的问GPT来解释这个正则。\1 模式:表示与第一个分组匹配相同的字符,第一个分组匹配是组quote。</remarks>
53+
private static Regex OriginValueRegex { get; } = new(@"\s*=\s*(?<quote>['""])(?<value>[^\1]+)\1\s*;\s*");
54+
55+
/// <summary>
56+
/// 要跳过翻译的值,列表项为要跳过的变量名
57+
/// </summary>
58+
/// <remarks>open cart 中的国际化的default.php文件中,包含格式化的配置、语言配置,这些属于配置信息,不需要进行翻译。
59+
/// 语言配置可以在外部进行完成,格式化的配置需要手动完成</remarks>
60+
private static IList<string> SkippedValues { get; } = new List<string>()
61+
{
62+
"code", "direction", "date_format_short",
63+
"date_format_long", "time_format", "datetime_format",
64+
"decimal_point", "thousand_point", "ckeditor", "datepicker", "DONTCHANGE"
65+
};
66+
67+
/// <summary>
68+
///
69+
/// </summary>
70+
/// <param name="from">原语言</param>
71+
/// <param name="to">目标语言</param>
72+
/// <param name="content">内容</param>
73+
public OpenCartTranslator(string from, string to, string content) : base(from, to, content)
74+
{
75+
TranslationInfos = new List<TranslationInfo>();
76+
}
77+
78+
protected override Task DoEditAndTranslation()
79+
{
80+
TranslationInfos.Clear();
81+
CollectTranslationInfos();
82+
AddTranslationTask();
83+
return Task.CompletedTask;
84+
}
85+
86+
protected override Task<string> GetStringResult(bool encode)
87+
{
88+
var sb = new StringBuilder(Content);
89+
foreach (var info in TranslationInfos)
90+
{
91+
sb.Replace($"{info.Quote}{info.Origin}{info.Quote}", $"{info.Quote}{info.Translated}{info.Quote}");
92+
}
93+
94+
return Task.FromResult(sb.ToString());
95+
}
96+
97+
/// <summary>
98+
/// 找出所有需要需要的内容,并保存到<see cref="TranslationInfos"/>
99+
/// </summary>
100+
private void CollectTranslationInfos()
101+
{
102+
using var reader = new StringReader(Content);
103+
// line 正常值是这样的
104+
// $_['text_yes'] = 'Yes';
105+
// 行号是从1开始的
106+
var lineNumber = 0;
107+
// 是否进入了多行注释块中
108+
var multiLineCommentScope = false;
109+
while (reader.ReadLine() is { } line)
110+
{
111+
lineNumber++;
112+
113+
#region 处理要跳过行,包括注释和SkippedValues定义的行
114+
115+
if (multiLineCommentScope)
116+
{
117+
// 在多行注释块中的行,跳过
118+
continue;
119+
}
120+
121+
// 去除空白符的行内容
122+
var trimmedLine = line.Trim();
123+
if (trimmedLine.StartsWith("//"))
124+
{
125+
// 跳过单行注释
126+
continue;
127+
}
128+
129+
// 处理多行注释,
130+
// 只支持 /* 开头和 */结尾的注释块,如果 /* 或者 */ 出现的位置不是开头或结尾则不会跳过
131+
if (trimmedLine.StartsWith("/*") && trimmedLine.EndsWith("*/"))
132+
{
133+
// 单行中的多行注释块,跳过
134+
continue;
135+
}
136+
137+
if (trimmedLine.StartsWith("/*"))
138+
{
139+
// 多行注释块开始
140+
// 接下来的多行注释全部跳过
141+
multiLineCommentScope = true;
142+
continue;
143+
}
144+
145+
if (trimmedLine.EndsWith("*/"))
146+
{
147+
// 多行注释块结束
148+
multiLineCommentScope = false;
149+
continue;
150+
}
151+
152+
// 去除了所有空白符的行内容
153+
var allTrimmed = Regex.Replace(line, @"\s", "");
154+
if (SkippedValues.Any(s => allTrimmed.StartsWith($"$_['{s}']") || allTrimmed.StartsWith($"$_[\"{s}\"]")))
155+
{
156+
// 行是 $_['value'] 或者 $_["value"] 格式开头的,value 为实际的索引Key,并且在需要跳过处理的列表中
157+
continue;
158+
}
159+
160+
#endregion
161+
162+
// 匹配要翻译的内容
163+
var matches = OriginValueRegex.Matches(line);
164+
foreach (Match match in matches)
165+
{
166+
var group = match.Groups["value"];
167+
var info = new TranslationInfo
168+
{
169+
LineNumber = lineNumber,
170+
StartIndexInLine = group.Index,
171+
EndIndexInLine = group.Index + group.Length - 1,
172+
Quote = match.Groups["quote"].Value[0],
173+
Origin = group.Value
174+
};
175+
TranslationInfos.Add(info);
176+
}
177+
}
178+
}
179+
180+
/// <summary>
181+
/// 对<see cref="TranslationInfos"/>内容创建异步任务并将异步任务添加到<see cref="Translator.EditTasks"/>中,方便父类进行处理
182+
/// </summary>
183+
private void AddTranslationTask()
184+
{
185+
foreach (var info in TranslationInfos)
186+
{
187+
EditTasks.Add(Translation(info));
188+
}
189+
}
190+
191+
private async Task Translation(TranslationInfo info)
192+
{
193+
info.Translated = await Translate(info.Origin);
194+
}
195+
}

0 commit comments

Comments
 (0)