Skip to content

Instantly share code, notes, and snippets.

@KevinJump
Created July 29, 2025 14:21
Show Gist options
  • Save KevinJump/884ab7d32a1cba1181870278b300ad6a to your computer and use it in GitHub Desktop.
Save KevinJump/884ab7d32a1cba1181870278b300ad6a to your computer and use it in GitHub Desktop.
Translation Manager : Limbo Tables property mapper
using Jumoo.TranslationManager.Core;
using Jumoo.TranslationManager.Core.Extensions;
using Jumoo.TranslationManager.Core.Models;
using Jumoo.TranslationManager.Core.ValueMappers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Cms.Core.Services;
namespace TranslationManager.Site.Thirteen.Code;
/// <summary>
/// Provides a value mapper for the Limbo tables property editor, enabling translation of table cell content.
/// </summary>
/// <remarks>
/// This property editor stores its content as a JSON object with an array of 'cells'.
/// Each cell contains row and column indices, a value (using a Rich Text Editor), and additional metadata.
/// The value in each cell is mapped using the TinyMCE value mapper to support translation of rich text content.
/// </remarks>
public class LimboTablesMapper : ValueMapperOptionsBase, IValueMapper
{
private readonly Lazy<ValueMapperCollection> _mappers;
/// <summary>
/// Initializes a new instance of the <see cref="LimboTablesMapper"/> class.
/// </summary>
/// <param name="contentService">The content service.</param>
/// <param name="dataTypeService">The data type service.</param>
/// <param name="contentTypeService">The content type service.</param>
/// <param name="logger">The logger instance.</param>
/// <param name="mappers">A lazy-loaded collection of value mappers.</param>
public LimboTablesMapper(
IContentService contentService,
IDataTypeService dataTypeService,
IContentTypeService contentTypeService,
ILogger<ValueMapperOptionsBase> logger,
Lazy<ValueMapperCollection> mappers)
: base(contentService, dataTypeService, contentTypeService, logger)
{
_mappers = mappers;
}
/// <summary>
/// Gets the name of the value mapper.
/// </summary>
public string Name => "Limbo Tables Mapper";
/// <summary>
/// Gets the list of property editor aliases this mapper supports.
/// </summary>
public override string[] Editors => ["Limbo.Umbraco.Tables"];
/// <summary>
/// Extracts the source value for translation from the Limbo tables property editor.
/// </summary>
/// <param name="options">The source options containing property and value information.</param>
/// <returns>
/// A <see cref="TranslationValue"/> representing the table's translatable content,
/// or <c>null</c> if the value is not valid or cannot be parsed.
/// </returns>
public override TranslationValue? GetSourceValue(PropertyValueMapperSourceOptions options)
{
var attempt = options.SourceValue.TryConvertTo<string>();
if (!attempt.Success || attempt.Result is null) return null;
var jsonValue = JsonConvert.DeserializeObject<JObject>(attempt.Result);
if (jsonValue is null) return null;
if (jsonValue.ContainsKey("cells") is false) return null;
if (jsonValue["cells"] is not JArray data) return null;
var translationValue = new TranslationValue(options.DisplayName, options.PropertyTypeAlias, options.Path);
foreach (var row in data)
{
// data is an array of rows, each row is an array of cells
foreach (var cell in row.Cast<JObject>())
{
// we use the rowIndex and columnIndex to identify the cell
// this way when the content comes back in we can put it back in the same cell,
// even if the translations come back in a different order
if (cell.TryGetValue("rowIndex", out var rowIndex) is false
|| cell.TryGetValue("columnIndex", out var columnIndex) is false)
continue;
if (cell.TryGetValue("value", out var cellValue) is false)
continue;
var cellTranslationValue = _mappers.Value.GetMapperSource(new PropertyValueMapperSourceOptions
{
DisplayName = $"{options.DisplayName} [{rowIndex}, {columnIndex}]",
PropertyTypeAlias = Umbraco.Cms.Core.Constants.PropertyEditors.Aliases.TinyMce,
Path = options.Path.AppendPath($"{rowIndex}-{columnIndex}"),
SourceValue = cellValue.ToString(),
MasterIsVariant = options.MasterIsVariant,
RequireVariant = options.RequireVariant
});
if (cellTranslationValue is not null)
{
translationValue.InnerValues.Add(
$"{rowIndex}-{columnIndex}",
cellTranslationValue);
}
}
}
return translationValue;
}
/// <summary>
/// Applies translated values to the Limbo tables property editor's JSON structure.
/// </summary>
/// <param name="options">The target options containing translation values and culture information.</param>
/// <returns>
/// A JSON string representing the updated table content with translated cell values,
/// or <c>null</c> if the value is not valid or cannot be parsed.
/// </returns>
public override object? GetTargetValue(PropertyValueMapperTargetOptions options)
{
var attempt = options.SourceValue.TryConvertTo<string>();
if (!attempt.Success || attempt.Result is null) return null;
var jsonValue = JsonConvert.DeserializeObject<JObject>(attempt.Result);
if (jsonValue is null) return null;
if (jsonValue.ContainsKey("cells") is false) return null;
if (jsonValue["cells"] is not JArray data) return null;
foreach (var row in data)
{
// data is an array of rows, each row is an array of cells
foreach (var cell in row.Cast<JObject>())
{
if (cell.TryGetValue("rowIndex", out var rowIndex) is false
|| cell.TryGetValue("columnIndex", out var columnIndex) is false)
continue;
if (cell.TryGetValue("value", out var cellValue) is false)
continue;
var translation = options.Values.GetInnerValue($"{rowIndex}-{columnIndex}", options.TargetCulture.Name);
if (translation is null)
continue;
var mappedValue = _mappers.Value.GetMapperTarget(new PropertyValueMapperTargetOptions
{
PropertyTypeAlias = Umbraco.Cms.Core.Constants.PropertyEditors.Aliases.TinyMce,
SourceValue = cellValue.ToString(),
Values = translation,
SourceCulture = options.SourceCulture,
TargetCulture = options.TargetCulture,
MasterIsVariant = options.MasterIsVariant,
RequireVariant = options.RequireVariant
});
cell["value"] = mappedValue.ToString();
}
}
return JsonConvert.SerializeObject(jsonValue, Formatting.Indented);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment