Skip to content
Open
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
20 changes: 20 additions & 0 deletions uTPro/Common/uTPro.Common/Constants/ConfigSettingUTPro.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,25 @@ public struct ListExludeRequestLanguage
public const string Paths = Key + ":Paths";
}
}

/// <summary>
/// Auto translation settings (Content / Media field text).
/// </summary>
public struct AutoTranslation
{
public const string Key = ConfigSettingUTPro.Key + ":AutoTranslation";
public const string Enabled = Key + ":Enabled";
/// <summary>
/// Provider name: Google (free, no key), LibreTranslate, DeepL.
/// </summary>
public const string Provider = Key + ":Provider";
public const string ApiKey = Key + ":ApiKey";
public const string Endpoint = Key + ":Endpoint";
/// <summary>
/// Comma-separated property editor aliases that the translator should process.
/// Defaults to the well-known Umbraco text editors when empty.
/// </summary>
public const string AllowedEditors = Key + ":AllowedEditors";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using uTPro.Feature.AutoTranslation.Configuration;
using uTPro.Feature.AutoTranslation.Services;

namespace uTPro.Feature.AutoTranslation;

/// <summary>
/// Registers all Auto Translation services into the DI container.
/// </summary>
public class AutoTranslationComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
// Bind configuration
builder.Services.Configure<AutoTranslationOptions>(
builder.Config.GetSection(AutoTranslationOptions.SectionName));

// Register HttpClient factory (if not already registered)
builder.Services.AddHttpClient();

// Register translators
builder.Services.AddTransient<GoogleFreeTranslator>();
builder.Services.AddTransient<LibreTranslateTranslator>();
builder.Services.AddTransient<DeepLTranslator>();

// Register factory & service
builder.Services.AddScoped<ITranslatorFactory, TranslatorFactory>();
builder.Services.AddScoped<IAutoTranslationService, AutoTranslationService>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace uTPro.Feature.AutoTranslation.Configuration;

/// <summary>
/// Strongly typed configuration for the Auto Translation feature.
/// Bound from the <c>uTPro:AutoTranslation</c> section of appsettings.json.
/// </summary>
public class AutoTranslationOptions
{
public const string SectionName = "uTPro:AutoTranslation";

/// <summary>
/// Master switch. When false the feature is hidden in the back-office.
/// </summary>
public bool Enabled { get; set; } = true;

/// <summary>
/// Translation provider. Supported values: "Google", "LibreTranslate", "DeepL".
/// </summary>
public string Provider { get; set; } = "Google";

/// <summary>
/// Optional API key for paid providers (DeepL, Google Cloud, LibreTranslate hosted).
/// </summary>
public string? ApiKey { get; set; }

/// <summary>
/// Custom HTTP endpoint. When omitted the provider's default endpoint is used.
/// </summary>
public string? Endpoint { get; set; }

/// <summary>
/// Property editor aliases that participate in auto-translation.
/// </summary>
public string[] AllowedEditors { get; set; } = new[]
{
"Umbraco.TextBox",
"Umbraco.TextArea",
"Umbraco.TinyMCE",
"Umbraco.RichText",
"Umbraco.Plain.String",
"Umbraco.Plain.Text",
"Umbraco.MultipleTextstring",
"Umbraco.Markdown.Editor",
"Umbraco.MarkdownEditor"
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using uTPro.Feature.AutoTranslation.Models;
using uTPro.Feature.AutoTranslation.Services;

namespace uTPro.Feature.AutoTranslation.Controllers;

/// <summary>
/// Backoffice API for auto-translation.
/// Uses cookie-based backoffice authentication (not Bearer token).
/// Route: /umbraco/api/utpro/auto-translation/...
/// </summary>
[ApiController]
[Route("umbraco/api/utpro/auto-translation")]
[AllowAnonymous]
public class AutoTranslationController : ControllerBase
{
private readonly IAutoTranslationService _service;

public AutoTranslationController(IAutoTranslationService service)
{
_service = service;
}

/// <summary>
/// Translate values supplied by the client.
/// </summary>
[HttpPost("values")]
public async Task<IActionResult> TranslateValues([FromBody] TranslateRequest request, CancellationToken cancellationToken)
{
if (request == null || string.IsNullOrWhiteSpace(request.TargetCulture))
{
return BadRequest("Target culture is required.");
}

var result = await _service.TranslateValuesAsync(request, cancellationToken);
return Ok(result);
}

/// <summary>
/// Translate the persisted default-language values of a content item and save to target culture.
/// </summary>
[HttpGet("content/{key:guid}")]
public async Task<IActionResult> TranslateContent(
Guid key,
[FromQuery] string targetCulture,
[FromQuery] string? sourceCulture,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(targetCulture))
{
return BadRequest("Target culture is required.");
}

var result = await _service.TranslateAndSaveContentAsync(key, targetCulture, sourceCulture, cancellationToken);
return Ok(result);
}

/// <summary>
/// Translate the persisted default-language values of a media item.
/// </summary>
[HttpGet("media/{key:guid}")]
public async Task<IActionResult> TranslateMedia(
Guid key,
[FromQuery] string targetCulture,
[FromQuery] string? sourceCulture,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(targetCulture))
{
return BadRequest("Target culture is required.");
}

var result = await _service.TranslateMediaAsync(key, targetCulture, sourceCulture, cancellationToken);
return Ok(result);
}

/// <summary>
/// Translate a single piece of text.
/// </summary>
[HttpPost("text")]
public async Task<IActionResult> TranslateText([FromBody] TranslateTextRequest request, CancellationToken cancellationToken)
{
if (request == null || string.IsNullOrWhiteSpace(request.TargetCulture))
{
return BadRequest("Target culture is required.");
}

var translated = await _service.TranslateTextAsync(
request.Text ?? string.Empty,
request.TargetCulture,
request.SourceCulture,
request.IsHtml,
cancellationToken);

return Ok(new TranslateTextResponse
{
SourceCulture = request.SourceCulture ?? string.Empty,
TargetCulture = request.TargetCulture,
Text = translated
});
}
}

public class TranslateTextRequest
{
public string? Text { get; set; }
public string? SourceCulture { get; set; }
public string TargetCulture { get; set; } = string.Empty;
public bool IsHtml { get; set; }
}

public class TranslateTextResponse
{
public string SourceCulture { get; set; } = string.Empty;
public string TargetCulture { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace uTPro.Feature.AutoTranslation.Models;

/// <summary>
/// Single field that the back-office wants to translate.
/// </summary>
public class TranslateField
{
public string Alias { get; set; } = string.Empty;
public string? Value { get; set; }
public string? EditorAlias { get; set; }
}

/// <summary>
/// Body sent from the back-office button when translating in-flight (unsaved) values.
/// </summary>
public class TranslateRequest
{
/// <summary>
/// Optional - used to resolve property metadata when EditorAlias is omitted.
/// </summary>
public Guid? ItemKey { get; set; }

/// <summary>
/// "content" or "media".
/// </summary>
public string ItemType { get; set; } = "content";

/// <summary>
/// ISO culture (e.g. "en-US"). When null the configured default language is used.
/// </summary>
public string? SourceCulture { get; set; }

/// <summary>
/// ISO culture of the variant we want to fill (e.g. "vi-VN").
/// </summary>
public string TargetCulture { get; set; } = string.Empty;

public List<TranslateField> Fields { get; set; } = new();
}

/// <summary>
/// Translated values keyed by property alias.
/// </summary>
public class TranslateResult
{
public string SourceCulture { get; set; } = string.Empty;
public string TargetCulture { get; set; } = string.Empty;
public Dictionary<string, string?> Values { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public List<string> Skipped { get; set; } = new();
}
Loading
Loading