Skip to content

Commit 2ec411b

Browse files
committed
#7388 Automated translation of content in multiple languages
1 parent bb04ce3 commit 2ec411b

47 files changed

Lines changed: 1062 additions & 18 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace Nop.Core.Domain.Translation;
2+
3+
/// <summary>
4+
/// Represents a translation service type
5+
/// </summary>
6+
public enum TranslationServiceType
7+
{
8+
/// <summary>
9+
/// Google cloud translate API
10+
/// </summary>
11+
GoogleTranslate,
12+
/// <summary>
13+
/// DeepL API
14+
/// </summary>
15+
DeepL
16+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Nop.Core.Configuration;
2+
3+
namespace Nop.Core.Domain.Translation;
4+
5+
/// <summary>
6+
/// Translation settings
7+
/// </summary>
8+
public partial class TranslationSettings : ISettings
9+
{
10+
/// <summary>
11+
/// Gets or sets a value indicating whether pre-translation is allowed
12+
/// </summary>
13+
public bool AllowPreTranslate { get; set; }
14+
15+
/// <summary>
16+
/// Gets or sets a language to translate from
17+
/// </summary>
18+
public int TranslateFromLanguageId { get; set; }
19+
20+
/// <summary>
21+
/// Gets or sets a list of languages which is not allowed to pre-translate
22+
/// </summary>
23+
public List<int> NotTranslateLanguages { get; set; } = new();
24+
25+
/// <summary>
26+
/// Gets or sets the Google Translate API key
27+
/// </summary>
28+
public string GoogleApiKey { get; set; }
29+
30+
/// <summary>
31+
/// Gets or sets the DeepL Auth key
32+
/// </summary>
33+
public string DeepLAuthKey { get; set; }
34+
35+
/// <summary>
36+
/// Gets or sets a translation service type id
37+
/// </summary>
38+
public int TranslationServiceId { get; set; }
39+
}

src/Libraries/Nop.Services/Installation/InstallRequiredData.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
using Nop.Core.Domain.Stores;
2929
using Nop.Core.Domain.Tax;
3030
using Nop.Core.Domain.Topics;
31+
using Nop.Core.Domain.Translation;
3132
using Nop.Core.Domain.Vendors;
3233
using Nop.Core.Http;
3334
using Nop.Core.Security;
@@ -1475,6 +1476,16 @@ protected virtual async Task InstallSettingsAsync()
14751476
IgnoreRtlPropertyForAdminArea = false
14761477
});
14771478

1479+
await SaveSettingAsync(dictionary, new TranslationSettings
1480+
{
1481+
TranslateFromLanguageId = (await Table<Language>().FirstAsync()).Id,
1482+
AllowPreTranslate = false,
1483+
GoogleApiKey = string.Empty,
1484+
NotTranslateLanguages = new List<int>(),
1485+
DeepLAuthKey = string.Empty,
1486+
TranslationServiceId = (int)TranslationServiceType.GoogleTranslate
1487+
});
1488+
14781489
await SaveSettingAsync(dictionary, new CustomerSettings
14791490
{
14801491
UsernamesEnabled = false,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using Nop.Web.Framework.Models;
2+
using Nop.Web.Framework.Models.Translation;
3+
4+
namespace Nop.Web.Framework.Factories;
5+
6+
/// <summary>
7+
/// Represents translation model factory
8+
/// </summary>
9+
public partial interface ITranslationModelFactory
10+
{
11+
/// <summary>
12+
/// Prepare translation model by the passed localized model
13+
/// </summary>
14+
/// <typeparam name="T">Localized model type</typeparam>
15+
/// <param name="model">The localized model to translate</param>
16+
/// <param name="propertiesToTranslate">List of properties which should be translated</param>
17+
/// <returns>
18+
/// A task that represents the asynchronous operation
19+
/// The task result contains the translation model
20+
/// </returns>
21+
Task<TranslationModel> PrepareTranslationModelAsync<T>(ILocalizedModel<T> model,
22+
params string[] propertiesToTranslate)
23+
where T : ILocalizedLocaleModel;
24+
25+
/// <summary>
26+
/// Prepare translation model by the passed localized model
27+
/// </summary>
28+
/// <typeparam name="T">Localized model type</typeparam>
29+
/// <param name="model">The localized model to translate</param>
30+
/// <param name="propertiesToTranslate">List of properties which should be translated</param>
31+
/// <returns>
32+
/// A task that represents the asynchronous operation
33+
/// The task result contains the translation model
34+
/// </returns>
35+
Task<TranslationModel> PrepareTranslationModelAsync<T>(ILocalizedModel<T> model,
36+
params (string PropertyName, bool IsHtml)[] propertiesToTranslate)
37+
where T : ILocalizedLocaleModel;
38+
}
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
using System.Reflection;
2+
using DeepL;
3+
using Nop.Core.Domain.Localization;
4+
using Nop.Core.Domain.Translation;
5+
using Nop.Services.Localization;
6+
using Nop.Services.Logging;
7+
using Nop.Web.Framework.Models;
8+
using Nop.Web.Framework.Models.Translation;
9+
10+
namespace Nop.Web.Framework.Factories;
11+
12+
/// <summary>
13+
/// Represents translation model factory implementation
14+
/// </summary>
15+
public partial class TranslationModelFactory : ITranslationModelFactory
16+
{
17+
#region Fields
18+
19+
protected readonly ILanguageService _languageService;
20+
protected readonly ILocalizationService _localizationService;
21+
protected readonly ILogger _logger;
22+
protected TranslationClientHelper _translationClientHelper;
23+
protected readonly TranslationSettings _translationSettings;
24+
25+
#endregion
26+
27+
#region Ctor
28+
29+
public TranslationModelFactory(ILanguageService languageService,
30+
ILocalizationService localizationService,
31+
ILogger logger,
32+
TranslationSettings translationSettings)
33+
{
34+
_languageService = languageService;
35+
_localizationService = localizationService;
36+
_logger = logger;
37+
_translationClientHelper = null;
38+
_translationSettings = translationSettings;
39+
}
40+
41+
#endregion
42+
43+
#region Methods
44+
45+
/// <summary>
46+
/// Prepare translation model by the passed localized model
47+
/// </summary>
48+
/// <typeparam name="T">Localized model type</typeparam>
49+
/// <param name="model">The localized model to translate</param>
50+
/// <param name="propertiesToTranslate">List of properties which should be translated</param>
51+
/// <returns>
52+
/// A task that represents the asynchronous operation
53+
/// The task result contains the translation model
54+
/// </returns>
55+
public virtual async Task<TranslationModel> PrepareTranslationModelAsync<T>(ILocalizedModel<T> model,
56+
params string[] propertiesToTranslate)
57+
where T : ILocalizedLocaleModel
58+
{
59+
return await PrepareTranslationModelAsync(model, propertiesToTranslate.Select(p => (p, false)).ToArray());
60+
}
61+
62+
/// <summary>
63+
/// Prepare translation model by the passed localized model
64+
/// </summary>
65+
/// <typeparam name="T">Localized model type</typeparam>
66+
/// <param name="model">The localized model to translate</param>
67+
/// <param name="propertiesToTranslate">List of properties which should be translated</param>
68+
/// <returns>
69+
/// A task that represents the asynchronous operation
70+
/// The task result contains the translation model
71+
/// </returns>
72+
public virtual async Task<TranslationModel> PrepareTranslationModelAsync<T>(ILocalizedModel<T> model,
73+
params (string PropertyName, bool IsHtml)[] propertiesToTranslate)
74+
where T : ILocalizedLocaleModel
75+
{
76+
_translationClientHelper ??= new TranslationClientHelper(_translationSettings);
77+
78+
var result = new TranslationModel();
79+
80+
var properties = propertiesToTranslate.Select(p => new KeyValuePair<string, bool>(p.PropertyName, p.IsHtml)).ToDictionary();
81+
82+
//get model properties to use as original text for translation
83+
var modelProperties = model.GetType().GetProperties()
84+
.Where(propertyFilter)
85+
.ToList();
86+
87+
//get properties to save translated text
88+
var localizedProperties = typeof(T).GetProperties()
89+
.Where(propertyFilter)
90+
.ToList();
91+
92+
//get original language
93+
var originalLanguage = await _languageService.GetLanguageByIdAsync(_translationSettings.TranslateFromLanguageId);
94+
var position = 0;
95+
96+
//the loop by locales which should be translated
97+
foreach (var modelLocale in model.Locales)
98+
{
99+
position++;
100+
101+
//we ignore the original language
102+
if (modelLocale.LanguageId == _translationSettings.TranslateFromLanguageId)
103+
continue;
104+
105+
//and languages which should be ignored
106+
if (_translationSettings.NotTranslateLanguages.Contains(modelLocale.LanguageId))
107+
continue;
108+
109+
//get target languages to translation for
110+
var translateToLanguage = await _languageService.GetLanguageByIdAsync(modelLocale.LanguageId);
111+
112+
//the loop by properties which should be translated
113+
foreach (var prop in localizedProperties)
114+
{
115+
//get the current value of property
116+
var currentTranslatedText = prop.GetValue(modelLocale, null)?.ToString();
117+
118+
//ignore the property which already has a value
119+
if (!string.IsNullOrEmpty(currentTranslatedText))
120+
continue;
121+
122+
//get original text to translate
123+
var originText = modelProperties.FirstOrDefault(p => p.Name.Equals(prop.Name))?.GetValue(model, null)?.ToString();
124+
125+
//ignore the empty original text
126+
if (string.IsNullOrEmpty(originText))
127+
continue;
128+
129+
try
130+
{
131+
var translatedText = await _translationClientHelper.TranslateAsync(originalLanguage, originText, translateToLanguage, properties[prop.Name]);
132+
if (string.IsNullOrEmpty(translatedText))
133+
continue;
134+
135+
//set translated text to property
136+
prop.SetValue(modelLocale, translatedText);
137+
var inputName = $"{nameof(model.Locales)}_{position - 1}__{prop.Name}";
138+
result.Translations.Add(new()
139+
{
140+
Name = inputName,
141+
Value = translatedText,
142+
OriginValue = originText,
143+
Language = translateToLanguage.UniqueSeoCode,
144+
OriginLanguage = originalLanguage.UniqueSeoCode
145+
});
146+
}
147+
catch (Exception e)
148+
{
149+
var serviceName = await _localizationService.GetLocalizedEnumAsync((TranslationServiceType)_translationSettings.TranslationServiceId);
150+
var errorMessage = $"{serviceName}: {e.Message}";
151+
await _logger.ErrorAsync(errorMessage, e);
152+
result.HasErrors = true;
153+
154+
//DeepL: stop translate if one of the languages aren't support
155+
//to reduce error count
156+
if (e.Message.Contains("Value for 'target_lang' not supported", StringComparison.InvariantCultureIgnoreCase) || e.Message.Contains("Value for 'source_lang' not supported", StringComparison.InvariantCultureIgnoreCase))
157+
break;
158+
}
159+
}
160+
}
161+
162+
return result;
163+
164+
//filter for get only string property which should be translated
165+
bool propertyFilter(PropertyInfo propertyInfo)
166+
{
167+
return propertyInfo.PropertyType == typeof(string) && properties.ContainsKey(propertyInfo.Name);
168+
}
169+
}
170+
171+
#endregion
172+
173+
#region Nested class
174+
175+
/// <summary>
176+
/// Helper class to translation client
177+
/// </summary>
178+
protected class TranslationClientHelper
179+
{
180+
#region Fields
181+
182+
protected readonly Google.Cloud.Translation.V2.TranslationClient _googleClient;
183+
protected readonly DeepLClient _deeplClient;
184+
185+
#endregion
186+
187+
#region Ctor
188+
189+
public TranslationClientHelper(TranslationSettings translationSettings)
190+
{
191+
switch ((TranslationServiceType)translationSettings.TranslationServiceId)
192+
{
193+
case TranslationServiceType.GoogleTranslate:
194+
{
195+
if (!string.IsNullOrEmpty(translationSettings.GoogleApiKey))
196+
_googleClient = Google.Cloud.Translation.V2.TranslationClient.CreateFromApiKey(translationSettings.GoogleApiKey);
197+
_deeplClient = null;
198+
}
199+
break;
200+
201+
case TranslationServiceType.DeepL:
202+
{
203+
if (!string.IsNullOrEmpty(translationSettings.DeepLAuthKey))
204+
_deeplClient = new DeepLClient(translationSettings.DeepLAuthKey);
205+
_googleClient = null;
206+
}
207+
break;
208+
}
209+
}
210+
211+
#endregion
212+
213+
#region Methods
214+
215+
/// <summary>
216+
/// Translate text or html
217+
/// </summary>
218+
/// <param name="originalLanguage">The language to translate from</param>
219+
/// <param name="originText">The text or HTML to translate</param>
220+
/// <param name="targetLanguage">The target language to translate</param>
221+
/// <param name="isHtml">Indicate whether the text to translate should be considered as HTML</param>
222+
/// <returns>
223+
/// A task that represents the asynchronous operation
224+
/// The task result contains the translated text
225+
/// </returns>
226+
public virtual async Task<string> TranslateAsync(Language originalLanguage, string originText, Language targetLanguage, bool isHtml)
227+
{
228+
if (_googleClient != null)
229+
{
230+
var response = isHtml
231+
? await _googleClient.TranslateHtmlAsync(originText, targetLanguage.UniqueSeoCode, originalLanguage.UniqueSeoCode)
232+
: await _googleClient.TranslateTextAsync(originText, targetLanguage.UniqueSeoCode, originalLanguage.UniqueSeoCode);
233+
234+
return response.TranslatedText;
235+
}
236+
237+
if (_deeplClient != null)
238+
{
239+
var response = await _deeplClient.TranslateTextAsync(originText, originalLanguage.UniqueSeoCode, targetLanguage.UniqueSeoCode, new TextTranslateOptions
240+
{
241+
TagHandling = isHtml ? "html" : null
242+
});
243+
244+
return response.Text;
245+
}
246+
247+
return string.Empty;
248+
}
249+
250+
#endregion
251+
}
252+
253+
#endregion
254+
}

0 commit comments

Comments
 (0)