Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class ElementButton
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Url { get; set; }

[Translate]
public string Title { get; set; } = string.Empty;

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace BotSharp.Abstraction.Translation.Attributes;

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false)]
public class TranslateAttribute : Attribute
{
public TranslateAttribute()
{

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace BotSharp.Abstraction.Translation;

public interface ITranslationService
{
T Translate<T>(T data, string language = "Spanish", bool clone = true) where T : class;
}
1 change: 1 addition & 0 deletions src/Infrastructure/BotSharp.Abstraction/Using.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
global using BotSharp.Abstraction.Routing.Models;
global using BotSharp.Abstraction.Routing.Planning;
global using BotSharp.Abstraction.Templating;
global using BotSharp.Abstraction.Translation.Attributes;
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using BotSharp.Core.Messaging;
using BotSharp.Core.Routing.Planning;
using BotSharp.Core.Templating;
using BotSharp.Core.Translation;
using Microsoft.Extensions.Configuration;

namespace BotSharp.Core.Conversations;
Expand Down Expand Up @@ -35,6 +36,7 @@ public void RegisterDI(IServiceCollection services, IConfiguration config)
services.AddScoped<IConversationService, ConversationService>();
services.AddScoped<IConversationStateService, ConversationStateService>();
services.AddScoped<IConversationAttachmentService, ConversationAttachmentService>();
services.AddScoped<ITranslationService, TranslationService>();

// Rich content messaging
services.AddScoped<IRichContentService, RichContentService>();
Expand Down
265 changes: 265 additions & 0 deletions src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
using BotSharp.Abstraction.Options;
using BotSharp.Abstraction.Translation.Attributes;
using Newtonsoft.Json;
using System.Reflection;

namespace BotSharp.Core.Translation;

public class TranslationService : ITranslationService
{
private readonly IServiceProvider _services;
private readonly ILogger<TranslationService> _logger;
private readonly BotSharpOptions _options;

public TranslationService(IServiceProvider services,
ILogger<TranslationService> logger,
BotSharpOptions options)
{
_services = services;
_logger = logger;
_options = options;
}

public T Translate<T>(T data, string language = "Spanish", bool clone = true) where T : class
{
var cloned = data;
if (clone)
{
cloned = Clone(data);
}

var unique = new HashSet<string>();
Collect(cloned, ref unique);
var map = InnerTranslate(unique, language);
cloned = Assign(cloned, map);
return cloned;
}

private T Clone<T>(T data) where T : class
{
if (data == null) return data;

var str = System.Text.Json.JsonSerializer.Serialize(data, _options.JsonSerializerOptions);
var cloned = System.Text.Json.JsonSerializer.Deserialize<T>(str, _options.JsonSerializerOptions);
return cloned;
}

/// <summary>
/// Collect unique strings in data
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <param name="res"></param>
private void Collect<T>(T data, ref HashSet<string> res) where T : class
{
if (data == null) return;

var dataType = data.GetType();
if (dataType == typeof(string))
{
res.Add(data.ToString());
return;
}

var interfaces = dataType.GetTypeInfo().ImplementedInterfaces;
if (interfaces.Any(x => x.Name == typeof(IDictionary<,>).Name))
{
return;
}

var isList = interfaces.Any(x => x.Name == typeof(IEnumerable<>).Name);
if (dataType.IsArray || isList)
{
var elementType = dataType.IsArray ? dataType.GetElementType() : dataType.GetGenericArguments().FirstOrDefault();
if (elementType == typeof(string))
{
foreach (var item in (data as IEnumerable<string>))
{
if (item == null) continue;
res.Add(item);
}
}
else if (elementType != null && (elementType.IsClass || elementType.IsInterface))
{
foreach (var item in (data as IEnumerable<object>))
{
if (item == null) continue;
Collect(item, ref res);
}
}
return;
}


var props = dataType.GetProperties();
foreach (var prop in props)
{
var value = prop.GetValue(data, null);
var propType = prop.PropertyType;
var translate = prop.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(TranslateAttribute));

if (value == null) continue;

if (propType == typeof(string))
{
if (translate != null)
{
Collect(value, ref res);
}
}
else if (propType.IsClass || propType.IsInterface)
{
interfaces = propType.GetTypeInfo().ImplementedInterfaces;
isList = interfaces.Any(x => x.Name == typeof(IEnumerable<>).Name);
if (interfaces.Any(x => x.Name == typeof(IDictionary<,>).Name))
{
Collect(value, ref res);
}
else if (propType.IsArray || isList)
{
var elementType = propType.IsArray ? propType.GetElementType() : propType.GetGenericArguments().FirstOrDefault();
if (elementType == typeof(string))
{
if (translate != null)
{
Collect(value, ref res);
}
}
else if (elementType != null && (elementType.IsClass || elementType.IsInterface))
{
Collect(value, ref res);
}
}
else
{
Collect(value, ref res);
}
}
}
}

/// <summary>
/// Assign translated values to corresponding attributes
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <param name="map"></param>
/// <returns></returns>
private T Assign<T>(T data, Dictionary<string, string> map) where T : class
{
if (data == null) return data;

var dataType = data.GetType();
if (dataType == typeof(string) && map.TryGetValue(data.ToString(), out var target))
{
return target as T;
}

var interfaces = dataType.GetTypeInfo().ImplementedInterfaces;
if (interfaces.Any(x => x.Name == typeof(IDictionary<,>).Name))
{
return data;
}

var isList = interfaces.Any(x => x.Name == typeof(IEnumerable<>).Name);
if (dataType.IsArray || isList)
{
var elementType = dataType.IsArray ? dataType.GetElementType() : dataType.GetGenericArguments().FirstOrDefault();
if (elementType == typeof(string))
{
var list = new List<string>();
foreach (var item in (data as IEnumerable<string>))
{
if (map.TryGetValue(item, out target))
{
list.Add(target);
}
else
{
list.Add(item?.ToString());
}
}

data = dataType.IsArray ? list.ToArray() as T : list as T;
}
else if (elementType != null && (elementType.IsClass || elementType.IsInterface))
{
foreach (var item in (data as IEnumerable<object>))
{
if (item == null) continue;
Assign(item, map);
}
}
return data;
}


var props = dataType.GetProperties();
foreach (var prop in props)
{
var value = prop.GetValue(data, null);
var propType = prop.PropertyType;
var translate = prop.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(TranslateAttribute));

if (value == null) continue;

if (propType == typeof(string))
{
if (translate != null)
{
prop.SetValue(data, Assign(value, map));
}
}
else if (propType.IsClass || propType.IsInterface)
{
interfaces = propType.GetTypeInfo().ImplementedInterfaces;
isList = interfaces.Any(x => x.Name == typeof(IEnumerable<>).Name);
if (interfaces.Any(x => x.Name == typeof(IDictionary<,>).Name))
{
Assign(value, map);
}
else if (propType.IsArray || isList)
{
var elementType = propType.IsArray ? propType.GetElementType() : propType.GetGenericArguments().FirstOrDefault();
if (elementType == typeof(string))
{
if (translate != null)
{
var targetValue = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(Assign(value, map)), propType);
prop.SetValue(data, targetValue);
}
}
else if (elementType != null && (elementType.IsClass || elementType.IsInterface))
{
prop.SetValue(data, Assign(value, map));
}
}
else
{
Assign(value, map);
}
}
}

return data;
}

/// <summary>
/// Translate
/// </summary>
/// <param name="list"></param>
/// <param name="language"></param>
/// <returns></returns>
private Dictionary<string, string> InnerTranslate(HashSet<string> list, string language)
{
var map = new Dictionary<string, string>();
if (list == null || !list.Any()) return map;

foreach (var item in list)
{
map.Add(item, "hello world");
}

return map;
}
}
1 change: 1 addition & 0 deletions src/Infrastructure/BotSharp.Core/Using.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
global using BotSharp.Abstraction.Functions.Models;
global using BotSharp.Abstraction.Repositories;
global using BotSharp.Abstraction.Repositories.Filters;
global using BotSharp.Abstraction.Translation;
global using BotSharp.Core.Repository;
global using BotSharp.Core.Routing;
global using BotSharp.Core.Agents.Services;
Expand Down