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