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
234 lines
7.5 KiB
C#
234 lines
7.5 KiB
C#
using System.Collections.Immutable;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace StellaOps.Notify.Models;
|
|
|
|
/// <summary>
|
|
/// A localization bundle containing translated strings for a specific locale.
|
|
/// </summary>
|
|
public sealed record NotifyLocalizationBundle
|
|
{
|
|
[JsonConstructor]
|
|
public NotifyLocalizationBundle(
|
|
string bundleId,
|
|
string tenantId,
|
|
string locale,
|
|
string bundleKey,
|
|
ImmutableDictionary<string, string> strings,
|
|
bool isDefault = false,
|
|
string? parentLocale = null,
|
|
string? description = null,
|
|
ImmutableDictionary<string, string>? metadata = null,
|
|
string? createdBy = null,
|
|
DateTimeOffset? createdAt = null,
|
|
string? updatedBy = null,
|
|
DateTimeOffset? updatedAt = null)
|
|
{
|
|
BundleId = NotifyValidation.EnsureNotNullOrWhiteSpace(bundleId, nameof(bundleId));
|
|
TenantId = NotifyValidation.EnsureNotNullOrWhiteSpace(tenantId, nameof(tenantId));
|
|
Locale = NotifyValidation.EnsureNotNullOrWhiteSpace(locale, nameof(locale)).ToLowerInvariant();
|
|
BundleKey = NotifyValidation.EnsureNotNullOrWhiteSpace(bundleKey, nameof(bundleKey));
|
|
Strings = strings;
|
|
IsDefault = isDefault;
|
|
ParentLocale = NormalizeParentLocale(parentLocale, Locale);
|
|
Description = NotifyValidation.TrimToNull(description);
|
|
Metadata = NotifyValidation.NormalizeStringDictionary(metadata);
|
|
CreatedBy = NotifyValidation.TrimToNull(createdBy);
|
|
CreatedAt = NotifyValidation.EnsureUtc(createdAt ?? DateTimeOffset.UtcNow);
|
|
UpdatedBy = NotifyValidation.TrimToNull(updatedBy);
|
|
UpdatedAt = NotifyValidation.EnsureUtc(updatedAt ?? CreatedAt);
|
|
}
|
|
|
|
public static NotifyLocalizationBundle Create(
|
|
string bundleId,
|
|
string tenantId,
|
|
string locale,
|
|
string bundleKey,
|
|
IEnumerable<KeyValuePair<string, string>>? strings = null,
|
|
bool isDefault = false,
|
|
string? parentLocale = null,
|
|
string? description = null,
|
|
IEnumerable<KeyValuePair<string, string>>? metadata = null,
|
|
string? createdBy = null,
|
|
DateTimeOffset? createdAt = null,
|
|
string? updatedBy = null,
|
|
DateTimeOffset? updatedAt = null)
|
|
{
|
|
return new NotifyLocalizationBundle(
|
|
bundleId,
|
|
tenantId,
|
|
locale,
|
|
bundleKey,
|
|
ToImmutableDictionary(strings) ?? ImmutableDictionary<string, string>.Empty,
|
|
isDefault,
|
|
parentLocale,
|
|
description,
|
|
ToImmutableDictionary(metadata),
|
|
createdBy,
|
|
createdAt,
|
|
updatedBy,
|
|
updatedAt);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unique identifier for this bundle.
|
|
/// </summary>
|
|
public string BundleId { get; }
|
|
|
|
/// <summary>
|
|
/// Tenant ID this bundle belongs to.
|
|
/// </summary>
|
|
public string TenantId { get; }
|
|
|
|
/// <summary>
|
|
/// Locale code (e.g., "en-us", "fr-fr", "ja-jp").
|
|
/// </summary>
|
|
public string Locale { get; }
|
|
|
|
/// <summary>
|
|
/// Bundle key for grouping related bundles (e.g., "notifications", "email-subjects").
|
|
/// </summary>
|
|
public string BundleKey { get; }
|
|
|
|
/// <summary>
|
|
/// Dictionary of string key to translated value.
|
|
/// </summary>
|
|
public ImmutableDictionary<string, string> Strings { get; }
|
|
|
|
/// <summary>
|
|
/// Whether this is the default/fallback bundle for the bundle key.
|
|
/// </summary>
|
|
public bool IsDefault { get; }
|
|
|
|
/// <summary>
|
|
/// Parent locale for fallback chain (e.g., "en" for "en-us").
|
|
/// Automatically computed if not specified.
|
|
/// </summary>
|
|
public string? ParentLocale { get; }
|
|
|
|
public string? Description { get; }
|
|
|
|
public ImmutableDictionary<string, string> Metadata { get; }
|
|
|
|
public string? CreatedBy { get; }
|
|
|
|
public DateTimeOffset CreatedAt { get; }
|
|
|
|
public string? UpdatedBy { get; }
|
|
|
|
public DateTimeOffset UpdatedAt { get; }
|
|
|
|
/// <summary>
|
|
/// Gets a localized string by key.
|
|
/// </summary>
|
|
public string? GetString(string key)
|
|
{
|
|
return Strings.TryGetValue(key, out var value) ? value : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a localized string by key with a default fallback.
|
|
/// </summary>
|
|
public string GetString(string key, string defaultValue)
|
|
{
|
|
return Strings.TryGetValue(key, out var value) ? value : defaultValue;
|
|
}
|
|
|
|
private static string? NormalizeParentLocale(string? parentLocale, string locale)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(parentLocale))
|
|
{
|
|
return parentLocale.ToLowerInvariant();
|
|
}
|
|
|
|
// Auto-compute parent locale from locale
|
|
// e.g., "en-us" -> "en", "pt-br" -> "pt"
|
|
var dashIndex = locale.IndexOf('-');
|
|
if (dashIndex > 0)
|
|
{
|
|
return locale[..dashIndex];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static ImmutableDictionary<string, string>? ToImmutableDictionary(IEnumerable<KeyValuePair<string, string>>? pairs)
|
|
{
|
|
if (pairs is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var builder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
|
foreach (var (key, value) in pairs)
|
|
{
|
|
builder[key] = value;
|
|
}
|
|
|
|
return builder.ToImmutable();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Service for resolving localized strings with fallback chain support.
|
|
/// </summary>
|
|
public interface ILocalizationResolver
|
|
{
|
|
/// <summary>
|
|
/// Resolves a localized string using the fallback chain.
|
|
/// </summary>
|
|
/// <param name="tenantId">The tenant ID.</param>
|
|
/// <param name="bundleKey">The bundle key.</param>
|
|
/// <param name="stringKey">The string key within the bundle.</param>
|
|
/// <param name="locale">The preferred locale.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The resolved string or null if not found.</returns>
|
|
Task<LocalizedString?> ResolveAsync(
|
|
string tenantId,
|
|
string bundleKey,
|
|
string stringKey,
|
|
string locale,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Resolves multiple strings at once for efficiency.
|
|
/// </summary>
|
|
Task<IReadOnlyDictionary<string, LocalizedString>> ResolveBatchAsync(
|
|
string tenantId,
|
|
string bundleKey,
|
|
IEnumerable<string> stringKeys,
|
|
string locale,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of a localization resolution.
|
|
/// </summary>
|
|
public sealed record LocalizedString
|
|
{
|
|
/// <summary>
|
|
/// The resolved string value.
|
|
/// </summary>
|
|
public required string Value { get; init; }
|
|
|
|
/// <summary>
|
|
/// The locale that provided the value.
|
|
/// </summary>
|
|
public required string ResolvedLocale { get; init; }
|
|
|
|
/// <summary>
|
|
/// The originally requested locale.
|
|
/// </summary>
|
|
public required string RequestedLocale { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether fallback was used.
|
|
/// </summary>
|
|
public bool UsedFallback => !ResolvedLocale.Equals(RequestedLocale, StringComparison.OrdinalIgnoreCase);
|
|
|
|
/// <summary>
|
|
/// The fallback chain that was traversed.
|
|
/// </summary>
|
|
public IReadOnlyList<string> FallbackChain { get; init; } = [];
|
|
}
|