Michael Hu commited on
Commit
9626844
Β·
1 Parent(s): 1be582a

Migrate translation service to infrastructure layer

Browse files
src/infrastructure/translation/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Translation provider implementations."""
src/infrastructure/translation/nllb_provider.py ADDED
@@ -0,0 +1,625 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """NLLB translation provider implementation."""
2
+
3
+ import logging
4
+ from typing import Dict, List, Optional
5
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
6
+
7
+ from ..base.translation_provider_base import TranslationProviderBase
8
+ from ...domain.exceptions import TranslationFailedException
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class NLLBTranslationProvider(TranslationProviderBase):
14
+ """NLLB-200-3.3B translation provider implementation."""
15
+
16
+ # NLLB language code mappings
17
+ LANGUAGE_MAPPINGS = {
18
+ 'en': 'eng_Latn',
19
+ 'zh': 'zho_Hans',
20
+ 'zh-cn': 'zho_Hans',
21
+ 'zh-tw': 'zho_Hant',
22
+ 'es': 'spa_Latn',
23
+ 'fr': 'fra_Latn',
24
+ 'de': 'deu_Latn',
25
+ 'ja': 'jpn_Jpan',
26
+ 'ko': 'kor_Hang',
27
+ 'ar': 'arb_Arab',
28
+ 'hi': 'hin_Deva',
29
+ 'pt': 'por_Latn',
30
+ 'ru': 'rus_Cyrl',
31
+ 'it': 'ita_Latn',
32
+ 'nl': 'nld_Latn',
33
+ 'pl': 'pol_Latn',
34
+ 'tr': 'tur_Latn',
35
+ 'sv': 'swe_Latn',
36
+ 'da': 'dan_Latn',
37
+ 'no': 'nor_Latn',
38
+ 'fi': 'fin_Latn',
39
+ 'el': 'ell_Grek',
40
+ 'he': 'heb_Hebr',
41
+ 'th': 'tha_Thai',
42
+ 'vi': 'vie_Latn',
43
+ 'id': 'ind_Latn',
44
+ 'ms': 'zsm_Latn',
45
+ 'tl': 'tgl_Latn',
46
+ 'uk': 'ukr_Cyrl',
47
+ 'cs': 'ces_Latn',
48
+ 'sk': 'slk_Latn',
49
+ 'hu': 'hun_Latn',
50
+ 'ro': 'ron_Latn',
51
+ 'bg': 'bul_Cyrl',
52
+ 'hr': 'hrv_Latn',
53
+ 'sr': 'srp_Cyrl',
54
+ 'sl': 'slv_Latn',
55
+ 'et': 'est_Latn',
56
+ 'lv': 'lvs_Latn',
57
+ 'lt': 'lit_Latn',
58
+ 'mt': 'mlt_Latn',
59
+ 'ga': 'gle_Latn',
60
+ 'cy': 'cym_Latn',
61
+ 'is': 'isl_Latn',
62
+ 'mk': 'mkd_Cyrl',
63
+ 'sq': 'sqi_Latn',
64
+ 'eu': 'eus_Latn',
65
+ 'ca': 'cat_Latn',
66
+ 'gl': 'glg_Latn',
67
+ 'ast': 'ast_Latn',
68
+ 'oc': 'oci_Latn',
69
+ 'br': 'bre_Latn',
70
+ 'co': 'cos_Latn',
71
+ 'sc': 'srd_Latn',
72
+ 'rm': 'roh_Latn',
73
+ 'fur': 'fur_Latn',
74
+ 'lij': 'lij_Latn',
75
+ 'vec': 'vec_Latn',
76
+ 'pms': 'pms_Latn',
77
+ 'lmo': 'lmo_Latn',
78
+ 'nap': 'nap_Latn',
79
+ 'scn': 'scn_Latn',
80
+ 'wa': 'wln_Latn',
81
+ 'frp': 'frp_Latn',
82
+ 'gsw': 'gsw_Latn',
83
+ 'bar': 'bar_Latn',
84
+ 'ksh': 'ksh_Latn',
85
+ 'lb': 'ltz_Latn',
86
+ 'li': 'lim_Latn',
87
+ 'nds': 'nds_Latn',
88
+ 'pdc': 'pdc_Latn',
89
+ 'sli': 'sli_Latn',
90
+ 'vmf': 'vmf_Latn',
91
+ 'yi': 'yid_Hebr',
92
+ 'af': 'afr_Latn',
93
+ 'zu': 'zul_Latn',
94
+ 'xh': 'xho_Latn',
95
+ 'st': 'sot_Latn',
96
+ 'tn': 'tsn_Latn',
97
+ 'ss': 'ssw_Latn',
98
+ 'nr': 'nbl_Latn',
99
+ 've': 'ven_Latn',
100
+ 'ts': 'tso_Latn',
101
+ 'sw': 'swh_Latn',
102
+ 'rw': 'kin_Latn',
103
+ 'rn': 'run_Latn',
104
+ 'ny': 'nya_Latn',
105
+ 'sn': 'sna_Latn',
106
+ 'yo': 'yor_Latn',
107
+ 'ig': 'ibo_Latn',
108
+ 'ha': 'hau_Latn',
109
+ 'ff': 'fuv_Latn',
110
+ 'wo': 'wol_Latn',
111
+ 'bm': 'bam_Latn',
112
+ 'dyu': 'dyu_Latn',
113
+ 'ee': 'ewe_Latn',
114
+ 'tw': 'twi_Latn',
115
+ 'ak': 'aka_Latn',
116
+ 'gaa': 'gaa_Latn',
117
+ 'lg': 'lug_Latn',
118
+ 'luo': 'luo_Latn',
119
+ 'ki': 'kik_Latn',
120
+ 'kam': 'kam_Latn',
121
+ 'luy': 'luy_Latn',
122
+ 'mer': 'mer_Latn',
123
+ 'kln': 'kln_Latn',
124
+ 'kab': 'kab_Latn',
125
+ 'ber': 'ber_Latn',
126
+ 'am': 'amh_Ethi',
127
+ 'ti': 'tir_Ethi',
128
+ 'om': 'orm_Latn',
129
+ 'so': 'som_Latn',
130
+ 'mg': 'plt_Latn',
131
+ 'ny': 'nya_Latn',
132
+ 'bem': 'bem_Latn',
133
+ 'tum': 'tum_Latn',
134
+ 'loz': 'loz_Latn',
135
+ 'lua': 'lua_Latn',
136
+ 'umb': 'umb_Latn',
137
+ 'kmb': 'kmb_Latn',
138
+ 'kg': 'kon_Latn',
139
+ 'ln': 'lin_Latn',
140
+ 'sg': 'sag_Latn',
141
+ 'fon': 'fon_Latn',
142
+ 'mos': 'mos_Latn',
143
+ 'dga': 'dga_Latn',
144
+ 'kbp': 'kbp_Latn',
145
+ 'nus': 'nus_Latn',
146
+ 'din': 'din_Latn',
147
+ 'luo': 'luo_Latn',
148
+ 'ach': 'ach_Latn',
149
+ 'teo': 'teo_Latn',
150
+ 'mdt': 'mdt_Latn',
151
+ 'knc': 'knc_Latn',
152
+ 'fuv': 'fuv_Latn',
153
+ 'kr': 'kau_Latn',
154
+ 'dje': 'dje_Latn',
155
+ 'son': 'son_Latn',
156
+ 'tmh': 'tmh_Latn',
157
+ 'taq': 'taq_Latn',
158
+ 'ttq': 'ttq_Latn',
159
+ 'thv': 'thv_Latn',
160
+ 'taq': 'taq_Tfng',
161
+ 'shi': 'shi_Tfng',
162
+ 'tzm': 'tzm_Tfng',
163
+ 'rif': 'rif_Latn',
164
+ 'kab': 'kab_Latn',
165
+ 'shy': 'shy_Latn',
166
+ 'ber': 'ber_Latn',
167
+ 'acm': 'acm_Arab',
168
+ 'aeb': 'aeb_Arab',
169
+ 'ajp': 'ajp_Arab',
170
+ 'apc': 'apc_Arab',
171
+ 'ars': 'ars_Arab',
172
+ 'ary': 'ary_Arab',
173
+ 'arz': 'arz_Arab',
174
+ 'auz': 'auz_Arab',
175
+ 'avl': 'avl_Arab',
176
+ 'ayh': 'ayh_Arab',
177
+ 'ayn': 'ayn_Arab',
178
+ 'ayp': 'ayp_Arab',
179
+ 'bbz': 'bbz_Arab',
180
+ 'pga': 'pga_Arab',
181
+ 'shu': 'shu_Arab',
182
+ 'ssh': 'ssh_Arab',
183
+ 'fa': 'pes_Arab',
184
+ 'tg': 'tgk_Cyrl',
185
+ 'ps': 'pbt_Arab',
186
+ 'ur': 'urd_Arab',
187
+ 'sd': 'snd_Arab',
188
+ 'ks': 'kas_Arab',
189
+ 'dv': 'div_Thaa',
190
+ 'ne': 'npi_Deva',
191
+ 'si': 'sin_Sinh',
192
+ 'my': 'mya_Mymr',
193
+ 'km': 'khm_Khmr',
194
+ 'lo': 'lao_Laoo',
195
+ 'ka': 'kat_Geor',
196
+ 'hy': 'hye_Armn',
197
+ 'az': 'azj_Latn',
198
+ 'kk': 'kaz_Cyrl',
199
+ 'ky': 'kir_Cyrl',
200
+ 'uz': 'uzn_Latn',
201
+ 'tk': 'tuk_Latn',
202
+ 'mn': 'khk_Cyrl',
203
+ 'bo': 'bod_Tibt',
204
+ 'dz': 'dzo_Tibt',
205
+ 'ug': 'uig_Arab',
206
+ 'tt': 'tat_Cyrl',
207
+ 'ba': 'bak_Cyrl',
208
+ 'cv': 'chv_Cyrl',
209
+ 'sah': 'sah_Cyrl',
210
+ 'tyv': 'tyv_Cyrl',
211
+ 'kjh': 'kjh_Cyrl',
212
+ 'alt': 'alt_Cyrl',
213
+ 'krc': 'krc_Cyrl',
214
+ 'kum': 'kum_Cyrl',
215
+ 'nog': 'nog_Cyrl',
216
+ 'kaa': 'kaa_Cyrl',
217
+ 'crh': 'crh_Latn',
218
+ 'gag': 'gag_Latn',
219
+ 'tr': 'tur_Latn',
220
+ 'az': 'azb_Arab',
221
+ 'ku': 'ckb_Arab',
222
+ 'lrc': 'lrc_Arab',
223
+ 'mzn': 'mzn_Arab',
224
+ 'glk': 'glk_Arab',
225
+ 'fa': 'pes_Arab',
226
+ 'tg': 'tgk_Cyrl',
227
+ 'prs': 'prs_Arab',
228
+ 'haz': 'haz_Arab',
229
+ 'bal': 'bal_Arab',
230
+ 'bcc': 'bcc_Arab',
231
+ 'bgp': 'bgp_Arab',
232
+ 'bqi': 'bqi_Arab',
233
+ 'ckb': 'ckb_Arab',
234
+ 'diq': 'diq_Latn',
235
+ 'hac': 'hac_Arab',
236
+ 'kur': 'kmr_Latn',
237
+ 'lki': 'lki_Arab',
238
+ 'pnb': 'pnb_Arab',
239
+ 'ps': 'pbt_Arab',
240
+ 'sd': 'snd_Arab',
241
+ 'skr': 'skr_Arab',
242
+ 'ur': 'urd_Arab',
243
+ 'wne': 'wne_Arab',
244
+ 'xmf': 'xmf_Geor',
245
+ 'ka': 'kat_Geor',
246
+ 'hy': 'hye_Armn',
247
+ 'xcl': 'xcl_Armn',
248
+ 'he': 'heb_Hebr',
249
+ 'yi': 'yid_Hebr',
250
+ 'lad': 'lad_Hebr',
251
+ 'ar': 'arb_Arab',
252
+ 'mt': 'mlt_Latn',
253
+ 'ml': 'mal_Mlym',
254
+ 'kn': 'kan_Knda',
255
+ 'te': 'tel_Telu',
256
+ 'ta': 'tam_Taml',
257
+ 'or': 'ory_Orya',
258
+ 'as': 'asm_Beng',
259
+ 'bn': 'ben_Beng',
260
+ 'gu': 'guj_Gujr',
261
+ 'pa': 'pan_Guru',
262
+ 'hi': 'hin_Deva',
263
+ 'mr': 'mar_Deva',
264
+ 'ne': 'npi_Deva',
265
+ 'sa': 'san_Deva',
266
+ 'mai': 'mai_Deva',
267
+ 'bho': 'bho_Deva',
268
+ 'mag': 'mag_Deva',
269
+ 'sck': 'sck_Deva',
270
+ 'new': 'new_Deva',
271
+ 'bpy': 'bpy_Beng',
272
+ 'ctg': 'ctg_Beng',
273
+ 'rkt': 'rkt_Beng',
274
+ 'syl': 'syl_Beng',
275
+ 'sat': 'sat_Olck',
276
+ 'kha': 'kha_Latn',
277
+ 'grt': 'grt_Beng',
278
+ 'lus': 'lus_Latn',
279
+ 'mni': 'mni_Beng',
280
+ 'kok': 'kok_Deva',
281
+ 'gom': 'gom_Deva',
282
+ 'sd': 'snd_Deva',
283
+ 'doi': 'doi_Deva',
284
+ 'ks': 'kas_Deva',
285
+ 'ur': 'urd_Arab',
286
+ 'ps': 'pbt_Arab',
287
+ 'bal': 'bal_Arab',
288
+ 'bcc': 'bcc_Arab',
289
+ 'bgp': 'bgp_Arab',
290
+ 'brh': 'brh_Arab',
291
+ 'hnd': 'hnd_Arab',
292
+ 'lah': 'lah_Arab',
293
+ 'pnb': 'pnb_Arab',
294
+ 'pst': 'pst_Arab',
295
+ 'skr': 'skr_Arab',
296
+ 'wne': 'wne_Arab',
297
+ 'si': 'sin_Sinh',
298
+ 'dv': 'div_Thaa',
299
+ 'my': 'mya_Mymr',
300
+ 'shn': 'shn_Mymr',
301
+ 'mnw': 'mnw_Mymr',
302
+ 'kac': 'kac_Latn',
303
+ 'shn': 'shn_Mymr',
304
+ 'km': 'khm_Khmr',
305
+ 'lo': 'lao_Laoo',
306
+ 'th': 'tha_Thai',
307
+ 'vi': 'vie_Latn',
308
+ 'cjm': 'cjm_Arab',
309
+ 'bjn': 'bjn_Latn',
310
+ 'bug': 'bug_Latn',
311
+ 'jv': 'jav_Latn',
312
+ 'mad': 'mad_Latn',
313
+ 'ms': 'zsm_Latn',
314
+ 'min': 'min_Latn',
315
+ 'su': 'sun_Latn',
316
+ 'ban': 'ban_Latn',
317
+ 'bbc': 'bbc_Latn',
318
+ 'btk': 'btk_Latn',
319
+ 'gor': 'gor_Latn',
320
+ 'ilo': 'ilo_Latn',
321
+ 'pag': 'pag_Latn',
322
+ 'war': 'war_Latn',
323
+ 'hil': 'hil_Latn',
324
+ 'bcl': 'bcl_Latn',
325
+ 'pam': 'pam_Latn',
326
+ 'tl': 'tgl_Latn',
327
+ 'ceb': 'ceb_Latn',
328
+ 'akl': 'akl_Latn',
329
+ 'bik': 'bik_Latn',
330
+ 'cbk': 'cbk_Latn',
331
+ 'krj': 'krj_Latn',
332
+ 'tsg': 'tsg_Latn',
333
+ 'zh': 'zho_Hans',
334
+ 'yue': 'yue_Hant',
335
+ 'wuu': 'wuu_Hans',
336
+ 'hsn': 'hsn_Hans',
337
+ 'nan': 'nan_Hant',
338
+ 'hak': 'hak_Hant',
339
+ 'gan': 'gan_Hans',
340
+ 'cdo': 'cdo_Hant',
341
+ 'lzh': 'lzh_Hans',
342
+ 'ja': 'jpn_Jpan',
343
+ 'ko': 'kor_Hang',
344
+ 'ain': 'ain_Kana',
345
+ 'ryu': 'ryu_Kana',
346
+ 'eo': 'epo_Latn',
347
+ 'ia': 'ina_Latn',
348
+ 'ie': 'ile_Latn',
349
+ 'io': 'ido_Latn',
350
+ 'vo': 'vol_Latn',
351
+ 'nov': 'nov_Latn',
352
+ 'lfn': 'lfn_Latn',
353
+ 'jbo': 'jbo_Latn',
354
+ 'tlh': 'tlh_Latn',
355
+ 'na': 'nau_Latn',
356
+ 'ch': 'cha_Latn',
357
+ 'mh': 'mah_Latn',
358
+ 'gil': 'gil_Latn',
359
+ 'kos': 'kos_Latn',
360
+ 'pon': 'pon_Latn',
361
+ 'yap': 'yap_Latn',
362
+ 'chk': 'chk_Latn',
363
+ 'uli': 'uli_Latn',
364
+ 'wol': 'wol_Latn',
365
+ 'pau': 'pau_Latn',
366
+ 'sm': 'smo_Latn',
367
+ 'to': 'ton_Latn',
368
+ 'fj': 'fij_Latn',
369
+ 'ty': 'tah_Latn',
370
+ 'mi': 'mri_Latn',
371
+ 'haw': 'haw_Latn',
372
+ 'rap': 'rap_Latn',
373
+ 'tvl': 'tvl_Latn',
374
+ 'niu': 'niu_Latn',
375
+ 'tkl': 'tkl_Latn',
376
+ 'bi': 'bis_Latn',
377
+ 'ho': 'hmo_Latn',
378
+ 'kg': 'kon_Latn',
379
+ 'kj': 'kua_Latn',
380
+ 'rw': 'kin_Latn',
381
+ 'rn': 'run_Latn',
382
+ 'sg': 'sag_Latn',
383
+ 'sn': 'sna_Latn',
384
+ 'ss': 'ssw_Latn',
385
+ 'st': 'sot_Latn',
386
+ 'sw': 'swh_Latn',
387
+ 'tn': 'tsn_Latn',
388
+ 'ts': 'tso_Latn',
389
+ 've': 'ven_Latn',
390
+ 'xh': 'xho_Latn',
391
+ 'zu': 'zul_Latn',
392
+ 'nd': 'nde_Latn',
393
+ 'nr': 'nbl_Latn',
394
+ 'ny': 'nya_Latn',
395
+ 'bm': 'bam_Latn',
396
+ 'ee': 'ewe_Latn',
397
+ 'ff': 'fuv_Latn',
398
+ 'ha': 'hau_Latn',
399
+ 'ig': 'ibo_Latn',
400
+ 'ki': 'kik_Latn',
401
+ 'lg': 'lug_Latn',
402
+ 'ln': 'lin_Latn',
403
+ 'mg': 'plt_Latn',
404
+ 'om': 'orm_Latn',
405
+ 'rw': 'kin_Latn',
406
+ 'rn': 'run_Latn',
407
+ 'sg': 'sag_Latn',
408
+ 'sn': 'sna_Latn',
409
+ 'so': 'som_Latn',
410
+ 'sw': 'swh_Latn',
411
+ 'ti': 'tir_Ethi',
412
+ 'tw': 'twi_Latn',
413
+ 'wo': 'wol_Latn',
414
+ 'xh': 'xho_Latn',
415
+ 'yo': 'yor_Latn',
416
+ 'zu': 'zul_Latn'
417
+ }
418
+
419
+ def __init__(self, model_name: str = "facebook/nllb-200-3.3B", max_chunk_length: int = 1000):
420
+ """
421
+ Initialize NLLB translation provider.
422
+
423
+ Args:
424
+ model_name: The NLLB model name to use
425
+ max_chunk_length: Maximum length for text chunks
426
+ """
427
+ # Build supported languages mapping
428
+ supported_languages = {}
429
+ for lang_code in self.LANGUAGE_MAPPINGS.keys():
430
+ # For simplicity, assume all languages can translate to all other languages
431
+ # In practice, you might want to be more specific about supported pairs
432
+ supported_languages[lang_code] = [
433
+ target for target in self.LANGUAGE_MAPPINGS.keys()
434
+ if target != lang_code
435
+ ]
436
+
437
+ super().__init__(
438
+ provider_name="NLLB-200-3.3B",
439
+ supported_languages=supported_languages
440
+ )
441
+
442
+ self.model_name = model_name
443
+ self.max_chunk_length = max_chunk_length
444
+ self._tokenizer: Optional[AutoTokenizer] = None
445
+ self._model: Optional[AutoModelForSeq2SeqLM] = None
446
+ self._model_loaded = False
447
+
448
+ def _translate_chunk(self, text: str, source_language: str, target_language: str) -> str:
449
+ """
450
+ Translate a single chunk of text using NLLB model.
451
+
452
+ Args:
453
+ text: The text chunk to translate
454
+ source_language: Source language code
455
+ target_language: Target language code
456
+
457
+ Returns:
458
+ str: The translated text chunk
459
+ """
460
+ try:
461
+ # Ensure model is loaded
462
+ self._ensure_model_loaded()
463
+
464
+ # Map language codes to NLLB format
465
+ source_nllb = self._map_language_code(source_language)
466
+ target_nllb = self._map_language_code(target_language)
467
+
468
+ logger.debug(f"Translating chunk from {source_nllb} to {target_nllb}")
469
+
470
+ # Tokenize with source language specification
471
+ inputs = self._tokenizer(
472
+ text,
473
+ return_tensors="pt",
474
+ max_length=1024,
475
+ truncation=True
476
+ )
477
+
478
+ # Generate translation with target language specification
479
+ outputs = self._model.generate(
480
+ **inputs,
481
+ forced_bos_token_id=self._tokenizer.convert_tokens_to_ids(target_nllb),
482
+ max_new_tokens=1024,
483
+ num_beams=4,
484
+ early_stopping=True
485
+ )
486
+
487
+ # Decode the translation
488
+ translated = self._tokenizer.decode(outputs[0], skip_special_tokens=True)
489
+
490
+ # Post-process the translation
491
+ translated = self._postprocess_text(translated)
492
+
493
+ logger.debug(f"Chunk translation completed: {len(text)} -> {len(translated)} chars")
494
+ return translated
495
+
496
+ except Exception as e:
497
+ self._handle_provider_error(e, "chunk translation")
498
+
499
+ def _ensure_model_loaded(self) -> None:
500
+ """Ensure the NLLB model and tokenizer are loaded."""
501
+ if self._model_loaded:
502
+ return
503
+
504
+ try:
505
+ logger.info(f"Loading NLLB model: {self.model_name}")
506
+
507
+ # Load tokenizer
508
+ self._tokenizer = AutoTokenizer.from_pretrained(
509
+ self.model_name,
510
+ src_lang="eng_Latn" # Default source language
511
+ )
512
+
513
+ # Load model
514
+ self._model = AutoModelForSeq2SeqLM.from_pretrained(self.model_name)
515
+
516
+ self._model_loaded = True
517
+ logger.info("NLLB model loaded successfully")
518
+
519
+ except Exception as e:
520
+ logger.error(f"Failed to load NLLB model: {str(e)}")
521
+ raise TranslationFailedException(f"Failed to load NLLB model: {str(e)}") from e
522
+
523
+ def _map_language_code(self, language_code: str) -> str:
524
+ """
525
+ Map standard language code to NLLB format.
526
+
527
+ Args:
528
+ language_code: Standard language code (e.g., 'en', 'zh')
529
+
530
+ Returns:
531
+ str: NLLB language code (e.g., 'eng_Latn', 'zho_Hans')
532
+ """
533
+ # Normalize language code to lowercase
534
+ normalized_code = language_code.lower()
535
+
536
+ # Check direct mapping
537
+ if normalized_code in self.LANGUAGE_MAPPINGS:
538
+ return self.LANGUAGE_MAPPINGS[normalized_code]
539
+
540
+ # Handle common variations
541
+ if normalized_code.startswith('zh'):
542
+ if 'tw' in normalized_code or 'hant' in normalized_code or 'traditional' in normalized_code:
543
+ return 'zho_Hant'
544
+ else:
545
+ return 'zho_Hans'
546
+
547
+ # Default fallback for unknown codes
548
+ logger.warning(f"Unknown language code: {language_code}, defaulting to English")
549
+ return 'eng_Latn'
550
+
551
+ def is_available(self) -> bool:
552
+ """
553
+ Check if the NLLB translation provider is available.
554
+
555
+ Returns:
556
+ bool: True if provider is available, False otherwise
557
+ """
558
+ try:
559
+ # Try to import required dependencies
560
+ import transformers
561
+ import torch
562
+
563
+ # Check if we can load the tokenizer (lightweight check)
564
+ if not self._model_loaded:
565
+ try:
566
+ test_tokenizer = AutoTokenizer.from_pretrained(
567
+ self.model_name,
568
+ src_lang="eng_Latn"
569
+ )
570
+ return True
571
+ except Exception as e:
572
+ logger.warning(f"NLLB model not available: {str(e)}")
573
+ return False
574
+ else:
575
+ return True
576
+
577
+ except ImportError as e:
578
+ logger.warning(f"NLLB dependencies not available: {str(e)}")
579
+ return False
580
+
581
+ def get_supported_languages(self) -> Dict[str, List[str]]:
582
+ """
583
+ Get supported language pairs for NLLB provider.
584
+
585
+ Returns:
586
+ dict: Mapping of source languages to supported target languages
587
+ """
588
+ return self.supported_languages.copy()
589
+
590
+ def get_model_info(self) -> Dict[str, str]:
591
+ """
592
+ Get information about the loaded model.
593
+
594
+ Returns:
595
+ dict: Model information
596
+ """
597
+ return {
598
+ 'provider': self.provider_name,
599
+ 'model_name': self.model_name,
600
+ 'model_loaded': str(self._model_loaded),
601
+ 'supported_language_count': str(len(self.LANGUAGE_MAPPINGS)),
602
+ 'max_chunk_length': str(self.max_chunk_length)
603
+ }
604
+
605
+ def set_model_name(self, model_name: str) -> None:
606
+ """
607
+ Set a different NLLB model name.
608
+
609
+ Args:
610
+ model_name: The new model name to use
611
+ """
612
+ if model_name != self.model_name:
613
+ self.model_name = model_name
614
+ self._model_loaded = False
615
+ self._tokenizer = None
616
+ self._model = None
617
+ logger.info(f"Model name changed to: {model_name}")
618
+
619
+ def clear_model_cache(self) -> None:
620
+ """Clear the loaded model from memory."""
621
+ if self._model_loaded:
622
+ self._tokenizer = None
623
+ self._model = None
624
+ self._model_loaded = False
625
+ logger.info("NLLB model cache cleared")
src/infrastructure/translation/provider_factory.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Translation provider factory for creating and managing translation providers."""
2
+
3
+ import logging
4
+ from typing import Dict, List, Optional, Type
5
+ from enum import Enum
6
+
7
+ from ..base.translation_provider_base import TranslationProviderBase
8
+ from .nllb_provider import NLLBTranslationProvider
9
+ from ...domain.exceptions import TranslationFailedException
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class TranslationProviderType(Enum):
15
+ """Enumeration of available translation provider types."""
16
+ NLLB = "nllb"
17
+ # Future providers can be added here
18
+ # GOOGLE = "google"
19
+ # AZURE = "azure"
20
+ # AWS = "aws"
21
+
22
+
23
+ class TranslationProviderFactory:
24
+ """Factory for creating and managing translation provider instances."""
25
+
26
+ # Registry of available provider classes
27
+ _PROVIDER_REGISTRY: Dict[TranslationProviderType, Type[TranslationProviderBase]] = {
28
+ TranslationProviderType.NLLB: NLLBTranslationProvider,
29
+ }
30
+
31
+ # Default provider configurations
32
+ _DEFAULT_CONFIGS = {
33
+ TranslationProviderType.NLLB: {
34
+ 'model_name': 'facebook/nllb-200-3.3B',
35
+ 'max_chunk_length': 1000
36
+ }
37
+ }
38
+
39
+ def __init__(self):
40
+ """Initialize the translation provider factory."""
41
+ self._provider_cache: Dict[str, TranslationProviderBase] = {}
42
+ self._availability_cache: Dict[TranslationProviderType, bool] = {}
43
+
44
+ def create_provider(
45
+ self,
46
+ provider_type: TranslationProviderType,
47
+ config: Optional[Dict] = None,
48
+ use_cache: bool = True
49
+ ) -> TranslationProviderBase:
50
+ """
51
+ Create a translation provider instance.
52
+
53
+ Args:
54
+ provider_type: The type of provider to create
55
+ config: Optional configuration parameters for the provider
56
+ use_cache: Whether to use cached provider instances
57
+
58
+ Returns:
59
+ TranslationProviderBase: The created provider instance
60
+
61
+ Raises:
62
+ TranslationFailedException: If provider creation fails
63
+ """
64
+ try:
65
+ # Generate cache key
66
+ cache_key = self._generate_cache_key(provider_type, config)
67
+
68
+ # Return cached instance if available and requested
69
+ if use_cache and cache_key in self._provider_cache:
70
+ logger.debug(f"Returning cached {provider_type.value} provider")
71
+ return self._provider_cache[cache_key]
72
+
73
+ # Check if provider type is registered
74
+ if provider_type not in self._PROVIDER_REGISTRY:
75
+ raise TranslationFailedException(
76
+ f"Unknown translation provider type: {provider_type.value}. "
77
+ f"Available types: {[t.value for t in self._PROVIDER_REGISTRY.keys()]}"
78
+ )
79
+
80
+ # Get provider class
81
+ provider_class = self._PROVIDER_REGISTRY[provider_type]
82
+
83
+ # Merge default config with provided config
84
+ final_config = self._DEFAULT_CONFIGS.get(provider_type, {}).copy()
85
+ if config:
86
+ final_config.update(config)
87
+
88
+ logger.info(f"Creating {provider_type.value} translation provider")
89
+ logger.debug(f"Provider config: {final_config}")
90
+
91
+ # Create provider instance
92
+ provider = provider_class(**final_config)
93
+
94
+ # Cache the provider if requested
95
+ if use_cache:
96
+ self._provider_cache[cache_key] = provider
97
+
98
+ logger.info(f"Successfully created {provider_type.value} translation provider")
99
+ return provider
100
+
101
+ except Exception as e:
102
+ logger.error(f"Failed to create {provider_type.value} provider: {str(e)}")
103
+ raise TranslationFailedException(
104
+ f"Failed to create {provider_type.value} provider: {str(e)}"
105
+ ) from e
106
+
107
+ def get_available_providers(self, force_check: bool = False) -> List[TranslationProviderType]:
108
+ """
109
+ Get list of available translation providers.
110
+
111
+ Args:
112
+ force_check: Whether to force availability check (ignore cache)
113
+
114
+ Returns:
115
+ List[TranslationProviderType]: List of available provider types
116
+ """
117
+ available_providers = []
118
+
119
+ for provider_type in self._PROVIDER_REGISTRY.keys():
120
+ if self._is_provider_available(provider_type, force_check):
121
+ available_providers.append(provider_type)
122
+
123
+ logger.info(f"Available translation providers: {[p.value for p in available_providers]}")
124
+ return available_providers
125
+
126
+ def get_default_provider(self, config: Optional[Dict] = None) -> TranslationProviderBase:
127
+ """
128
+ Get the default translation provider.
129
+
130
+ Args:
131
+ config: Optional configuration for the provider
132
+
133
+ Returns:
134
+ TranslationProviderBase: The default provider instance
135
+
136
+ Raises:
137
+ TranslationFailedException: If no providers are available
138
+ """
139
+ available_providers = self.get_available_providers()
140
+
141
+ if not available_providers:
142
+ raise TranslationFailedException("No translation providers are available")
143
+
144
+ # Use NLLB as default if available, otherwise use the first available
145
+ default_type = TranslationProviderType.NLLB
146
+ if default_type not in available_providers:
147
+ default_type = available_providers[0]
148
+
149
+ logger.info(f"Using {default_type.value} as default translation provider")
150
+ return self.create_provider(default_type, config)
151
+
152
+ def get_provider_with_fallback(
153
+ self,
154
+ preferred_types: List[TranslationProviderType],
155
+ config: Optional[Dict] = None
156
+ ) -> TranslationProviderBase:
157
+ """
158
+ Get a provider with fallback options.
159
+
160
+ Args:
161
+ preferred_types: List of preferred provider types in order of preference
162
+ config: Optional configuration for the provider
163
+
164
+ Returns:
165
+ TranslationProviderBase: The first available provider from the list
166
+
167
+ Raises:
168
+ TranslationFailedException: If none of the preferred providers are available
169
+ """
170
+ available_providers = self.get_available_providers()
171
+
172
+ for provider_type in preferred_types:
173
+ if provider_type in available_providers:
174
+ logger.info(f"Using {provider_type.value} translation provider")
175
+ return self.create_provider(provider_type, config)
176
+
177
+ # If no preferred providers are available, try any available provider
178
+ if available_providers:
179
+ fallback_type = available_providers[0]
180
+ logger.warning(
181
+ f"None of preferred providers {[p.value for p in preferred_types]} available. "
182
+ f"Falling back to {fallback_type.value}"
183
+ )
184
+ return self.create_provider(fallback_type, config)
185
+
186
+ raise TranslationFailedException(
187
+ f"None of the preferred translation providers are available: "
188
+ f"{[p.value for p in preferred_types]}"
189
+ )
190
+
191
+ def clear_cache(self) -> None:
192
+ """Clear all cached provider instances."""
193
+ self._provider_cache.clear()
194
+ self._availability_cache.clear()
195
+ logger.info("Translation provider cache cleared")
196
+
197
+ def get_provider_info(self, provider_type: TranslationProviderType) -> Dict:
198
+ """
199
+ Get information about a specific provider type.
200
+
201
+ Args:
202
+ provider_type: The provider type to get info for
203
+
204
+ Returns:
205
+ dict: Provider information
206
+ """
207
+ if provider_type not in self._PROVIDER_REGISTRY:
208
+ raise TranslationFailedException(f"Unknown provider type: {provider_type.value}")
209
+
210
+ provider_class = self._PROVIDER_REGISTRY[provider_type]
211
+ default_config = self._DEFAULT_CONFIGS.get(provider_type, {})
212
+ is_available = self._is_provider_available(provider_type)
213
+
214
+ return {
215
+ 'type': provider_type.value,
216
+ 'class_name': provider_class.__name__,
217
+ 'module': provider_class.__module__,
218
+ 'available': is_available,
219
+ 'default_config': default_config,
220
+ 'description': provider_class.__doc__ or "No description available"
221
+ }
222
+
223
+ def get_all_providers_info(self) -> Dict[str, Dict]:
224
+ """
225
+ Get information about all registered providers.
226
+
227
+ Returns:
228
+ dict: Information about all providers
229
+ """
230
+ providers_info = {}
231
+ for provider_type in self._PROVIDER_REGISTRY.keys():
232
+ providers_info[provider_type.value] = self.get_provider_info(provider_type)
233
+
234
+ return providers_info
235
+
236
+ def _is_provider_available(self, provider_type: TranslationProviderType, force_check: bool = False) -> bool:
237
+ """
238
+ Check if a provider type is available.
239
+
240
+ Args:
241
+ provider_type: The provider type to check
242
+ force_check: Whether to force availability check (ignore cache)
243
+
244
+ Returns:
245
+ bool: True if provider is available, False otherwise
246
+ """
247
+ # Return cached result if available and not forcing check
248
+ if not force_check and provider_type in self._availability_cache:
249
+ return self._availability_cache[provider_type]
250
+
251
+ try:
252
+ # Create a temporary instance to check availability
253
+ provider_class = self._PROVIDER_REGISTRY[provider_type]
254
+ default_config = self._DEFAULT_CONFIGS.get(provider_type, {})
255
+ temp_provider = provider_class(**default_config)
256
+ is_available = temp_provider.is_available()
257
+
258
+ # Cache the result
259
+ self._availability_cache[provider_type] = is_available
260
+
261
+ logger.debug(f"Provider {provider_type.value} availability: {is_available}")
262
+ return is_available
263
+
264
+ except Exception as e:
265
+ logger.warning(f"Error checking {provider_type.value} availability: {str(e)}")
266
+ self._availability_cache[provider_type] = False
267
+ return False
268
+
269
+ def _generate_cache_key(self, provider_type: TranslationProviderType, config: Optional[Dict]) -> str:
270
+ """
271
+ Generate a cache key for provider instances.
272
+
273
+ Args:
274
+ provider_type: The provider type
275
+ config: The provider configuration
276
+
277
+ Returns:
278
+ str: Cache key
279
+ """
280
+ config_str = ""
281
+ if config:
282
+ # Sort config items for consistent key generation
283
+ sorted_config = sorted(config.items())
284
+ config_str = "_".join(f"{k}={v}" for k, v in sorted_config)
285
+
286
+ return f"{provider_type.value}_{config_str}"
287
+
288
+ @classmethod
289
+ def register_provider(
290
+ cls,
291
+ provider_type: TranslationProviderType,
292
+ provider_class: Type[TranslationProviderBase],
293
+ default_config: Optional[Dict] = None
294
+ ) -> None:
295
+ """
296
+ Register a new translation provider type.
297
+
298
+ Args:
299
+ provider_type: The provider type enum
300
+ provider_class: The provider class
301
+ default_config: Default configuration for the provider
302
+ """
303
+ cls._PROVIDER_REGISTRY[provider_type] = provider_class
304
+ if default_config:
305
+ cls._DEFAULT_CONFIGS[provider_type] = default_config
306
+
307
+ logger.info(f"Registered translation provider: {provider_type.value}")
308
+
309
+ @classmethod
310
+ def get_supported_provider_types(cls) -> List[TranslationProviderType]:
311
+ """
312
+ Get all supported provider types.
313
+
314
+ Returns:
315
+ List[TranslationProviderType]: List of supported provider types
316
+ """
317
+ return list(cls._PROVIDER_REGISTRY.keys())
318
+
319
+
320
+ # Global factory instance for convenience
321
+ translation_provider_factory = TranslationProviderFactory()
322
+
323
+
324
+ def create_translation_provider(
325
+ provider_type: TranslationProviderType = TranslationProviderType.NLLB,
326
+ config: Optional[Dict] = None
327
+ ) -> TranslationProviderBase:
328
+ """
329
+ Convenience function to create a translation provider.
330
+
331
+ Args:
332
+ provider_type: The type of provider to create
333
+ config: Optional configuration parameters
334
+
335
+ Returns:
336
+ TranslationProviderBase: The created provider instance
337
+ """
338
+ return translation_provider_factory.create_provider(provider_type, config)
339
+
340
+
341
+ def get_default_translation_provider(config: Optional[Dict] = None) -> TranslationProviderBase:
342
+ """
343
+ Convenience function to get the default translation provider.
344
+
345
+ Args:
346
+ config: Optional configuration parameters
347
+
348
+ Returns:
349
+ TranslationProviderBase: The default provider instance
350
+ """
351
+ return translation_provider_factory.get_default_provider(config)
test_translation_migration.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Test script to verify the translation service migration."""
3
+
4
+ import sys
5
+ import os
6
+
7
+ # Add src to path
8
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
9
+
10
+ def test_translation_provider_creation():
11
+ """Test that we can create translation providers."""
12
+ print("Testing translation provider creation...")
13
+
14
+ try:
15
+ from infrastructure.translation.provider_factory import (
16
+ TranslationProviderFactory,
17
+ TranslationProviderType
18
+ )
19
+
20
+ factory = TranslationProviderFactory()
21
+
22
+ # Test getting available providers
23
+ available = factory.get_available_providers()
24
+ print(f"Available providers: {[p.value for p in available]}")
25
+
26
+ # Test provider info
27
+ all_info = factory.get_all_providers_info()
28
+ print(f"Provider info: {all_info}")
29
+
30
+ # Test creating NLLB provider (may fail if transformers not installed)
31
+ try:
32
+ provider = factory.create_provider(TranslationProviderType.NLLB)
33
+ print(f"Created provider: {provider.provider_name}")
34
+ print(f"Provider available: {provider.is_available()}")
35
+
36
+ # Test supported languages
37
+ supported = provider.get_supported_languages()
38
+ print(f"Supported language pairs: {len(supported)} source languages")
39
+
40
+ if 'en' in supported:
41
+ print(f"English can translate to: {len(supported['en'])} languages")
42
+
43
+ except Exception as e:
44
+ print(f"Could not create NLLB provider (expected if transformers not installed): {e}")
45
+
46
+ print("βœ“ Translation provider creation test passed")
47
+ return True
48
+
49
+ except Exception as e:
50
+ print(f"βœ— Translation provider creation test failed: {e}")
51
+ return False
52
+
53
+ def test_domain_models():
54
+ """Test that domain models work correctly."""
55
+ print("\nTesting domain models...")
56
+
57
+ try:
58
+ from domain.models.text_content import TextContent
59
+ from domain.models.translation_request import TranslationRequest
60
+
61
+ # Test TextContent creation
62
+ text_content = TextContent(
63
+ text="Hello, world!",
64
+ language="en",
65
+ encoding="utf-8"
66
+ )
67
+ print(f"Created TextContent: {text_content.text} ({text_content.language})")
68
+
69
+ # Test TranslationRequest creation
70
+ translation_request = TranslationRequest(
71
+ source_text=text_content,
72
+ target_language="zh"
73
+ )
74
+ print(f"Created TranslationRequest: {translation_request.effective_source_language} -> {translation_request.target_language}")
75
+
76
+ print("βœ“ Domain models test passed")
77
+ return True
78
+
79
+ except Exception as e:
80
+ print(f"βœ— Domain models test failed: {e}")
81
+ return False
82
+
83
+ def test_base_class_functionality():
84
+ """Test the base class functionality."""
85
+ print("\nTesting base class functionality...")
86
+
87
+ try:
88
+ from infrastructure.base.translation_provider_base import TranslationProviderBase
89
+ from domain.models.text_content import TextContent
90
+ from domain.models.translation_request import TranslationRequest
91
+
92
+ # Create a mock provider for testing
93
+ class MockTranslationProvider(TranslationProviderBase):
94
+ def __init__(self):
95
+ super().__init__("MockProvider", {"en": ["zh", "es", "fr"]})
96
+
97
+ def _translate_chunk(self, text: str, source_language: str, target_language: str) -> str:
98
+ return f"[TRANSLATED:{source_language}->{target_language}]{text}"
99
+
100
+ def is_available(self) -> bool:
101
+ return True
102
+
103
+ def get_supported_languages(self) -> dict:
104
+ return self.supported_languages
105
+
106
+ # Test the mock provider
107
+ provider = MockTranslationProvider()
108
+
109
+ # Test text chunking
110
+ long_text = "This is a test sentence. " * 50 # Create long text
111
+ chunks = provider._chunk_text(long_text)
112
+ print(f"Text chunked into {len(chunks)} pieces")
113
+
114
+ # Test translation request
115
+ text_content = TextContent(text="Hello world", language="en")
116
+ request = TranslationRequest(source_text=text_content, target_language="zh")
117
+
118
+ result = provider.translate(request)
119
+ print(f"Translation result: {result.text}")
120
+
121
+ print("βœ“ Base class functionality test passed")
122
+ return True
123
+
124
+ except Exception as e:
125
+ print(f"βœ— Base class functionality test failed: {e}")
126
+ return False
127
+
128
+ def main():
129
+ """Run all tests."""
130
+ print("Running translation service migration tests...\n")
131
+
132
+ tests = [
133
+ test_domain_models,
134
+ test_translation_provider_creation,
135
+ test_base_class_functionality
136
+ ]
137
+
138
+ passed = 0
139
+ total = len(tests)
140
+
141
+ for test in tests:
142
+ if test():
143
+ passed += 1
144
+
145
+ print(f"\n{'='*50}")
146
+ print(f"Test Results: {passed}/{total} tests passed")
147
+
148
+ if passed == total:
149
+ print("πŸŽ‰ All tests passed! Translation service migration is working correctly.")
150
+ return 0
151
+ else:
152
+ print("❌ Some tests failed. Please check the implementation.")
153
+ return 1
154
+
155
+ if __name__ == "__main__":
156
+ sys.exit(main())