Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
306 lines
10 KiB
C#
306 lines
10 KiB
C#
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using StellaOps.Notifier.Worker.Localization;
|
|
|
|
namespace StellaOps.Notifier.WebService.Endpoints;
|
|
|
|
/// <summary>
|
|
/// REST API endpoints for localization operations.
|
|
/// </summary>
|
|
public static class LocalizationEndpoints
|
|
{
|
|
/// <summary>
|
|
/// Maps localization API endpoints.
|
|
/// </summary>
|
|
public static RouteGroupBuilder MapLocalizationEndpoints(this IEndpointRouteBuilder endpoints)
|
|
{
|
|
var group = endpoints.MapGroup("/api/v2/localization")
|
|
.WithTags("Localization")
|
|
.WithOpenApi();
|
|
|
|
// List bundles
|
|
group.MapGet("/bundles", async (
|
|
HttpContext context,
|
|
ILocalizationService localizationService,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
|
|
|
var bundles = await localizationService.ListBundlesAsync(tenantId, cancellationToken);
|
|
|
|
return Results.Ok(new
|
|
{
|
|
tenantId,
|
|
bundles = bundles.Select(b => new
|
|
{
|
|
b.BundleId,
|
|
b.TenantId,
|
|
b.Locale,
|
|
b.Namespace,
|
|
stringCount = b.Strings.Count,
|
|
b.Priority,
|
|
b.Enabled,
|
|
b.Source,
|
|
b.Description,
|
|
b.CreatedAt,
|
|
b.UpdatedAt
|
|
}).ToList(),
|
|
count = bundles.Count
|
|
});
|
|
})
|
|
.WithName("ListLocalizationBundles")
|
|
.WithSummary("Lists all localization bundles for a tenant");
|
|
|
|
// Get supported locales
|
|
group.MapGet("/locales", async (
|
|
HttpContext context,
|
|
ILocalizationService localizationService,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
|
|
|
var locales = await localizationService.GetSupportedLocalesAsync(tenantId, cancellationToken);
|
|
|
|
return Results.Ok(new
|
|
{
|
|
tenantId,
|
|
locales,
|
|
count = locales.Count
|
|
});
|
|
})
|
|
.WithName("GetSupportedLocales")
|
|
.WithSummary("Gets all supported locales for a tenant");
|
|
|
|
// Get bundle contents
|
|
group.MapGet("/bundles/{locale}", async (
|
|
string locale,
|
|
HttpContext context,
|
|
ILocalizationService localizationService,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
|
|
|
var strings = await localizationService.GetBundleAsync(tenantId, locale, cancellationToken);
|
|
|
|
return Results.Ok(new
|
|
{
|
|
tenantId,
|
|
locale,
|
|
strings,
|
|
count = strings.Count
|
|
});
|
|
})
|
|
.WithName("GetLocalizationBundle")
|
|
.WithSummary("Gets all localized strings for a locale");
|
|
|
|
// Get single string
|
|
group.MapGet("/strings/{key}", async (
|
|
string key,
|
|
string? locale,
|
|
HttpContext context,
|
|
ILocalizationService localizationService,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
|
var effectiveLocale = locale ?? "en-US";
|
|
|
|
var value = await localizationService.GetStringAsync(tenantId, key, effectiveLocale, cancellationToken);
|
|
|
|
return Results.Ok(new
|
|
{
|
|
tenantId,
|
|
key,
|
|
locale = effectiveLocale,
|
|
value
|
|
});
|
|
})
|
|
.WithName("GetLocalizedString")
|
|
.WithSummary("Gets a single localized string");
|
|
|
|
// Format string with parameters
|
|
group.MapPost("/strings/{key}/format", async (
|
|
string key,
|
|
FormatStringRequest request,
|
|
HttpContext context,
|
|
ILocalizationService localizationService,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
|
var locale = request.Locale ?? "en-US";
|
|
|
|
var parameters = request.Parameters ?? new Dictionary<string, object>();
|
|
var value = await localizationService.GetFormattedStringAsync(
|
|
tenantId, key, locale, parameters, cancellationToken);
|
|
|
|
return Results.Ok(new
|
|
{
|
|
tenantId,
|
|
key,
|
|
locale,
|
|
formatted = value
|
|
});
|
|
})
|
|
.WithName("FormatLocalizedString")
|
|
.WithSummary("Gets a localized string with parameter substitution");
|
|
|
|
// Create/update bundle
|
|
group.MapPut("/bundles", async (
|
|
CreateBundleRequest request,
|
|
HttpContext context,
|
|
ILocalizationService localizationService,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
|
var actor = context.Request.Headers["X-Actor"].FirstOrDefault() ?? "system";
|
|
|
|
var bundle = new LocalizationBundle
|
|
{
|
|
BundleId = request.BundleId ?? $"bundle-{Guid.NewGuid():N}"[..20],
|
|
TenantId = tenantId,
|
|
Locale = request.Locale,
|
|
Namespace = request.Namespace ?? "default",
|
|
Strings = request.Strings,
|
|
Priority = request.Priority,
|
|
Enabled = request.Enabled,
|
|
Description = request.Description,
|
|
Source = "api"
|
|
};
|
|
|
|
var result = await localizationService.UpsertBundleAsync(bundle, actor, cancellationToken);
|
|
|
|
if (!result.Success)
|
|
{
|
|
return Results.BadRequest(new { error = result.Error });
|
|
}
|
|
|
|
return result.IsNew
|
|
? Results.Created($"/api/v2/localization/bundles/{bundle.Locale}", new
|
|
{
|
|
bundleId = result.BundleId,
|
|
message = "Bundle created successfully"
|
|
})
|
|
: Results.Ok(new
|
|
{
|
|
bundleId = result.BundleId,
|
|
message = "Bundle updated successfully"
|
|
});
|
|
})
|
|
.WithName("UpsertLocalizationBundle")
|
|
.WithSummary("Creates or updates a localization bundle");
|
|
|
|
// Delete bundle
|
|
group.MapDelete("/bundles/{bundleId}", async (
|
|
string bundleId,
|
|
HttpContext context,
|
|
ILocalizationService localizationService,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
|
var actor = context.Request.Headers["X-Actor"].FirstOrDefault() ?? "system";
|
|
|
|
var deleted = await localizationService.DeleteBundleAsync(tenantId, bundleId, actor, cancellationToken);
|
|
|
|
if (!deleted)
|
|
{
|
|
return Results.NotFound(new { error = $"Bundle '{bundleId}' not found" });
|
|
}
|
|
|
|
return Results.Ok(new { message = $"Bundle '{bundleId}' deleted successfully" });
|
|
})
|
|
.WithName("DeleteLocalizationBundle")
|
|
.WithSummary("Deletes a localization bundle");
|
|
|
|
// Validate bundle
|
|
group.MapPost("/bundles/validate", (
|
|
CreateBundleRequest request,
|
|
HttpContext context,
|
|
ILocalizationService localizationService) =>
|
|
{
|
|
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
|
|
|
var bundle = new LocalizationBundle
|
|
{
|
|
BundleId = request.BundleId ?? "validation",
|
|
TenantId = tenantId,
|
|
Locale = request.Locale,
|
|
Namespace = request.Namespace ?? "default",
|
|
Strings = request.Strings,
|
|
Priority = request.Priority,
|
|
Enabled = request.Enabled,
|
|
Description = request.Description
|
|
};
|
|
|
|
var result = localizationService.Validate(bundle);
|
|
|
|
return Results.Ok(new
|
|
{
|
|
result.IsValid,
|
|
result.Errors,
|
|
result.Warnings
|
|
});
|
|
})
|
|
.WithName("ValidateLocalizationBundle")
|
|
.WithSummary("Validates a localization bundle without saving");
|
|
|
|
return group;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request to format a localized string.
|
|
/// </summary>
|
|
public sealed record FormatStringRequest
|
|
{
|
|
/// <summary>
|
|
/// Target locale.
|
|
/// </summary>
|
|
public string? Locale { get; init; }
|
|
|
|
/// <summary>
|
|
/// Parameters for substitution.
|
|
/// </summary>
|
|
public Dictionary<string, object>? Parameters { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request to create/update a localization bundle.
|
|
/// </summary>
|
|
public sealed record CreateBundleRequest
|
|
{
|
|
/// <summary>
|
|
/// Bundle ID (auto-generated if not provided).
|
|
/// </summary>
|
|
public string? BundleId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Locale code.
|
|
/// </summary>
|
|
public required string Locale { get; init; }
|
|
|
|
/// <summary>
|
|
/// Namespace/category.
|
|
/// </summary>
|
|
public string? Namespace { get; init; }
|
|
|
|
/// <summary>
|
|
/// Localized strings.
|
|
/// </summary>
|
|
public required Dictionary<string, string> Strings { get; init; }
|
|
|
|
/// <summary>
|
|
/// Bundle priority.
|
|
/// </summary>
|
|
public int Priority { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether bundle is enabled.
|
|
/// </summary>
|
|
public bool Enabled { get; init; } = true;
|
|
|
|
/// <summary>
|
|
/// Bundle description.
|
|
/// </summary>
|
|
public string? Description { get; init; }
|
|
}
|