search and ai stabilization work, localization stablized.

This commit is contained in:
master
2026-02-24 23:29:36 +02:00
parent 4f947a8b61
commit b07d27772e
766 changed files with 55299 additions and 3221 deletions

View File

@@ -49,4 +49,8 @@ public static class PlatformPolicies
public const string TrustRead = "platform.trust.read";
public const string TrustWrite = "platform.trust.write";
public const string TrustAdmin = "platform.trust.admin";
// Identity provider management policies (SPRINT_20260224_100)
public const string IdentityProviderRead = "platform.idp.read";
public const string IdentityProviderAdmin = "platform.idp.admin";
}

View File

@@ -50,4 +50,8 @@ public static class PlatformScopes
public const string TrustRead = StellaOpsScopes.TrustRead;
public const string TrustWrite = StellaOpsScopes.TrustWrite;
public const string TrustAdmin = StellaOpsScopes.TrustAdmin;
// Identity provider management scopes (SPRINT_20260224_100)
public const string IdentityProviderRead = "platform.idp.read";
public const string IdentityProviderAdmin = "platform.idp.admin";
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Platform.WebService.Contracts;
public sealed record IdentityProviderConfigDto(
Guid Id,
string Name,
string Type,
bool Enabled,
Dictionary<string, string?> Configuration,
string? Description,
string? HealthStatus,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt,
string? CreatedBy,
string? UpdatedBy);
public sealed record CreateIdentityProviderRequest(
string Name,
string Type,
bool Enabled,
Dictionary<string, string?> Configuration,
string? Description);
public sealed record UpdateIdentityProviderRequest(
bool? Enabled,
Dictionary<string, string?>? Configuration,
string? Description);
public sealed record TestConnectionRequest(
string Type,
Dictionary<string, string?> Configuration);
public sealed record TestConnectionResult(
bool Success,
string Message,
long? LatencyMs);
public sealed record IdentityProviderTypeSchema(
string Type,
string DisplayName,
IReadOnlyList<IdentityProviderFieldSchema> RequiredFields,
IReadOnlyList<IdentityProviderFieldSchema> OptionalFields);
public sealed record IdentityProviderFieldSchema(
string Name,
string DisplayName,
string FieldType,
string? DefaultValue,
string? Description);

View File

@@ -13,6 +13,16 @@ public sealed record PlatformDashboardPreferences(
public sealed record PlatformDashboardPreferencesRequest(
JsonObject Preferences);
public sealed record PlatformLanguagePreference(
string TenantId,
string ActorId,
string? Locale,
DateTimeOffset UpdatedAt,
string? UpdatedBy);
public sealed record PlatformLanguagePreferenceRequest(
string Locale);
public sealed record PlatformDashboardProfile(
string ProfileId,
string Name,

View File

@@ -0,0 +1,277 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.Platform.WebService.Constants;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Platform.WebService.Endpoints;
public static class IdentityProviderEndpoints
{
public static IEndpointRouteBuilder MapIdentityProviderEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/platform/identity-providers")
.WithTags("Identity Providers")
.RequireAuthorization(PlatformPolicies.IdentityProviderAdmin)
.RequireTenant();
group.MapGet("/", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IdentityProviderManagementService service,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
return failure!;
var items = await service.ListAsync(requestContext!.TenantId, cancellationToken).ConfigureAwait(false);
return Results.Ok(items);
});
group.MapGet("/{id:guid}", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IdentityProviderManagementService service,
Guid id,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
return failure!;
var item = await service.GetAsync(requestContext!.TenantId, id, cancellationToken).ConfigureAwait(false);
return item is null ? Results.NotFound() : Results.Ok(item);
});
group.MapPost("/", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IdentityProviderManagementService service,
CreateIdentityProviderRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
return failure!;
try
{
var created = await service.CreateAsync(
requestContext!.TenantId,
requestContext.ActorId,
request,
cancellationToken).ConfigureAwait(false);
return Results.Created($"/api/v1/platform/identity-providers/{created.Id}", created);
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
});
group.MapPut("/{id:guid}", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IdentityProviderManagementService service,
Guid id,
UpdateIdentityProviderRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
return failure!;
try
{
var updated = await service.UpdateAsync(
requestContext!.TenantId,
requestContext.ActorId,
id,
request,
cancellationToken).ConfigureAwait(false);
return updated is null ? Results.NotFound() : Results.Ok(updated);
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
});
group.MapDelete("/{id:guid}", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IdentityProviderManagementService service,
Guid id,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
return failure!;
var deleted = await service.DeleteAsync(requestContext!.TenantId, id, cancellationToken).ConfigureAwait(false);
return deleted ? Results.NoContent() : Results.NotFound();
});
group.MapPost("/{id:guid}/enable", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IdentityProviderManagementService service,
Guid id,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
return failure!;
var result = await service.SetEnabledAsync(
requestContext!.TenantId,
requestContext.ActorId,
id,
true,
cancellationToken).ConfigureAwait(false);
return result is null ? Results.NotFound() : Results.Ok(result);
});
group.MapPost("/{id:guid}/disable", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IdentityProviderManagementService service,
Guid id,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
return failure!;
var result = await service.SetEnabledAsync(
requestContext!.TenantId,
requestContext.ActorId,
id,
false,
cancellationToken).ConfigureAwait(false);
return result is null ? Results.NotFound() : Results.Ok(result);
});
group.MapPost("/test-connection", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IdentityProviderManagementService service,
TestConnectionRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out _, out var failure))
return failure!;
try
{
var result = await service.TestConnectionAsync(request, cancellationToken).ConfigureAwait(false);
return Results.Ok(result);
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
});
group.MapGet("/{id:guid}/health", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IdentityProviderManagementService service,
Guid id,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
return failure!;
var result = await service.GetHealthAsync(requestContext!.TenantId, id, cancellationToken).ConfigureAwait(false);
return Results.Ok(result);
});
group.MapPost("/{id:guid}/apply", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IdentityProviderManagementService service,
IHttpClientFactory httpClientFactory,
ILogger<IdentityProviderManagementService> logger,
Guid id,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
return failure!;
var item = await service.GetAsync(requestContext!.TenantId, id, cancellationToken).ConfigureAwait(false);
if (item is null)
return Results.NotFound();
try
{
var client = httpClientFactory.CreateClient("AuthorityInternal");
using var response = await client.PostAsync("internal/plugins/reload", null, cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
logger.LogInformation(
"Authority plugin reload triggered for provider '{ProviderName}' ({ProviderId}).",
item.Name,
id);
return Results.Ok(new { applied = true, providerId = id, providerName = item.Name });
}
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogWarning(
"Authority plugin reload returned {StatusCode} for provider '{ProviderName}': {Body}",
(int)response.StatusCode,
item.Name,
body);
return Results.Ok(new { applied = false, providerId = id, providerName = item.Name, error = $"Authority returned {(int)response.StatusCode}" });
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
{
logger.LogWarning(
ex,
"Failed to reach Authority reload endpoint for provider '{ProviderName}'. Config saved but not applied.",
item.Name);
return Results.Ok(new { applied = false, providerId = id, providerName = item.Name, error = "Authority unreachable; config saved but not applied." });
}
});
group.MapGet("/types", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IdentityProviderManagementService service,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out _, out var failure))
return failure!;
var types = await service.GetTypeSchemasAsync(cancellationToken).ConfigureAwait(false);
return Results.Ok(types);
});
return app;
}
private static bool TryResolveContext(
HttpContext context,
PlatformRequestContextResolver resolver,
out PlatformRequestContext? requestContext,
out IResult? failure)
{
if (resolver.TryResolve(context, out requestContext, out var error))
{
failure = null;
return true;
}
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
return false;
}
}

View File

@@ -0,0 +1,185 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.Localization;
using StellaOps.Platform.WebService.Constants;
using StellaOps.Platform.WebService.Services;
namespace StellaOps.Platform.WebService.Endpoints;
/// <summary>
/// REST API endpoints for translation management and serving UI bundles.
/// </summary>
public static class LocalizationEndpoints
{
public static IEndpointRouteBuilder MapLocalizationEndpoints(this IEndpointRouteBuilder app)
{
// Anonymous UI bundle endpoint (same pattern as /platform/envsettings.json)
app.MapGet("/platform/i18n/{locale}.json", async (
string locale,
HttpContext context,
PlatformTranslationService translationService,
CancellationToken ct) =>
{
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "_system";
var bundle = await translationService.GetMergedBundleAsync(tenantId, locale, ct);
// Cache for 5 minutes — translations don't change often
context.Response.Headers.CacheControl = "public, max-age=300";
return Results.Ok(bundle);
})
.WithTags("Localization")
.WithName("GetUiTranslationBundle")
.WithSummary("Gets the merged translation bundle for the UI")
.WithDescription(
"Returns all translations for the specified locale, merging embedded defaults with DB overrides. " +
"Anonymous access, cacheable. Used by the Angular frontend at boot time.")
.AllowAnonymous();
// Authenticated API group
var group = app.MapGroup("/api/v1/platform/localization")
.WithTags("Localization")
.RequireAuthorization(PlatformPolicies.PreferencesRead)
.RequireTenant();
// Get all translations for a locale
group.MapGet("/bundles/{locale}", async (
string locale,
HttpContext context,
PlatformTranslationService translationService,
CancellationToken ct) =>
{
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "_system";
var bundle = await translationService.GetMergedBundleAsync(tenantId, locale, ct);
return Results.Ok(new
{
locale,
strings = bundle,
count = bundle.Count
});
})
.WithName("GetTranslationBundle")
.WithSummary("Gets all translations for a locale")
.WithDescription(
"Returns the merged set of all translations for the specified locale, " +
"combining embedded defaults with system and tenant DB overrides in priority order.");
// Get translations filtered by namespace
group.MapGet("/bundles/{locale}/{ns}", async (
string locale,
string ns,
HttpContext context,
PlatformTranslationService translationService,
CancellationToken ct) =>
{
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "_system";
var bundle = await translationService.GetMergedBundleAsync(tenantId, locale, ns, ct);
return Results.Ok(new
{
locale,
@namespace = ns,
strings = bundle,
count = bundle.Count
});
})
.WithName("GetTranslationBundleByNamespace")
.WithSummary("Gets translations for a locale filtered by namespace prefix");
// Get available locales
group.MapGet("/locales", async (
HttpContext context,
PlatformTranslationService translationService,
CancellationToken ct) =>
{
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "_system";
var locales = await translationService.GetAllLocalesAsync(tenantId, ct);
return Results.Ok(new
{
locales,
count = locales.Count
});
})
.WithName("GetAvailableLocales")
.WithSummary("Gets all available locales");
// Upsert translations (admin)
group.MapPut("/bundles", async (
UpsertTranslationsRequest request,
HttpContext context,
ITranslationStore store,
TranslationRegistry registry,
CancellationToken ct) =>
{
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "_system";
var actor = context.Request.Headers["X-Actor"].FirstOrDefault() ?? "system";
if (string.IsNullOrWhiteSpace(request.Locale))
{
return Results.BadRequest(new { error = "Locale is required." });
}
if (request.Strings is null || request.Strings.Count == 0)
{
return Results.BadRequest(new { error = "At least one translation string is required." });
}
await store.UpsertBatchAsync(tenantId, request.Locale, request.Strings, actor, ct);
// Also merge into the in-memory registry for immediate effect
registry.MergeBundles(request.Locale, request.Strings);
return Results.Ok(new
{
locale = request.Locale,
upserted = request.Strings.Count,
message = "Translations updated successfully."
});
})
.WithName("UpsertTranslations")
.WithSummary("Creates or updates translation strings")
.WithDescription(
"Upserts translation key-value pairs for a locale. DB values override embedded defaults. " +
"Changes take immediate effect in the in-memory registry.")
.RequireAuthorization(PlatformPolicies.PreferencesWrite);
// Delete a translation (admin)
group.MapDelete("/strings/{locale}/{key}", async (
string locale,
string key,
HttpContext context,
ITranslationStore store,
CancellationToken ct) =>
{
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "_system";
var deleted = await store.DeleteAsync(tenantId, locale, key, ct);
if (!deleted)
{
return Results.NotFound(new { error = $"Translation '{key}' not found for locale '{locale}'." });
}
return Results.Ok(new { message = $"Translation '{key}' deleted for locale '{locale}'." });
})
.WithName("DeleteTranslation")
.WithSummary("Deletes a translation override")
.RequireAuthorization(PlatformPolicies.PreferencesWrite);
return app;
}
}
/// <summary>
/// Request to upsert translations.
/// </summary>
public sealed record UpsertTranslationsRequest
{
/// <summary>Target locale (e.g., "en-US", "de-DE").</summary>
public required string Locale { get; init; }
/// <summary>Key-value pairs to upsert.</summary>
public required Dictionary<string, string> Strings { get; init; }
}

View File

@@ -348,6 +348,44 @@ public static class PlatformEndpoints
}
}).RequireAuthorization(PlatformPolicies.PreferencesWrite);
preferences.MapGet("/language", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
PlatformPreferencesService service,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var locale = await service.GetLanguagePreferenceAsync(requestContext!, cancellationToken).ConfigureAwait(false);
return Results.Ok(locale);
}).RequireAuthorization(PlatformPolicies.PreferencesRead);
preferences.MapPut("/language", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
PlatformPreferencesService service,
PlatformLanguagePreferenceRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
try
{
var locale = await service.UpsertLanguagePreferenceAsync(requestContext!, request, cancellationToken).ConfigureAwait(false);
return Results.Ok(locale);
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}).RequireAuthorization(PlatformPolicies.PreferencesWrite);
var profiles = platform.MapGroup("/dashboard/profiles").WithTags("Platform Preferences");
profiles.MapGet("/", async Task<IResult> (
@@ -420,6 +458,8 @@ public static class PlatformEndpoints
return failure!;
}
ApplyLegacySearchDeprecationHeaders(context.Response.Headers);
var sources = query.Sources
?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.ToArray();
@@ -452,6 +492,14 @@ public static class PlatformEndpoints
.RequireAuthorization(PlatformPolicies.SearchRead);
}
private static void ApplyLegacySearchDeprecationHeaders(IHeaderDictionary headers)
{
headers["Deprecation"] = "true";
headers["Sunset"] = "2026-04-30T00:00:00Z";
headers["Link"] = "</api/v1/search/query>; rel=\"successor-version\"";
headers["Warning"] = "299 - Legacy platform search is deprecated; migrate to /api/v1/search/query";
}
private static void MapMetadataEndpoints(IEndpointRouteBuilder platform)
{
platform.MapGet("/metadata", async Task<IResult> (

View File

@@ -4,6 +4,7 @@ using Microsoft.Extensions.Options;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.Configuration;
using StellaOps.Localization;
using StellaOps.Messaging.DependencyInjection;
using StellaOps.Platform.Analytics;
using StellaOps.Platform.WebService.Configuration;
@@ -151,6 +152,8 @@ builder.Services.AddAuthorization(options =>
options.AddStellaOpsScopePolicy(PlatformPolicies.ReleaseControlOperate, PlatformScopes.OrchOperate);
options.AddStellaOpsScopePolicy(PlatformPolicies.FederationRead, PlatformScopes.FederationRead);
options.AddStellaOpsScopePolicy(PlatformPolicies.FederationManage, PlatformScopes.FederationManage);
options.AddStellaOpsScopePolicy(PlatformPolicies.IdentityProviderRead, PlatformScopes.IdentityProviderRead);
options.AddStellaOpsScopePolicy(PlatformPolicies.IdentityProviderAdmin, PlatformScopes.IdentityProviderAdmin);
});
builder.Services.AddSingleton<PlatformRequestContextResolver>();
@@ -171,6 +174,19 @@ builder.Services.AddSingleton<PlatformDashboardProfileStore>();
builder.Services.AddSingleton<PlatformPreferencesService>();
builder.Services.AddSingleton<PlatformSearchService>();
builder.Services.AddSingleton<IdentityProviderManagementService>();
builder.Services.AddHttpClient("AuthorityInternal", client =>
{
var authorityUrl = builder.Configuration["STELLAOPS_AUTHORITY_URL"]
?? builder.Configuration["Authority:InternalUrl"]
?? "https://authority.stella-ops.local";
client.BaseAddress = new Uri(authorityUrl.TrimEnd('/') + "/");
client.DefaultRequestHeaders.Add("X-StellaOps-Bootstrap-Key",
builder.Configuration["STELLAOPS_BOOTSTRAP_KEY"] ?? builder.Configuration["Authority:BootstrapKey"] ?? "");
client.Timeout = TimeSpan.FromSeconds(30);
});
builder.Services.AddSingleton<PlatformMetadataService>();
builder.Services.AddSingleton<PlatformContextService>();
builder.Services.AddSingleton<TopologyReadModelService>();
@@ -208,6 +224,7 @@ if (!string.IsNullOrWhiteSpace(bootstrapOptions.Storage.PostgresConnectionString
builder.Services.AddSingleton<IReleaseControlBundleStore, PostgresReleaseControlBundleStore>();
builder.Services.AddSingleton<IAdministrationTrustSigningStore, PostgresAdministrationTrustSigningStore>();
builder.Services.AddSingleton<IPlatformContextStore, PostgresPlatformContextStore>();
builder.Services.AddSingleton<ITranslationStore, PostgresTranslationStore>();
}
else
{
@@ -216,8 +233,15 @@ else
builder.Services.AddSingleton<IReleaseControlBundleStore, InMemoryReleaseControlBundleStore>();
builder.Services.AddSingleton<IAdministrationTrustSigningStore, InMemoryAdministrationTrustSigningStore>();
builder.Services.AddSingleton<IPlatformContextStore, InMemoryPlatformContextStore>();
builder.Services.AddSingleton<ITranslationStore, InMemoryTranslationStore>();
}
// Localization: common base + platform-specific embedded bundles + DB overrides
builder.Services.AddStellaOpsLocalization(builder.Configuration);
builder.Services.AddTranslationBundle(System.Reflection.Assembly.GetExecutingAssembly());
builder.Services.AddSingleton<PlatformTranslationService>();
builder.Services.AddSingleton<ITranslationBundleProvider>(sp => sp.GetRequiredService<PlatformTranslationService>());
// Environment settings composer (3-layer merge: env vars -> YAML -> DB)
builder.Services.AddSingleton<EnvironmentSettingsComposer>();
builder.Services.AddSingleton<SetupStateDetector>();
@@ -254,6 +278,7 @@ if (!string.Equals(bootstrapOptions.Storage.Driver, "memory", StringComparison.O
}
app.UseStellaOpsCors();
app.UseStellaOpsLocalization();
app.UseStellaOpsTelemetryContext();
app.UseAuthentication();
app.UseAuthorization();
@@ -270,10 +295,14 @@ app.Use(async (context, next) =>
}
});
await app.LoadTranslationsAsync();
app.MapLocalizationEndpoints();
app.MapEnvironmentSettingsEndpoints();
app.MapEnvironmentSettingsAdminEndpoints();
app.MapContextEndpoints();
app.MapPlatformEndpoints();
app.MapIdentityProviderEndpoints();
app.MapSetupEndpoints();
app.MapAnalyticsEndpoints();
app.MapScoreEndpoints();

View File

@@ -0,0 +1,29 @@
namespace StellaOps.Platform.WebService.Services;
/// <summary>
/// Persistence for platform translations (DB layer).
/// </summary>
public interface ITranslationStore
{
/// <summary>Get all translations for a tenant and locale.</summary>
Task<IReadOnlyDictionary<string, string>> GetAllAsync(
string tenantId, string locale, CancellationToken ct = default);
/// <summary>Get translations filtered by key prefix for a tenant and locale.</summary>
Task<IReadOnlyDictionary<string, string>> GetByPrefixAsync(
string tenantId, string locale, string keyPrefix, CancellationToken ct = default);
/// <summary>Upsert a single translation.</summary>
Task UpsertAsync(string tenantId, string locale, string key, string value, string actor,
CancellationToken ct = default);
/// <summary>Upsert a batch of translations.</summary>
Task UpsertBatchAsync(string tenantId, string locale, IReadOnlyDictionary<string, string> strings,
string actor, CancellationToken ct = default);
/// <summary>Delete a single translation.</summary>
Task<bool> DeleteAsync(string tenantId, string locale, string key, CancellationToken ct = default);
/// <summary>Get all locales that have at least one translation for a tenant.</summary>
Task<IReadOnlyList<string>> GetAvailableLocalesAsync(string tenantId, CancellationToken ct = default);
}

View File

@@ -0,0 +1,481 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Platform.WebService.Contracts;
namespace StellaOps.Platform.WebService.Services;
public sealed class IdentityProviderManagementService
{
private static readonly HashSet<string> ValidTypes = new(StringComparer.OrdinalIgnoreCase)
{
"standard", "ldap", "saml", "oidc"
};
private static readonly Dictionary<string, string[]> RequiredFieldsByType = new(StringComparer.OrdinalIgnoreCase)
{
["ldap"] = ["host", "port", "bindDn", "bindPassword", "searchBase"],
["saml"] = ["spEntityId", "idpEntityId"],
["oidc"] = ["authority", "clientId"],
["standard"] = []
};
private static readonly Dictionary<string, string[]> OptionalFieldsByType = new(StringComparer.OrdinalIgnoreCase)
{
["ldap"] = ["useSsl", "usernameAttribute", "groupAttribute", "groupToRoleMapping", "timeoutSeconds"],
["saml"] = ["idpSsoUrl", "idpMetadataUrl", "idpSigningCertificate", "assertionConsumerServiceUrl", "attributeMappings", "roleMappings"],
["oidc"] = ["clientSecret", "audience", "scopes", "claimMappings", "roleMappings", "requireHttpsMetadata"],
["standard"] = []
};
private readonly IHttpClientFactory? _httpClientFactory;
private readonly ILogger<IdentityProviderManagementService> _logger;
// In-memory store keyed by (tenantId, id)
private readonly Dictionary<Guid, IdentityProviderConfigEntry> _store = new();
private readonly object _lock = new();
public IdentityProviderManagementService(
ILogger<IdentityProviderManagementService> logger,
IHttpClientFactory? httpClientFactory = null)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
}
public Task<IReadOnlyList<IdentityProviderConfigDto>> ListAsync(string tenantId, CancellationToken cancellationToken)
{
lock (_lock)
{
var items = _store.Values
.Where(e => string.Equals(e.TenantId, tenantId, StringComparison.OrdinalIgnoreCase))
.OrderBy(e => e.Name)
.Select(MapToDto)
.ToList();
return Task.FromResult<IReadOnlyList<IdentityProviderConfigDto>>(items);
}
}
public Task<IdentityProviderConfigDto?> GetAsync(string tenantId, Guid id, CancellationToken cancellationToken)
{
lock (_lock)
{
if (_store.TryGetValue(id, out var entry) &&
string.Equals(entry.TenantId, tenantId, StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult<IdentityProviderConfigDto?>(MapToDto(entry));
}
return Task.FromResult<IdentityProviderConfigDto?>(null);
}
}
public Task<IdentityProviderConfigDto> CreateAsync(
string tenantId,
string actorId,
CreateIdentityProviderRequest request,
CancellationToken cancellationToken)
{
ValidateType(request.Type);
ValidateRequiredFields(request.Type, request.Configuration);
if (string.IsNullOrWhiteSpace(request.Name))
throw new InvalidOperationException("Name is required.");
lock (_lock)
{
var exists = _store.Values.Any(e =>
string.Equals(e.TenantId, tenantId, StringComparison.OrdinalIgnoreCase) &&
string.Equals(e.Name, request.Name, StringComparison.OrdinalIgnoreCase));
if (exists)
throw new InvalidOperationException($"An identity provider with name '{request.Name}' already exists for this tenant.");
var now = DateTimeOffset.UtcNow;
var entry = new IdentityProviderConfigEntry
{
Id = Guid.NewGuid(),
TenantId = tenantId,
Name = request.Name.Trim(),
Type = request.Type.Trim().ToLowerInvariant(),
Enabled = request.Enabled,
Configuration = new Dictionary<string, string?>(request.Configuration, StringComparer.OrdinalIgnoreCase),
Description = request.Description,
CreatedAt = now,
UpdatedAt = now,
CreatedBy = actorId,
UpdatedBy = actorId
};
_store[entry.Id] = entry;
_logger.LogInformation("Created identity provider '{Name}' (type={Type}) for tenant '{TenantId}'",
entry.Name, entry.Type, tenantId);
return Task.FromResult(MapToDto(entry));
}
}
public Task<IdentityProviderConfigDto?> UpdateAsync(
string tenantId,
string actorId,
Guid id,
UpdateIdentityProviderRequest request,
CancellationToken cancellationToken)
{
lock (_lock)
{
if (!_store.TryGetValue(id, out var entry) ||
!string.Equals(entry.TenantId, tenantId, StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult<IdentityProviderConfigDto?>(null);
}
if (request.Enabled.HasValue)
entry.Enabled = request.Enabled.Value;
if (request.Configuration is not null)
{
ValidateRequiredFields(entry.Type, request.Configuration);
entry.Configuration = new Dictionary<string, string?>(request.Configuration, StringComparer.OrdinalIgnoreCase);
}
if (request.Description is not null)
entry.Description = request.Description;
entry.UpdatedAt = DateTimeOffset.UtcNow;
entry.UpdatedBy = actorId;
_logger.LogInformation("Updated identity provider '{Name}' ({Id}) for tenant '{TenantId}'",
entry.Name, id, tenantId);
return Task.FromResult<IdentityProviderConfigDto?>(MapToDto(entry));
}
}
public Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken)
{
lock (_lock)
{
if (_store.TryGetValue(id, out var entry) &&
string.Equals(entry.TenantId, tenantId, StringComparison.OrdinalIgnoreCase))
{
_store.Remove(id);
_logger.LogInformation("Deleted identity provider '{Name}' ({Id}) for tenant '{TenantId}'",
entry.Name, id, tenantId);
return Task.FromResult(true);
}
return Task.FromResult(false);
}
}
public Task<IdentityProviderConfigDto?> SetEnabledAsync(
string tenantId,
string actorId,
Guid id,
bool enabled,
CancellationToken cancellationToken)
{
lock (_lock)
{
if (!_store.TryGetValue(id, out var entry) ||
!string.Equals(entry.TenantId, tenantId, StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult<IdentityProviderConfigDto?>(null);
}
entry.Enabled = enabled;
entry.UpdatedAt = DateTimeOffset.UtcNow;
entry.UpdatedBy = actorId;
return Task.FromResult<IdentityProviderConfigDto?>(MapToDto(entry));
}
}
public async Task<TestConnectionResult> TestConnectionAsync(
TestConnectionRequest request,
CancellationToken cancellationToken)
{
ValidateType(request.Type);
var sw = Stopwatch.StartNew();
try
{
return request.Type.ToLowerInvariant() switch
{
"ldap" => await TestLdapConnectionAsync(request.Configuration, cancellationToken).ConfigureAwait(false),
"saml" => await TestSamlConnectionAsync(request.Configuration, cancellationToken).ConfigureAwait(false),
"oidc" => await TestOidcConnectionAsync(request.Configuration, cancellationToken).ConfigureAwait(false),
"standard" => new TestConnectionResult(true, "Standard provider requires no external connection.", sw.ElapsedMilliseconds),
_ => new TestConnectionResult(false, $"Unknown provider type: {request.Type}", sw.ElapsedMilliseconds)
};
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogWarning(ex, "Connection test failed for type '{Type}'", request.Type);
return new TestConnectionResult(false, $"Connection test failed: {ex.Message}", sw.ElapsedMilliseconds);
}
}
public Task<TestConnectionResult> GetHealthAsync(
string tenantId,
Guid id,
CancellationToken cancellationToken)
{
lock (_lock)
{
if (!_store.TryGetValue(id, out var entry) ||
!string.Equals(entry.TenantId, tenantId, StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(new TestConnectionResult(false, "Provider not found.", null));
}
if (!entry.Enabled)
{
return Task.FromResult(new TestConnectionResult(false, "Provider is disabled.", null));
}
return Task.FromResult(new TestConnectionResult(true, "Provider is active.", null));
}
}
public Task<IReadOnlyList<IdentityProviderTypeSchema>> GetTypeSchemasAsync(CancellationToken cancellationToken)
{
var schemas = new List<IdentityProviderTypeSchema>
{
BuildTypeSchema("standard", "Standard (Built-in)"),
BuildTypeSchema("ldap", "LDAP / Active Directory"),
BuildTypeSchema("saml", "SAML 2.0"),
BuildTypeSchema("oidc", "OpenID Connect")
};
return Task.FromResult<IReadOnlyList<IdentityProviderTypeSchema>>(schemas);
}
private static IdentityProviderTypeSchema BuildTypeSchema(string type, string displayName)
{
var required = (RequiredFieldsByType.TryGetValue(type, out var reqFields) ? reqFields : [])
.Select(f => new IdentityProviderFieldSchema(f, FormatDisplayName(f), InferFieldType(f), null, null))
.ToList();
var optional = (OptionalFieldsByType.TryGetValue(type, out var optFields) ? optFields : [])
.Select(f => new IdentityProviderFieldSchema(f, FormatDisplayName(f), InferFieldType(f), InferDefault(f), null))
.ToList();
return new IdentityProviderTypeSchema(type, displayName, required, optional);
}
private static string FormatDisplayName(string fieldName)
{
// Convert camelCase to Title Case
var chars = new List<char>();
for (var i = 0; i < fieldName.Length; i++)
{
if (i == 0)
{
chars.Add(char.ToUpperInvariant(fieldName[i]));
}
else if (char.IsUpper(fieldName[i]))
{
chars.Add(' ');
chars.Add(fieldName[i]);
}
else
{
chars.Add(fieldName[i]);
}
}
return new string(chars.ToArray());
}
private static string InferFieldType(string fieldName)
{
if (fieldName.Contains("password", StringComparison.OrdinalIgnoreCase) ||
fieldName.Contains("secret", StringComparison.OrdinalIgnoreCase))
return "secret";
if (fieldName.Contains("port", StringComparison.OrdinalIgnoreCase) ||
fieldName.Contains("timeout", StringComparison.OrdinalIgnoreCase))
return "number";
if (fieldName.StartsWith("use", StringComparison.OrdinalIgnoreCase) ||
fieldName.StartsWith("require", StringComparison.OrdinalIgnoreCase))
return "boolean";
if (fieldName.Contains("url", StringComparison.OrdinalIgnoreCase) ||
fieldName.Contains("authority", StringComparison.OrdinalIgnoreCase))
return "url";
if (fieldName.Contains("certificate", StringComparison.OrdinalIgnoreCase) ||
fieldName.Contains("Mapping", StringComparison.OrdinalIgnoreCase))
return "textarea";
return "text";
}
private static string? InferDefault(string fieldName)
{
return fieldName switch
{
"useSsl" => "false",
"usernameAttribute" => "uid",
"groupAttribute" => "memberOf",
"scopes" => "openid profile email",
"requireHttpsMetadata" => "true",
"timeoutSeconds" => "30",
_ => null
};
}
private static void ValidateType(string type)
{
if (!ValidTypes.Contains(type))
throw new InvalidOperationException($"Invalid provider type '{type}'. Valid types: {string.Join(", ", ValidTypes)}.");
}
private static void ValidateRequiredFields(string type, Dictionary<string, string?> config)
{
if (!RequiredFieldsByType.TryGetValue(type, out var required))
return;
var missing = required
.Where(field => !config.ContainsKey(field) || string.IsNullOrWhiteSpace(config[field]))
.ToList();
if (missing.Count > 0)
throw new InvalidOperationException($"Missing required fields for type '{type}': {string.Join(", ", missing)}.");
}
private async Task<TestConnectionResult> TestLdapConnectionAsync(
Dictionary<string, string?> config,
CancellationToken cancellationToken)
{
var host = config.GetValueOrDefault("host") ?? throw new InvalidOperationException("LDAP host is required.");
var portStr = config.GetValueOrDefault("port") ?? "389";
if (!int.TryParse(portStr, out var port))
throw new InvalidOperationException("LDAP port must be a valid integer.");
var sw = Stopwatch.StartNew();
using var tcp = new TcpClient();
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(10));
await tcp.ConnectAsync(host, port, cts.Token).ConfigureAwait(false);
sw.Stop();
return new TestConnectionResult(true, $"TCP connection to {host}:{port} succeeded.", sw.ElapsedMilliseconds);
}
private async Task<TestConnectionResult> TestSamlConnectionAsync(
Dictionary<string, string?> config,
CancellationToken cancellationToken)
{
var metadataUrl = config.GetValueOrDefault("idpMetadataUrl");
if (string.IsNullOrWhiteSpace(metadataUrl))
{
// If no metadata URL, just validate that required fields exist
var entityId = config.GetValueOrDefault("idpEntityId");
if (string.IsNullOrWhiteSpace(entityId))
throw new InvalidOperationException("Either idpMetadataUrl or idpEntityId is required for SAML.");
return new TestConnectionResult(true, "SAML configuration validated (no metadata URL to test).", null);
}
var sw = Stopwatch.StartNew();
var httpClient = _httpClientFactory?.CreateClient("idp-test") ?? new HttpClient();
try
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(15));
var response = await httpClient.GetAsync(metadataUrl, cts.Token).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(cts.Token).ConfigureAwait(false);
if (!content.Contains("EntityDescriptor", StringComparison.OrdinalIgnoreCase))
{
return new TestConnectionResult(false, "SAML metadata URL responded but content does not appear to be valid SAML metadata.", sw.ElapsedMilliseconds);
}
return new TestConnectionResult(true, $"SAML metadata fetched successfully from {metadataUrl}.", sw.ElapsedMilliseconds);
}
finally
{
if (_httpClientFactory is null)
httpClient.Dispose();
}
}
private async Task<TestConnectionResult> TestOidcConnectionAsync(
Dictionary<string, string?> config,
CancellationToken cancellationToken)
{
var authority = config.GetValueOrDefault("authority") ?? throw new InvalidOperationException("OIDC authority is required.");
var discoveryUrl = authority.TrimEnd('/') + "/.well-known/openid-configuration";
var sw = Stopwatch.StartNew();
var httpClient = _httpClientFactory?.CreateClient("idp-test") ?? new HttpClient();
try
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(15));
var response = await httpClient.GetAsync(discoveryUrl, cts.Token).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(cts.Token).ConfigureAwait(false);
// Basic validation: should contain issuer field
if (!content.Contains("issuer", StringComparison.OrdinalIgnoreCase))
{
return new TestConnectionResult(false, "OIDC discovery endpoint responded but content does not appear to be a valid OpenID configuration.", sw.ElapsedMilliseconds);
}
return new TestConnectionResult(true, $"OIDC discovery document fetched successfully from {discoveryUrl}.", sw.ElapsedMilliseconds);
}
finally
{
if (_httpClientFactory is null)
httpClient.Dispose();
}
}
private static IdentityProviderConfigDto MapToDto(IdentityProviderConfigEntry entry)
{
return new IdentityProviderConfigDto(
entry.Id,
entry.Name,
entry.Type,
entry.Enabled,
new Dictionary<string, string?>(entry.Configuration),
entry.Description,
entry.Enabled ? "healthy" : "disabled",
entry.CreatedAt,
entry.UpdatedAt,
entry.CreatedBy,
entry.UpdatedBy);
}
private sealed class IdentityProviderConfigEntry
{
public Guid Id { get; set; }
public string TenantId { get; set; } = null!;
public string Name { get; set; } = null!;
public string Type { get; set; } = null!;
public bool Enabled { get; set; }
public Dictionary<string, string?> Configuration { get; set; } = new();
public string? Description { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
public string CreatedBy { get; set; } = null!;
public string UpdatedBy { get; set; } = null!;
}
}

View File

@@ -0,0 +1,102 @@
using System.Collections.Concurrent;
namespace StellaOps.Platform.WebService.Services;
/// <summary>
/// In-memory implementation of <see cref="ITranslationStore"/> for development/testing.
/// </summary>
public sealed class InMemoryTranslationStore : ITranslationStore
{
// Key: "{tenantId}:{locale}:{key}" -> value
private readonly ConcurrentDictionary<string, string> _store = new(StringComparer.Ordinal);
public Task<IReadOnlyDictionary<string, string>> GetAllAsync(
string tenantId, string locale, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var prefix = $"{tenantId}:{locale}:";
var result = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var (compositeKey, value) in _store)
{
if (compositeKey.StartsWith(prefix, StringComparison.Ordinal))
{
var key = compositeKey[prefix.Length..];
result[key] = value;
}
}
return Task.FromResult<IReadOnlyDictionary<string, string>>(result);
}
public Task<IReadOnlyDictionary<string, string>> GetByPrefixAsync(
string tenantId, string locale, string keyPrefix, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var storePrefix = $"{tenantId}:{locale}:";
var dotPrefix = keyPrefix.EndsWith('.') ? keyPrefix : keyPrefix + ".";
var result = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var (compositeKey, value) in _store)
{
if (compositeKey.StartsWith(storePrefix, StringComparison.Ordinal))
{
var key = compositeKey[storePrefix.Length..];
if (key.StartsWith(dotPrefix, StringComparison.Ordinal))
{
result[key] = value;
}
}
}
return Task.FromResult<IReadOnlyDictionary<string, string>>(result);
}
public Task UpsertAsync(string tenantId, string locale, string key, string value, string actor,
CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
_store[$"{tenantId}:{locale}:{key}"] = value;
return Task.CompletedTask;
}
public Task UpsertBatchAsync(string tenantId, string locale, IReadOnlyDictionary<string, string> strings,
string actor, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
foreach (var (key, value) in strings)
{
_store[$"{tenantId}:{locale}:{key}"] = value;
}
return Task.CompletedTask;
}
public Task<bool> DeleteAsync(string tenantId, string locale, string key, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
return Task.FromResult(_store.TryRemove($"{tenantId}:{locale}:{key}", out _));
}
public Task<IReadOnlyList<string>> GetAvailableLocalesAsync(string tenantId, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var prefix = $"{tenantId}:";
var locales = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var compositeKey in _store.Keys)
{
if (compositeKey.StartsWith(prefix, StringComparison.Ordinal))
{
var afterTenant = compositeKey[prefix.Length..];
var colonIdx = afterTenant.IndexOf(':');
if (colonIdx > 0)
{
locales.Add(afterTenant[..colonIdx]);
}
}
}
return Task.FromResult<IReadOnlyList<string>>(locales.OrderBy(l => l).ToList());
}
}

View File

@@ -13,6 +13,43 @@ namespace StellaOps.Platform.WebService.Services;
public sealed class PlatformPreferencesService
{
private static readonly Dictionary<string, string> SupportedLocaleMap = new(StringComparer.OrdinalIgnoreCase)
{
["en-US"] = "en-US",
["en_US"] = "en-US",
["en"] = "en-US",
["de-DE"] = "de-DE",
["de_DE"] = "de-DE",
["de"] = "de-DE",
["bg-BG"] = "bg-BG",
["bg_BG"] = "bg-BG",
["bg"] = "bg-BG",
["ru-RU"] = "ru-RU",
["ru_RU"] = "ru-RU",
["ru"] = "ru-RU",
["es-ES"] = "es-ES",
["es_ES"] = "es-ES",
["es"] = "es-ES",
["fr-FR"] = "fr-FR",
["fr_FR"] = "fr-FR",
["fr"] = "fr-FR",
["uk-UA"] = "uk-UA",
["uk_UA"] = "uk-UA",
["uk"] = "uk-UA",
["ua"] = "uk-UA",
["zh-TW"] = "zh-TW",
["zh_TW"] = "zh-TW",
["zh-Hant"] = "zh-TW",
["zh_Hant"] = "zh-TW",
["zh-CN"] = "zh-CN",
["zh_CN"] = "zh-CN",
["zh-Hans"] = "zh-CN",
["zh_Hans"] = "zh-CN",
["zh"] = "zh-CN",
};
private const string LocalePreferenceKey = "locale";
private static readonly JsonObject DefaultPreferences = new()
{
["layout"] = "default",
@@ -41,16 +78,7 @@ public sealed class PlatformPreferencesService
PlatformRequestContext context,
CancellationToken cancellationToken)
{
var preferences = store.GetOrCreate(context.TenantId, context.ActorId, () =>
{
var now = timeProvider.GetUtcNow();
return new PlatformDashboardPreferences(
TenantId: context.TenantId,
ActorId: context.ActorId,
Preferences: ClonePreferences(DefaultPreferences),
UpdatedAt: now,
UpdatedBy: context.ActorId);
});
var preferences = GetOrCreatePreferences(context);
return Task.FromResult(preferences with { Preferences = ClonePreferences(preferences.Preferences) });
}
@@ -62,11 +90,19 @@ public sealed class PlatformPreferencesService
{
ArgumentNullException.ThrowIfNull(request);
var existing = GetOrCreatePreferences(context);
var updatedPreferences = ClonePreferences(request.Preferences);
if (!updatedPreferences.ContainsKey(LocalePreferenceKey) &&
existing.Preferences.TryGetPropertyValue(LocalePreferenceKey, out var existingLocaleValue))
{
updatedPreferences[LocalePreferenceKey] = existingLocaleValue?.DeepClone();
}
var now = timeProvider.GetUtcNow();
var preferences = new PlatformDashboardPreferences(
TenantId: context.TenantId,
ActorId: context.ActorId,
Preferences: ClonePreferences(request.Preferences),
Preferences: updatedPreferences,
UpdatedAt: now,
UpdatedBy: context.ActorId);
@@ -76,6 +112,56 @@ public sealed class PlatformPreferencesService
return Task.FromResult(preferences with { Preferences = ClonePreferences(preferences.Preferences) });
}
public Task<PlatformLanguagePreference> GetLanguagePreferenceAsync(
PlatformRequestContext context,
CancellationToken cancellationToken)
{
var preferences = GetOrCreatePreferences(context);
var locale = TryNormalizeLocale(preferences.Preferences[LocalePreferenceKey]?.GetValue<string>());
return Task.FromResult(new PlatformLanguagePreference(
TenantId: context.TenantId,
ActorId: context.ActorId,
Locale: locale,
UpdatedAt: preferences.UpdatedAt,
UpdatedBy: preferences.UpdatedBy));
}
public Task<PlatformLanguagePreference> UpsertLanguagePreferenceAsync(
PlatformRequestContext context,
PlatformLanguagePreferenceRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var normalizedLocale = NormalizeLocaleOrThrow(request.Locale);
var existing = GetOrCreatePreferences(context);
var updatedPreferences = ClonePreferences(existing.Preferences);
updatedPreferences[LocalePreferenceKey] = normalizedLocale;
var now = timeProvider.GetUtcNow();
var updated = existing with
{
Preferences = updatedPreferences,
UpdatedAt = now,
UpdatedBy = context.ActorId
};
store.Upsert(context.TenantId, context.ActorId, updated);
logger.LogInformation(
"Updated locale preference for tenant {TenantId} actor {ActorId} to {Locale}.",
context.TenantId,
context.ActorId,
normalizedLocale);
return Task.FromResult(new PlatformLanguagePreference(
TenantId: context.TenantId,
ActorId: context.ActorId,
Locale: normalizedLocale,
UpdatedAt: now,
UpdatedBy: context.ActorId));
}
public Task<IReadOnlyList<PlatformDashboardProfile>> GetProfilesAsync(
PlatformRequestContext context,
CancellationToken cancellationToken)
@@ -135,6 +221,44 @@ public sealed class PlatformPreferencesService
return Task.FromResult(profile with { Preferences = ClonePreferences(profile.Preferences) });
}
private PlatformDashboardPreferences GetOrCreatePreferences(PlatformRequestContext context)
{
return store.GetOrCreate(context.TenantId, context.ActorId, () =>
{
var now = timeProvider.GetUtcNow();
return new PlatformDashboardPreferences(
TenantId: context.TenantId,
ActorId: context.ActorId,
Preferences: ClonePreferences(DefaultPreferences),
UpdatedAt: now,
UpdatedBy: context.ActorId);
});
}
private static string NormalizeLocaleOrThrow(string? locale)
{
var normalized = TryNormalizeLocale(locale);
if (normalized is null)
{
throw new InvalidOperationException("locale is required and must be one of: en-US, de-DE, bg-BG, ru-RU, es-ES, fr-FR, uk-UA, zh-TW, zh-CN.");
}
return normalized;
}
private static string? TryNormalizeLocale(string? locale)
{
if (string.IsNullOrWhiteSpace(locale))
{
return null;
}
var trimmed = locale.Trim();
return SupportedLocaleMap.TryGetValue(trimmed, out var normalized)
? normalized
: null;
}
private static JsonObject ClonePreferences(JsonObject? source)
{
if (source is null)

View File

@@ -24,7 +24,6 @@ public sealed class PlatformSearchService
private readonly PlatformCache cache;
private readonly PlatformAggregationMetrics metrics;
private readonly TimeProvider timeProvider;
private readonly PlatformSearchOptions searchOptions;
private readonly PlatformCacheOptions cacheOptions;
private readonly ILogger<PlatformSearchService> logger;
@@ -32,13 +31,11 @@ public sealed class PlatformSearchService
public PlatformSearchService(
PlatformCache cache,
PlatformAggregationMetrics metrics,
TimeProvider timeProvider,
IOptions<PlatformServiceOptions> options,
ILogger<PlatformSearchService> logger)
{
this.cache = cache ?? throw new ArgumentNullException(nameof(cache));
this.metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
this.searchOptions = options?.Value.Search ?? throw new ArgumentNullException(nameof(options));
this.cacheOptions = options.Value.Cache;
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -95,11 +92,9 @@ public sealed class PlatformSearchService
.ToArray();
var total = ordered.Length;
var now = timeProvider.GetUtcNow();
var items = ordered
.Skip(offset)
.Take(limit)
.Select(item => item with { UpdatedAt = now })
.ToArray();
return new PlatformSearchResult(items, total, limit, offset, query);

View File

@@ -0,0 +1,150 @@
using Microsoft.Extensions.Logging;
using StellaOps.Localization;
namespace StellaOps.Platform.WebService.Services;
/// <summary>
/// Merges embedded translations from the <see cref="TranslationRegistry"/> with DB overrides
/// from the <see cref="ITranslationStore"/>. DB values take precedence.
/// Also acts as the <see cref="ITranslationBundleProvider"/> for DB translations (priority 100).
/// </summary>
public sealed class PlatformTranslationService : ITranslationBundleProvider
{
private readonly ITranslationStore _store;
private readonly TranslationRegistry _registry;
private readonly ILogger<PlatformTranslationService> _logger;
public int Priority => 100;
public PlatformTranslationService(
ITranslationStore store,
TranslationRegistry registry,
ILogger<PlatformTranslationService> logger)
{
_store = store ?? throw new ArgumentNullException(nameof(store));
_registry = registry ?? throw new ArgumentNullException(nameof(registry));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Returns the fully merged bundle for a locale: embedded base + DB overrides.
/// Used to serve the frontend via <c>/platform/i18n/{locale}.json</c>.
/// </summary>
public async Task<IReadOnlyDictionary<string, string>> GetMergedBundleAsync(
string tenantId, string locale, CancellationToken ct = default)
{
// Start with all embedded translations for the locale
var baseBundle = _registry.GetBundle(locale);
var merged = new Dictionary<string, string>(baseBundle, StringComparer.Ordinal);
// Overlay DB translations (system-level first, then tenant-specific)
var systemOverrides = await _store.GetAllAsync("_system", locale, ct).ConfigureAwait(false);
foreach (var (key, value) in systemOverrides)
{
merged[key] = value;
}
if (!string.Equals(tenantId, "_system", StringComparison.Ordinal))
{
var tenantOverrides = await _store.GetAllAsync(tenantId, locale, ct).ConfigureAwait(false);
foreach (var (key, value) in tenantOverrides)
{
merged[key] = value;
}
}
return merged;
}
/// <summary>
/// Returns the merged bundle filtered by namespace prefix.
/// </summary>
public async Task<IReadOnlyDictionary<string, string>> GetMergedBundleAsync(
string tenantId, string locale, string namespacePrefix, CancellationToken ct = default)
{
var prefix = namespacePrefix.EndsWith('.') ? namespacePrefix : namespacePrefix + ".";
// Start with embedded translations filtered by prefix
var baseBundle = _registry.GetBundle(locale, namespacePrefix);
var merged = new Dictionary<string, string>(baseBundle, StringComparer.Ordinal);
// Overlay DB translations
var systemOverrides = await _store.GetByPrefixAsync("_system", locale, namespacePrefix, ct)
.ConfigureAwait(false);
foreach (var (key, value) in systemOverrides)
{
merged[key] = value;
}
if (!string.Equals(tenantId, "_system", StringComparison.Ordinal))
{
var tenantOverrides = await _store.GetByPrefixAsync(tenantId, locale, namespacePrefix, ct)
.ConfigureAwait(false);
foreach (var (key, value) in tenantOverrides)
{
merged[key] = value;
}
}
return merged;
}
/// <summary>
/// Gets all locales that have translations (embedded + DB).
/// </summary>
public async Task<IReadOnlyList<string>> GetAllLocalesAsync(
string tenantId, CancellationToken ct = default)
{
var locales = new HashSet<string>(_registry.GetLoadedLocales(), StringComparer.OrdinalIgnoreCase);
var dbLocales = await _store.GetAvailableLocalesAsync("_system", ct).ConfigureAwait(false);
foreach (var locale in dbLocales)
{
locales.Add(locale);
}
if (!string.Equals(tenantId, "_system", StringComparison.Ordinal))
{
var tenantLocales = await _store.GetAvailableLocalesAsync(tenantId, ct).ConfigureAwait(false);
foreach (var locale in tenantLocales)
{
locales.Add(locale);
}
}
return locales.OrderBy(l => l).ToList();
}
// ITranslationBundleProvider — used during LoadTranslationsAsync() to merge DB into the registry
public async Task<IReadOnlyDictionary<string, string>> LoadAsync(string locale, CancellationToken ct)
{
try
{
var systemTranslations = await _store.GetAllAsync("_system", locale, ct).ConfigureAwait(false);
if (systemTranslations.Count > 0)
{
_logger.LogDebug("Loaded {Count} DB translations for locale {Locale}", systemTranslations.Count, locale);
}
return systemTranslations;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load DB translations for locale {Locale}", locale);
return new Dictionary<string, string>();
}
}
public async Task<IReadOnlyList<string>> GetAvailableLocalesAsync(CancellationToken ct)
{
try
{
return await _store.GetAvailableLocalesAsync("_system", ct).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get available locales from DB");
return [];
}
}
}

View File

@@ -46,20 +46,31 @@ public sealed class PostgresEnvironmentSettingsStore : IEnvironmentSettingsStore
ct.ThrowIfCancellationRequested();
await using var connection = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
await using var dbContext = PlatformDbContextFactory.Create(
connection, DefaultCommandTimeoutSeconds, PlatformDbContextFactory.DefaultSchemaName);
var entities = await dbContext.EnvironmentSettings
.AsNoTracking()
.OrderBy(e => e.Key)
.ToListAsync(ct)
.ConfigureAwait(false);
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var entity in entities)
Dictionary<string, string> dict;
try
{
dict[entity.Key] = entity.Value;
await using var connection = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
await using var dbContext = PlatformDbContextFactory.Create(
connection, DefaultCommandTimeoutSeconds, PlatformDbContextFactory.DefaultSchemaName);
var entities = await dbContext.EnvironmentSettings
.AsNoTracking()
.OrderBy(e => e.Key)
.ToListAsync(ct)
.ConfigureAwait(false);
dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var entity in entities)
{
dict[entity.Key] = entity.Value;
}
}
catch (Npgsql.PostgresException ex) when (ex.SqlState == "42P01")
{
// Table does not exist yet (migration not applied). Return empty so
// Layer 1 (env vars) and Layer 2 (config) still compose a valid response.
_logger.LogWarning("platform.environment_settings table does not exist yet; returning empty DB layer");
dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
lock (_cacheLock)

View File

@@ -0,0 +1,170 @@
using Microsoft.Extensions.Logging;
using Npgsql;
namespace StellaOps.Platform.WebService.Services;
/// <summary>
/// PostgreSQL implementation of <see cref="ITranslationStore"/>.
/// </summary>
public sealed class PostgresTranslationStore : ITranslationStore
{
private readonly NpgsqlDataSource _dataSource;
private readonly ILogger<PostgresTranslationStore> _logger;
private const string SelectAllSql = """
SELECT key, value
FROM platform.translations
WHERE tenant_id = @tenant_id AND locale = @locale
ORDER BY key
""";
private const string SelectByPrefixSql = """
SELECT key, value
FROM platform.translations
WHERE tenant_id = @tenant_id AND locale = @locale AND key LIKE @prefix
ORDER BY key
""";
private const string UpsertSql = """
INSERT INTO platform.translations (tenant_id, locale, key, value, updated_by, updated_at)
VALUES (@tenant_id, @locale, @key, @value, @updated_by, now())
ON CONFLICT (tenant_id, locale, key) DO UPDATE
SET value = EXCLUDED.value, updated_by = EXCLUDED.updated_by, updated_at = now()
""";
private const string DeleteSql = """
DELETE FROM platform.translations
WHERE tenant_id = @tenant_id AND locale = @locale AND key = @key
""";
private const string SelectLocalesSql = """
SELECT DISTINCT locale
FROM platform.translations
WHERE tenant_id = @tenant_id
ORDER BY locale
""";
public PostgresTranslationStore(
NpgsqlDataSource dataSource,
ILogger<PostgresTranslationStore>? logger = null)
{
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<PostgresTranslationStore>.Instance;
}
public async Task<IReadOnlyDictionary<string, string>> GetAllAsync(
string tenantId, string locale, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var result = new Dictionary<string, string>(StringComparer.Ordinal);
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(SelectAllSql, conn);
cmd.Parameters.AddWithValue("tenant_id", tenantId);
cmd.Parameters.AddWithValue("locale", locale);
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
while (await reader.ReadAsync(ct).ConfigureAwait(false))
{
result[reader.GetString(0)] = reader.GetString(1);
}
return result;
}
public async Task<IReadOnlyDictionary<string, string>> GetByPrefixAsync(
string tenantId, string locale, string keyPrefix, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var result = new Dictionary<string, string>(StringComparer.Ordinal);
var prefix = keyPrefix.EndsWith('.') ? keyPrefix : keyPrefix + ".";
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(SelectByPrefixSql, conn);
cmd.Parameters.AddWithValue("tenant_id", tenantId);
cmd.Parameters.AddWithValue("locale", locale);
cmd.Parameters.AddWithValue("prefix", prefix + "%");
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
while (await reader.ReadAsync(ct).ConfigureAwait(false))
{
result[reader.GetString(0)] = reader.GetString(1);
}
return result;
}
public async Task UpsertAsync(string tenantId, string locale, string key, string value, string actor,
CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(UpsertSql, conn);
cmd.Parameters.AddWithValue("tenant_id", tenantId);
cmd.Parameters.AddWithValue("locale", locale);
cmd.Parameters.AddWithValue("key", key);
cmd.Parameters.AddWithValue("value", value);
cmd.Parameters.AddWithValue("updated_by", actor);
await cmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
_logger.LogDebug("Upserted translation {Key} for {Locale} tenant {TenantId}", key, locale, tenantId);
}
public async Task UpsertBatchAsync(string tenantId, string locale, IReadOnlyDictionary<string, string> strings,
string actor, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
await using var transaction = await conn.BeginTransactionAsync(ct).ConfigureAwait(false);
foreach (var (key, value) in strings)
{
await using var cmd = new NpgsqlCommand(UpsertSql, conn, transaction);
cmd.Parameters.AddWithValue("tenant_id", tenantId);
cmd.Parameters.AddWithValue("locale", locale);
cmd.Parameters.AddWithValue("key", key);
cmd.Parameters.AddWithValue("value", value);
cmd.Parameters.AddWithValue("updated_by", actor);
await cmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
}
await transaction.CommitAsync(ct).ConfigureAwait(false);
_logger.LogDebug("Upserted {Count} translations for {Locale} tenant {TenantId}",
strings.Count, locale, tenantId);
}
public async Task<bool> DeleteAsync(string tenantId, string locale, string key, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(DeleteSql, conn);
cmd.Parameters.AddWithValue("tenant_id", tenantId);
cmd.Parameters.AddWithValue("locale", locale);
cmd.Parameters.AddWithValue("key", key);
var affected = await cmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
return affected > 0;
}
public async Task<IReadOnlyList<string>> GetAvailableLocalesAsync(string tenantId, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var locales = new List<string>();
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(SelectLocalesSql, conn);
cmd.Parameters.AddWithValue("tenant_id", tenantId);
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
while (await reader.ReadAsync(ct).ConfigureAwait(false))
{
locales.Add(reader.GetString(0));
}
return locales;
}
}

View File

@@ -35,6 +35,11 @@
<ProjectReference Include="..\..\Policy\__Libraries\StellaOps.Policy.Persistence\StellaOps.Policy.Persistence.csproj" />
<ProjectReference Include="..\..\Notify\__Libraries\StellaOps.Notify.Persistence\StellaOps.Notify.Persistence.csproj" />
<ProjectReference Include="..\..\Excititor\__Libraries\StellaOps.Excititor.Persistence\StellaOps.Excititor.Persistence.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Localization\StellaOps.Localization.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Translations\*.json" />
</ItemGroup>
<PropertyGroup Label="StellaOpsReleaseVersion">

View File

@@ -36,4 +36,12 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| TASK-030-017 | BLOCKED | Stored procedures delivered; validation blocked pending ingestion datasets. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| PLATFORM-EF-03-WS | DONE | Sprint `docs/implplan/SPRINT_20260222_096_Platform_dal_to_efcore.md`: converted `PostgresEnvironmentSettingsStore` and `PostgresPlatformContextStore` to EF Core LINQ reads with `AsNoTracking()`, raw SQL upserts. Added `Microsoft.EntityFrameworkCore` package reference. |
| SPRINT_20260224_001-LOC-002 | DONE | Sprint `docs/implplan/SPRINT_20260224_001_Platform_unified_translation_gap_closure.md`: added `057_PlatformTranslations.sql` migration and localization endpoint verification coverage. |
| SPRINT_20260224_002-LOC-102 | DONE | Sprint `docs/implplan/SPRINT_20260224_002_Platform_translation_rollout_phase3_phase4.md`: added/aligned Platform `de-DE.ui.json` locale assets (including locale selector keys) for phase-4 second-locale rollout. |
| SPRINT_20260224_004-LOC-301 | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: added locale bundles for `bg-BG`, `ru-RU`, `es-ES`, `fr-FR`, `zh-TW`, `zh-CN` and locale label key coverage in Platform translation assets. |
| SPRINT_20260224_004-LOC-302 | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: added persisted user locale preference APIs (`GET/PUT /api/v1/platform/preferences/language`) and preference service normalization/persistence wiring. |
| SPRINT_20260224_004-LOC-305 | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: completed locale storage parity by adding `*.platform.json` bundles for all supported locales and full `common` locale coverage in `StellaOps.Localization` translation assets; localization tests now assert common/platform key availability across supported locales. |
| SPRINT_20260224_004-LOC-306 | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: added dedicated `/settings/language` UX wiring that reuses Platform persisted language preference API for authenticated users. |
| SPRINT_20260224_004-LOC-307 | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: added Ukrainian locale support (`uk-UA`) across Platform translation assets and preference normalization aliases (`uk-UA`/`uk_UA`/`uk`/`ua`). |
| SPRINT_20260224_004-LOC-308 | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: platform locale catalog endpoint (`GET /api/v1/platform/localization/locales`) is now consumed by both UI and CLI locale-selection paths. |

View File

@@ -0,0 +1,29 @@
{
"_meta": { "locale": "bg-BG", "namespace": "platform", "version": "1.0" },
"platform.health.status_healthy": "Vsichki sistemi rabotyat normalno.",
"platform.health.status_degraded": "Nyakoi uslugi imat problemi.",
"platform.health.status_unavailable": "Platformata v momenta ne e dostupna.",
"platform.quota.limit_exceeded": "Limitat na kvotata za {0} e nadhvurlen.",
"platform.quota.usage_warning": "Izpolzvaneto e {0}% ot limita na kvotata.",
"platform.quota.reset_at": "Kvotata se nulyava v {0}.",
"platform.onboarding.welcome": "Dobro doshli v StellaOps.",
"platform.onboarding.step_authority": "Konfiguriraite dostavchik na identichnost.",
"platform.onboarding.step_registry": "Svurzhete container registry.",
"platform.onboarding.step_environments": "Definiraite tselevi sredi.",
"platform.onboarding.step_complete": "Nastroikata e zavarshena. Gotovo za rabota.",
"platform.setup.required": "Predi izpolzvane na platformata e nuzhna nachalna nastroika.",
"platform.setup.in_progress": "Nastroikata e v proces.",
"platform.setup.complete": "Nastroikata e zavarshena.",
"platform.context.region_not_found": "Region {0} ne e nameren.",
"platform.context.environment_not_found": "Sreda {0} ne e namerena.",
"platform.migration.started": "Migratsiyata zapochna.",
"platform.migration.completed": "Migratsiyata priklyuchi uspeshno.",
"platform.migration.failed": "Migratsiyata se provali: {0}."
}

View File

@@ -0,0 +1,275 @@
{
"_meta": { "locale": "bg-BG", "namespace": "ui", "version": "1.0" },
"ui.loading.skeleton": "Loading...",
"ui.loading.spinner": "Please wait...",
"ui.loading.slow": "This is taking longer than expected...",
"ui.error.generic": "Something went wrong.",
"ui.error.network": "Network error. Check your connection.",
"ui.error.timeout": "Request timed out. Please try again.",
"ui.error.not_found": "The requested resource was not found.",
"ui.error.unauthorized": "You don't have permission to view this.",
"ui.error.server_error": "Server error. Please try again later.",
"ui.error.try_again": "Try again",
"ui.error.go_back": "Go back",
"ui.offline.banner": "You're offline.",
"ui.offline.description": "Some features may be unavailable.",
"ui.offline.reconnecting": "Reconnecting...",
"ui.offline.reconnected": "Back online.",
"ui.toast.success": "Success",
"ui.toast.info": "Info",
"ui.toast.warning": "Warning",
"ui.toast.error": "Error",
"ui.toast.dismiss": "Dismiss",
"ui.toast.undo": "Undo",
"ui.actions.save": "Save",
"ui.actions.saving": "Saving...",
"ui.actions.saved": "Saved",
"ui.actions.cancel": "Cancel",
"ui.actions.confirm": "Confirm",
"ui.actions.delete": "Delete",
"ui.actions.deleting": "Deleting...",
"ui.actions.deleted": "Deleted",
"ui.actions.submit": "Submit",
"ui.actions.submitting": "Submitting...",
"ui.actions.submitted": "Submitted",
"ui.actions.close": "Close",
"ui.actions.expand": "Expand",
"ui.actions.collapse": "Collapse",
"ui.actions.show_more": "Show more",
"ui.actions.show_less": "Show less",
"ui.actions.retry": "Retry",
"ui.actions.refresh": "Refresh",
"ui.actions.export": "Export",
"ui.actions.search": "Search",
"ui.actions.clear": "Clear",
"ui.actions.view": "View",
"ui.actions.dismiss": "Dismiss",
"ui.actions.show": "Show",
"ui.actions.hide": "Hide",
"ui.actions.sign_in": "Sign in",
"ui.actions.back_to_list": "Back to list",
"ui.actions.load_more": "Load more",
"ui.labels.all": "All",
"ui.labels.title": "Title",
"ui.labels.description": "Description",
"ui.labels.status": "Status",
"ui.labels.score": "Score",
"ui.labels.severity": "Severity",
"ui.labels.details": "Details",
"ui.labels.actions": "Actions",
"ui.labels.type": "Type",
"ui.labels.tags": "Tags",
"ui.labels.filters": "Filters",
"ui.labels.updated": "Updated",
"ui.labels.showing": "Showing",
"ui.labels.of": "of",
"ui.labels.total": "Total",
"ui.labels.not_applicable": "n/a",
"ui.labels.selected": "selected",
"ui.labels.last_updated": "Last updated:",
"ui.labels.expires": "Expires",
"ui.validation.required": "This field is required.",
"ui.validation.invalid": "Invalid value.",
"ui.validation.too_long": "Maximum {max} characters allowed.",
"ui.validation.too_short": "Minimum {min} characters required.",
"ui.validation.invalid_email": "Please enter a valid email address.",
"ui.validation.invalid_url": "Please enter a valid URL.",
"ui.a11y.loading": "Content is loading.",
"ui.a11y.loaded": "Content loaded.",
"ui.a11y.error": "An error occurred.",
"ui.a11y.expanded": "Expanded",
"ui.a11y.collapsed": "Collapsed",
"ui.a11y.selected": "Selected",
"ui.a11y.deselected": "Deselected",
"ui.a11y.required": "Required field",
"ui.a11y.optional": "Optional",
"ui.motion.reduced": "Animations reduced.",
"ui.motion.enabled": "Animations enabled.",
"ui.auth.fresh_active": "Fresh auth: Active",
"ui.auth.fresh_stale": "Fresh auth: Stale",
"ui.locale.label": "Ezik",
"ui.locale.en_us": "Angliiski (USA)",
"ui.locale.de_de": "Nemski (Germania)",
"ui.locale.bg_bg": "Balgarski (Balgaria)",
"ui.locale.ru_ru": "Ruski (Rusia)",
"ui.locale.es_es": "Ispanski (Ispania)",
"ui.locale.fr_fr": "Frenski (Francia)",
"ui.locale.zh_tw": "Kitaiski tradicionen (Taiwan)",
"ui.locale.zh_cn": "Kitaiski oprosten (Kitai)",
"ui.locale.uk_ua": "Ukrainian (Ukraine)",
"ui.settings.language.title": "Ezik",
"ui.settings.language.subtitle": "Zadadeite predpochtaniya ezik na konzolata.",
"ui.settings.language.description": "Promenite se prilagat vednaga v UI.",
"ui.settings.language.selector_label": "Predpochtan ezik",
"ui.settings.language.persisted": "Zapazeno za vashiya akaunt i preizpolzvano ot CLI.",
"ui.settings.language.persisted_error": "Zapazeno lokalno, no sinkhronizatsiyata na akaunta se provali.",
"ui.settings.language.sign_in_hint": "Vlezte v sistema, za da sinkhronizirate tazi nastroika s CLI.",
"ui.first_signal.label": "First signal",
"ui.first_signal.run_prefix": "Run:",
"ui.first_signal.live": "Live",
"ui.first_signal.polling": "Polling",
"ui.first_signal.range_prefix": "Range",
"ui.first_signal.range_separator": "\u2013",
"ui.first_signal.stage_separator": " \u00b7 ",
"ui.first_signal.waiting": "Waiting for first signal\u2026",
"ui.first_signal.not_available": "Signal not available yet.",
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
"ui.first_signal.failed": "Failed to load signal.",
"ui.first_signal.retry": "Retry",
"ui.first_signal.try_again": "Try again",
"ui.first_signal.kind.queued": "Queued",
"ui.first_signal.kind.started": "Started",
"ui.first_signal.kind.phase": "In progress",
"ui.first_signal.kind.blocked": "Blocked",
"ui.first_signal.kind.failed": "Failed",
"ui.first_signal.kind.succeeded": "Succeeded",
"ui.first_signal.kind.canceled": "Canceled",
"ui.first_signal.kind.unavailable": "Unavailable",
"ui.first_signal.kind.unknown": "Signal",
"ui.first_signal.stage.resolve": "Resolving",
"ui.first_signal.stage.fetch": "Fetching",
"ui.first_signal.stage.restore": "Restoring",
"ui.first_signal.stage.analyze": "Analyzing",
"ui.first_signal.stage.policy": "Evaluating policy",
"ui.first_signal.stage.report": "Generating report",
"ui.first_signal.stage.unknown": "Processing",
"ui.first_signal.aria.card_label": "First signal status",
"ui.severity.critical": "Critical",
"ui.severity.high": "High",
"ui.severity.medium": "Medium",
"ui.severity.low": "Low",
"ui.severity.info": "Info",
"ui.severity.none": "None",
"ui.release_orchestrator.title": "Release Orchestrator",
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
"ui.risk_dashboard.title": "Risk Profiles",
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
"ui.risk_dashboard.up_to_date": "Up to date",
"ui.risk_dashboard.last_computation": "Last Computation",
"ui.risk_dashboard.search_placeholder": "Title contains",
"ui.risk_dashboard.evaluated": "Evaluated",
"ui.risk_dashboard.risks_suffix": "risks.",
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
"ui.findings.title": "Findings",
"ui.findings.search_placeholder": "Search findings...",
"ui.findings.clear_filters": "Clear Filters",
"ui.findings.bulk_triage": "Bulk Triage",
"ui.findings.export_all": "Export all findings",
"ui.findings.export_selected": "Export selected findings",
"ui.findings.select_all": "Select all findings",
"ui.findings.trust": "Trust",
"ui.findings.advisory": "Advisory",
"ui.findings.package": "Package",
"ui.findings.flags": "Flags",
"ui.findings.why": "Why",
"ui.findings.select": "Select",
"ui.findings.no_findings": "No findings to display.",
"ui.findings.no_match": "No findings match the current filters.",
"ui.sources_dashboard.title": "Sources Dashboard",
"ui.sources_dashboard.verifying": "Verifying...",
"ui.sources_dashboard.verify_24h": "Verify last 24h",
"ui.sources_dashboard.loading_aoc": "Loading AOC metrics...",
"ui.sources_dashboard.pass_fail_title": "AOC Pass/Fail",
"ui.sources_dashboard.pass_rate": "Pass Rate",
"ui.sources_dashboard.passed": "Passed",
"ui.sources_dashboard.failed": "Failed",
"ui.sources_dashboard.recent_violations": "Recent Violations",
"ui.sources_dashboard.no_violations": "No violations in time window",
"ui.sources_dashboard.throughput_title": "Ingest Throughput",
"ui.sources_dashboard.docs_per_min": "docs/min",
"ui.sources_dashboard.avg_ms": "avg ms",
"ui.sources_dashboard.p95_ms": "p95 ms",
"ui.sources_dashboard.queue": "queue",
"ui.sources_dashboard.errors": "errors",
"ui.sources_dashboard.verification_complete": "Verification Complete",
"ui.sources_dashboard.checked": "Checked:",
"ui.sources_dashboard.violations": "violation(s)",
"ui.sources_dashboard.field": "Field:",
"ui.sources_dashboard.expected": "expected:",
"ui.sources_dashboard.actual": "actual:",
"ui.sources_dashboard.cli_equivalent": "CLI equivalent:",
"ui.sources_dashboard.data_from": "Data from",
"ui.sources_dashboard.to": "to",
"ui.sources_dashboard.hour_window": "h window",
"ui.timeline.title": "Timeline",
"ui.timeline.event_timeline": "Event Timeline",
"ui.timeline.refresh_timeline": "Refresh timeline",
"ui.timeline.loading": "Loading timeline...",
"ui.timeline.empty_state": "Enter a correlation ID to view the event timeline",
"ui.timeline.critical_path": "Critical path analysis",
"ui.timeline.causal_lanes": "Event causal lanes",
"ui.timeline.load_more": "Load more events",
"ui.timeline.event_details": "Event details",
"ui.timeline.events": "events",
"ui.exception_center.title": "Exception Center",
"ui.exception_center.list_view": "List view",
"ui.exception_center.kanban_view": "Kanban view",
"ui.exception_center.new_exception": "+ New Exception",
"ui.exception_center.search_placeholder": "Search exceptions...",
"ui.exception_center.type_vulnerability": "vulnerability",
"ui.exception_center.type_license": "license",
"ui.exception_center.type_policy": "policy",
"ui.exception_center.type_entropy": "entropy",
"ui.exception_center.type_determinism": "determinism",
"ui.exception_center.expiring_soon": "Expiring soon",
"ui.exception_center.clear_filters": "Clear filters",
"ui.exception_center.audit_label": "[A]",
"ui.exception_center.audit_title": "View audit log",
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
"ui.exception_center.column_empty": "No exceptions",
"ui.exception_center.exceptions_suffix": "exceptions",
"ui.evidence_thread.back_to_list": "Back to list",
"ui.evidence_thread.title_default": "Evidence Thread",
"ui.evidence_thread.copy_digest": "Copy full digest",
"ui.evidence_thread.risk_label": "Risk:",
"ui.evidence_thread.nodes": "nodes",
"ui.evidence_thread.loading": "Loading evidence thread...",
"ui.evidence_thread.graph_tab": "Graph",
"ui.evidence_thread.timeline_tab": "Timeline",
"ui.evidence_thread.transcript_tab": "Transcript",
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
"ui.vulnerability_detail.eyebrow": "Vulnerability",
"ui.vulnerability_detail.cvss": "CVSS",
"ui.vulnerability_detail.impact_first": "Impact First",
"ui.vulnerability_detail.epss": "EPSS",
"ui.vulnerability_detail.kev": "KEV",
"ui.vulnerability_detail.kev_listed": "Listed",
"ui.vulnerability_detail.kev_not_listed": "Not listed",
"ui.vulnerability_detail.reachability": "Reachability",
"ui.vulnerability_detail.blast_radius": "Blast Radius",
"ui.vulnerability_detail.assets": "assets",
"ui.vulnerability_detail.binary_resolution": "Binary Resolution",
"ui.vulnerability_detail.evidence_suffix": "evidence",
"ui.vulnerability_detail.fingerprint_note": "This binary was identified as patched using fingerprint analysis, not just version matching.",
"ui.vulnerability_detail.affected_components": "Affected Components",
"ui.vulnerability_detail.fix": "fix",
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
"ui.vulnerability_detail.references": "References",
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
}

View File

@@ -0,0 +1,29 @@
{
"_meta": { "locale": "de-DE", "namespace": "platform", "version": "1.0" },
"platform.health.status_healthy": "Alle Systeme betriebsbereit.",
"platform.health.status_degraded": "Einige Dienste haben Probleme.",
"platform.health.status_unavailable": "Die Plattform ist derzeit nicht verfuegbar.",
"platform.quota.limit_exceeded": "Kontingentgrenze fuer {0} ueberschritten.",
"platform.quota.usage_warning": "Die Nutzung liegt bei {0}% des Kontingents.",
"platform.quota.reset_at": "Kontingent wird um {0} zurueckgesetzt.",
"platform.onboarding.welcome": "Willkommen bei StellaOps.",
"platform.onboarding.step_authority": "Identitaetsanbieter konfigurieren.",
"platform.onboarding.step_registry": "Container-Registry verbinden.",
"platform.onboarding.step_environments": "Zielumgebungen definieren.",
"platform.onboarding.step_complete": "Einrichtung abgeschlossen. Bereit.",
"platform.setup.required": "Eine Ersteinrichtung ist vor der Nutzung erforderlich.",
"platform.setup.in_progress": "Einrichtung laeuft.",
"platform.setup.complete": "Einrichtung abgeschlossen.",
"platform.context.region_not_found": "Region {0} wurde nicht gefunden.",
"platform.context.environment_not_found": "Umgebung {0} wurde nicht gefunden.",
"platform.migration.started": "Migration gestartet.",
"platform.migration.completed": "Migration erfolgreich abgeschlossen.",
"platform.migration.failed": "Migration fehlgeschlagen: {0}."
}

View File

@@ -0,0 +1,275 @@
{
"_meta": { "locale": "de-DE", "namespace": "ui", "version": "1.0" },
"ui.loading.skeleton": "Wird geladen...",
"ui.loading.spinner": "Please wait...",
"ui.loading.slow": "This is taking longer than expected...",
"ui.error.generic": "Etwas ist schiefgelaufen.",
"ui.error.network": "Network error. Check your connection.",
"ui.error.timeout": "Request timed out. Please try again.",
"ui.error.not_found": "The requested resource was not found.",
"ui.error.unauthorized": "You don't have permission to view this.",
"ui.error.server_error": "Server error. Please try again later.",
"ui.error.try_again": "Try again",
"ui.error.go_back": "Go back",
"ui.offline.banner": "You're offline.",
"ui.offline.description": "Some features may be unavailable.",
"ui.offline.reconnecting": "Reconnecting...",
"ui.offline.reconnected": "Back online.",
"ui.toast.success": "Success",
"ui.toast.info": "Info",
"ui.toast.warning": "Warning",
"ui.toast.error": "Error",
"ui.toast.dismiss": "Dismiss",
"ui.toast.undo": "Undo",
"ui.actions.save": "Speichern",
"ui.actions.saving": "Saving...",
"ui.actions.saved": "Saved",
"ui.actions.cancel": "Abbrechen",
"ui.actions.confirm": "Confirm",
"ui.actions.delete": "Delete",
"ui.actions.deleting": "Deleting...",
"ui.actions.deleted": "Deleted",
"ui.actions.submit": "Submit",
"ui.actions.submitting": "Submitting...",
"ui.actions.submitted": "Submitted",
"ui.actions.close": "Close",
"ui.actions.expand": "Expand",
"ui.actions.collapse": "Collapse",
"ui.actions.show_more": "Show more",
"ui.actions.show_less": "Show less",
"ui.actions.retry": "Erneut versuchen",
"ui.actions.refresh": "Refresh",
"ui.actions.export": "Export",
"ui.actions.search": "Search",
"ui.actions.clear": "Clear",
"ui.actions.view": "View",
"ui.actions.dismiss": "Dismiss",
"ui.actions.show": "Show",
"ui.actions.hide": "Hide",
"ui.actions.sign_in": "Sign in",
"ui.actions.back_to_list": "Back to list",
"ui.actions.load_more": "Load more",
"ui.labels.all": "All",
"ui.labels.title": "Title",
"ui.labels.description": "Description",
"ui.labels.status": "Status",
"ui.labels.score": "Score",
"ui.labels.severity": "Severity",
"ui.labels.details": "Details",
"ui.labels.actions": "Actions",
"ui.labels.type": "Type",
"ui.labels.tags": "Tags",
"ui.labels.filters": "Filters",
"ui.labels.updated": "Updated",
"ui.labels.showing": "Showing",
"ui.labels.of": "of",
"ui.labels.total": "Total",
"ui.labels.not_applicable": "n/a",
"ui.labels.selected": "selected",
"ui.labels.last_updated": "Last updated:",
"ui.labels.expires": "Expires",
"ui.validation.required": "This field is required.",
"ui.validation.invalid": "Invalid value.",
"ui.validation.too_long": "Maximum {max} characters allowed.",
"ui.validation.too_short": "Minimum {min} characters required.",
"ui.validation.invalid_email": "Please enter a valid email address.",
"ui.validation.invalid_url": "Please enter a valid URL.",
"ui.a11y.loading": "Content is loading.",
"ui.a11y.loaded": "Content loaded.",
"ui.a11y.error": "An error occurred.",
"ui.a11y.expanded": "Expanded",
"ui.a11y.collapsed": "Collapsed",
"ui.a11y.selected": "Selected",
"ui.a11y.deselected": "Deselected",
"ui.a11y.required": "Required field",
"ui.a11y.optional": "Optional",
"ui.motion.reduced": "Animations reduced.",
"ui.motion.enabled": "Animations enabled.",
"ui.auth.fresh_active": "Frische Anmeldung: Aktiv",
"ui.auth.fresh_stale": "Frische Anmeldung: Veraltet",
"ui.locale.label": "Sprache",
"ui.locale.en_us": "Englisch (USA)",
"ui.locale.de_de": "Deutsch (Deutschland)",
"ui.locale.bg_bg": "Bulgarisch (Bulgarien)",
"ui.locale.ru_ru": "Russisch (Russland)",
"ui.locale.es_es": "Spanisch (Spanien)",
"ui.locale.fr_fr": "Franzoesisch (Frankreich)",
"ui.locale.zh_tw": "Chinesisch (Traditionell, Taiwan)",
"ui.locale.zh_cn": "Chinesisch (Vereinfacht, China)",
"ui.locale.uk_ua": "Ukrainian (Ukraine)",
"ui.settings.language.title": "Sprache",
"ui.settings.language.subtitle": "Legen Sie Ihre bevorzugte Konsolensprache fest.",
"ui.settings.language.description": "Aenderungen werden sofort in der UI angewendet.",
"ui.settings.language.selector_label": "Bevorzugte Sprache",
"ui.settings.language.persisted": "Fuer Ihr Konto gespeichert und im CLI wiederverwendet.",
"ui.settings.language.persisted_error": "Lokal gespeichert, aber Kontosynchronisierung fehlgeschlagen.",
"ui.settings.language.sign_in_hint": "Melden Sie sich an, um diese Einstellung mit dem CLI zu synchronisieren.",
"ui.first_signal.label": "Erstes Signal",
"ui.first_signal.run_prefix": "Run:",
"ui.first_signal.live": "Live",
"ui.first_signal.polling": "Polling",
"ui.first_signal.range_prefix": "Range",
"ui.first_signal.range_separator": "\u2013",
"ui.first_signal.stage_separator": " \u00b7 ",
"ui.first_signal.waiting": "Warten auf erstes Signal\u2026",
"ui.first_signal.not_available": "Signal not available yet.",
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
"ui.first_signal.failed": "Signal konnte nicht geladen werden.",
"ui.first_signal.retry": "Retry",
"ui.first_signal.try_again": "Try again",
"ui.first_signal.kind.queued": "Queued",
"ui.first_signal.kind.started": "Started",
"ui.first_signal.kind.phase": "In progress",
"ui.first_signal.kind.blocked": "Blocked",
"ui.first_signal.kind.failed": "Failed",
"ui.first_signal.kind.succeeded": "Succeeded",
"ui.first_signal.kind.canceled": "Canceled",
"ui.first_signal.kind.unavailable": "Unavailable",
"ui.first_signal.kind.unknown": "Signal",
"ui.first_signal.stage.resolve": "Resolving",
"ui.first_signal.stage.fetch": "Fetching",
"ui.first_signal.stage.restore": "Restoring",
"ui.first_signal.stage.analyze": "Analyzing",
"ui.first_signal.stage.policy": "Evaluating policy",
"ui.first_signal.stage.report": "Generating report",
"ui.first_signal.stage.unknown": "Processing",
"ui.first_signal.aria.card_label": "First signal status",
"ui.severity.critical": "Critical",
"ui.severity.high": "High",
"ui.severity.medium": "Medium",
"ui.severity.low": "Low",
"ui.severity.info": "Info",
"ui.severity.none": "None",
"ui.release_orchestrator.title": "Release Orchestrator",
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
"ui.risk_dashboard.title": "Risk Profiles",
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
"ui.risk_dashboard.up_to_date": "Up to date",
"ui.risk_dashboard.last_computation": "Last Computation",
"ui.risk_dashboard.search_placeholder": "Title contains",
"ui.risk_dashboard.evaluated": "Evaluated",
"ui.risk_dashboard.risks_suffix": "risks.",
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
"ui.findings.title": "Findings",
"ui.findings.search_placeholder": "Search findings...",
"ui.findings.clear_filters": "Clear Filters",
"ui.findings.bulk_triage": "Bulk Triage",
"ui.findings.export_all": "Export all findings",
"ui.findings.export_selected": "Export selected findings",
"ui.findings.select_all": "Select all findings",
"ui.findings.trust": "Trust",
"ui.findings.advisory": "Advisory",
"ui.findings.package": "Package",
"ui.findings.flags": "Flags",
"ui.findings.why": "Why",
"ui.findings.select": "Select",
"ui.findings.no_findings": "No findings to display.",
"ui.findings.no_match": "No findings match the current filters.",
"ui.sources_dashboard.title": "Sources Dashboard",
"ui.sources_dashboard.verifying": "Verifying...",
"ui.sources_dashboard.verify_24h": "Verify last 24h",
"ui.sources_dashboard.loading_aoc": "Loading AOC metrics...",
"ui.sources_dashboard.pass_fail_title": "AOC Pass/Fail",
"ui.sources_dashboard.pass_rate": "Pass Rate",
"ui.sources_dashboard.passed": "Passed",
"ui.sources_dashboard.failed": "Failed",
"ui.sources_dashboard.recent_violations": "Recent Violations",
"ui.sources_dashboard.no_violations": "No violations in time window",
"ui.sources_dashboard.throughput_title": "Ingest Throughput",
"ui.sources_dashboard.docs_per_min": "docs/min",
"ui.sources_dashboard.avg_ms": "avg ms",
"ui.sources_dashboard.p95_ms": "p95 ms",
"ui.sources_dashboard.queue": "queue",
"ui.sources_dashboard.errors": "errors",
"ui.sources_dashboard.verification_complete": "Verification Complete",
"ui.sources_dashboard.checked": "Checked:",
"ui.sources_dashboard.violations": "violation(s)",
"ui.sources_dashboard.field": "Field:",
"ui.sources_dashboard.expected": "expected:",
"ui.sources_dashboard.actual": "actual:",
"ui.sources_dashboard.cli_equivalent": "CLI equivalent:",
"ui.sources_dashboard.data_from": "Data from",
"ui.sources_dashboard.to": "to",
"ui.sources_dashboard.hour_window": "h window",
"ui.timeline.title": "Timeline",
"ui.timeline.event_timeline": "Event Timeline",
"ui.timeline.refresh_timeline": "Refresh timeline",
"ui.timeline.loading": "Loading timeline...",
"ui.timeline.empty_state": "Enter a correlation ID to view the event timeline",
"ui.timeline.critical_path": "Critical path analysis",
"ui.timeline.causal_lanes": "Event causal lanes",
"ui.timeline.load_more": "Load more events",
"ui.timeline.event_details": "Event details",
"ui.timeline.events": "events",
"ui.exception_center.title": "Exception Center",
"ui.exception_center.list_view": "List view",
"ui.exception_center.kanban_view": "Kanban view",
"ui.exception_center.new_exception": "+ New Exception",
"ui.exception_center.search_placeholder": "Search exceptions...",
"ui.exception_center.type_vulnerability": "vulnerability",
"ui.exception_center.type_license": "license",
"ui.exception_center.type_policy": "policy",
"ui.exception_center.type_entropy": "entropy",
"ui.exception_center.type_determinism": "determinism",
"ui.exception_center.expiring_soon": "Expiring soon",
"ui.exception_center.clear_filters": "Clear filters",
"ui.exception_center.audit_label": "[A]",
"ui.exception_center.audit_title": "View audit log",
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
"ui.exception_center.column_empty": "No exceptions",
"ui.exception_center.exceptions_suffix": "exceptions",
"ui.evidence_thread.back_to_list": "Back to list",
"ui.evidence_thread.title_default": "Evidence Thread",
"ui.evidence_thread.copy_digest": "Copy full digest",
"ui.evidence_thread.risk_label": "Risk:",
"ui.evidence_thread.nodes": "nodes",
"ui.evidence_thread.loading": "Loading evidence thread...",
"ui.evidence_thread.graph_tab": "Graph",
"ui.evidence_thread.timeline_tab": "Timeline",
"ui.evidence_thread.transcript_tab": "Transcript",
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
"ui.vulnerability_detail.eyebrow": "Vulnerability",
"ui.vulnerability_detail.cvss": "CVSS",
"ui.vulnerability_detail.impact_first": "Impact First",
"ui.vulnerability_detail.epss": "EPSS",
"ui.vulnerability_detail.kev": "KEV",
"ui.vulnerability_detail.kev_listed": "Listed",
"ui.vulnerability_detail.kev_not_listed": "Not listed",
"ui.vulnerability_detail.reachability": "Reachability",
"ui.vulnerability_detail.blast_radius": "Blast Radius",
"ui.vulnerability_detail.assets": "assets",
"ui.vulnerability_detail.binary_resolution": "Binary Resolution",
"ui.vulnerability_detail.evidence_suffix": "evidence",
"ui.vulnerability_detail.fingerprint_note": "This binary was identified as patched using fingerprint analysis, not just version matching.",
"ui.vulnerability_detail.affected_components": "Affected Components",
"ui.vulnerability_detail.fix": "fix",
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
"ui.vulnerability_detail.references": "References",
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
}

View File

@@ -0,0 +1,28 @@
{
"_meta": { "locale": "en-US", "namespace": "platform", "version": "1.0" },
"platform.health.status_healthy": "All systems operational.",
"platform.health.status_degraded": "Some services are experiencing issues.",
"platform.health.status_unavailable": "Platform is currently unavailable.",
"platform.quota.limit_exceeded": "Quota limit exceeded for {0}.",
"platform.quota.usage_warning": "Usage is at {0}% of the quota limit.",
"platform.quota.reset_at": "Quota resets at {0}.",
"platform.onboarding.welcome": "Welcome to StellaOps.",
"platform.onboarding.step_authority": "Configure identity provider.",
"platform.onboarding.step_registry": "Connect container registry.",
"platform.onboarding.step_environments": "Define target environments.",
"platform.onboarding.step_complete": "Setup complete. Ready to go.",
"platform.setup.required": "Initial setup is required before using the platform.",
"platform.setup.in_progress": "Setup is in progress.",
"platform.setup.complete": "Setup is complete.",
"platform.context.region_not_found": "Region {0} not found.",
"platform.context.environment_not_found": "Environment {0} not found.",
"platform.migration.started": "Migration started.",
"platform.migration.completed": "Migration completed successfully.",
"platform.migration.failed": "Migration failed: {0}."
}

View File

@@ -0,0 +1,275 @@
{
"_meta": { "locale": "en-US", "namespace": "ui", "version": "1.0" },
"ui.loading.skeleton": "Loading...",
"ui.loading.spinner": "Please wait...",
"ui.loading.slow": "This is taking longer than expected...",
"ui.error.generic": "Something went wrong.",
"ui.error.network": "Network error. Check your connection.",
"ui.error.timeout": "Request timed out. Please try again.",
"ui.error.not_found": "The requested resource was not found.",
"ui.error.unauthorized": "You don't have permission to view this.",
"ui.error.server_error": "Server error. Please try again later.",
"ui.error.try_again": "Try again",
"ui.error.go_back": "Go back",
"ui.offline.banner": "You're offline.",
"ui.offline.description": "Some features may be unavailable.",
"ui.offline.reconnecting": "Reconnecting...",
"ui.offline.reconnected": "Back online.",
"ui.toast.success": "Success",
"ui.toast.info": "Info",
"ui.toast.warning": "Warning",
"ui.toast.error": "Error",
"ui.toast.dismiss": "Dismiss",
"ui.toast.undo": "Undo",
"ui.actions.save": "Save",
"ui.actions.saving": "Saving...",
"ui.actions.saved": "Saved",
"ui.actions.cancel": "Cancel",
"ui.actions.confirm": "Confirm",
"ui.actions.delete": "Delete",
"ui.actions.deleting": "Deleting...",
"ui.actions.deleted": "Deleted",
"ui.actions.submit": "Submit",
"ui.actions.submitting": "Submitting...",
"ui.actions.submitted": "Submitted",
"ui.actions.close": "Close",
"ui.actions.expand": "Expand",
"ui.actions.collapse": "Collapse",
"ui.actions.show_more": "Show more",
"ui.actions.show_less": "Show less",
"ui.actions.retry": "Retry",
"ui.actions.refresh": "Refresh",
"ui.actions.export": "Export",
"ui.actions.search": "Search",
"ui.actions.clear": "Clear",
"ui.actions.view": "View",
"ui.actions.dismiss": "Dismiss",
"ui.actions.show": "Show",
"ui.actions.hide": "Hide",
"ui.actions.sign_in": "Sign in",
"ui.actions.back_to_list": "Back to list",
"ui.actions.load_more": "Load more",
"ui.labels.all": "All",
"ui.labels.title": "Title",
"ui.labels.description": "Description",
"ui.labels.status": "Status",
"ui.labels.score": "Score",
"ui.labels.severity": "Severity",
"ui.labels.details": "Details",
"ui.labels.actions": "Actions",
"ui.labels.type": "Type",
"ui.labels.tags": "Tags",
"ui.labels.filters": "Filters",
"ui.labels.updated": "Updated",
"ui.labels.showing": "Showing",
"ui.labels.of": "of",
"ui.labels.total": "Total",
"ui.labels.not_applicable": "n/a",
"ui.labels.selected": "selected",
"ui.labels.last_updated": "Last updated:",
"ui.labels.expires": "Expires",
"ui.validation.required": "This field is required.",
"ui.validation.invalid": "Invalid value.",
"ui.validation.too_long": "Maximum {max} characters allowed.",
"ui.validation.too_short": "Minimum {min} characters required.",
"ui.validation.invalid_email": "Please enter a valid email address.",
"ui.validation.invalid_url": "Please enter a valid URL.",
"ui.a11y.loading": "Content is loading.",
"ui.a11y.loaded": "Content loaded.",
"ui.a11y.error": "An error occurred.",
"ui.a11y.expanded": "Expanded",
"ui.a11y.collapsed": "Collapsed",
"ui.a11y.selected": "Selected",
"ui.a11y.deselected": "Deselected",
"ui.a11y.required": "Required field",
"ui.a11y.optional": "Optional",
"ui.motion.reduced": "Animations reduced.",
"ui.motion.enabled": "Animations enabled.",
"ui.auth.fresh_active": "Fresh auth: Active",
"ui.auth.fresh_stale": "Fresh auth: Stale",
"ui.locale.label": "Locale",
"ui.locale.en_us": "English (US)",
"ui.locale.de_de": "German (Germany)",
"ui.locale.bg_bg": "Bulgarian (Bulgaria)",
"ui.locale.ru_ru": "Russian (Russia)",
"ui.locale.es_es": "Spanish (Spain)",
"ui.locale.fr_fr": "French (France)",
"ui.locale.zh_tw": "Chinese (Traditional, Taiwan)",
"ui.locale.zh_cn": "Chinese (Simplified, China)",
"ui.locale.uk_ua": "Ukrainian (Ukraine)",
"ui.settings.language.title": "Language",
"ui.settings.language.subtitle": "Set your preferred console language.",
"ui.settings.language.description": "Changes apply immediately in the UI.",
"ui.settings.language.selector_label": "Preferred language",
"ui.settings.language.persisted": "Saved for your account and reused by CLI.",
"ui.settings.language.persisted_error": "Saved locally, but account sync failed.",
"ui.settings.language.sign_in_hint": "Sign in to sync this preference with CLI.",
"ui.first_signal.label": "First signal",
"ui.first_signal.run_prefix": "Run:",
"ui.first_signal.live": "Live",
"ui.first_signal.polling": "Polling",
"ui.first_signal.range_prefix": "Range",
"ui.first_signal.range_separator": "\u2013",
"ui.first_signal.stage_separator": " \u00b7 ",
"ui.first_signal.waiting": "Waiting for first signal\u2026",
"ui.first_signal.not_available": "Signal not available yet.",
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
"ui.first_signal.failed": "Failed to load signal.",
"ui.first_signal.retry": "Retry",
"ui.first_signal.try_again": "Try again",
"ui.first_signal.kind.queued": "Queued",
"ui.first_signal.kind.started": "Started",
"ui.first_signal.kind.phase": "In progress",
"ui.first_signal.kind.blocked": "Blocked",
"ui.first_signal.kind.failed": "Failed",
"ui.first_signal.kind.succeeded": "Succeeded",
"ui.first_signal.kind.canceled": "Canceled",
"ui.first_signal.kind.unavailable": "Unavailable",
"ui.first_signal.kind.unknown": "Signal",
"ui.first_signal.stage.resolve": "Resolving",
"ui.first_signal.stage.fetch": "Fetching",
"ui.first_signal.stage.restore": "Restoring",
"ui.first_signal.stage.analyze": "Analyzing",
"ui.first_signal.stage.policy": "Evaluating policy",
"ui.first_signal.stage.report": "Generating report",
"ui.first_signal.stage.unknown": "Processing",
"ui.first_signal.aria.card_label": "First signal status",
"ui.severity.critical": "Critical",
"ui.severity.high": "High",
"ui.severity.medium": "Medium",
"ui.severity.low": "Low",
"ui.severity.info": "Info",
"ui.severity.none": "None",
"ui.release_orchestrator.title": "Release Orchestrator",
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
"ui.risk_dashboard.title": "Risk Profiles",
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
"ui.risk_dashboard.up_to_date": "Up to date",
"ui.risk_dashboard.last_computation": "Last Computation",
"ui.risk_dashboard.search_placeholder": "Title contains",
"ui.risk_dashboard.evaluated": "Evaluated",
"ui.risk_dashboard.risks_suffix": "risks.",
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
"ui.findings.title": "Findings",
"ui.findings.search_placeholder": "Search findings...",
"ui.findings.clear_filters": "Clear Filters",
"ui.findings.bulk_triage": "Bulk Triage",
"ui.findings.export_all": "Export all findings",
"ui.findings.export_selected": "Export selected findings",
"ui.findings.select_all": "Select all findings",
"ui.findings.trust": "Trust",
"ui.findings.advisory": "Advisory",
"ui.findings.package": "Package",
"ui.findings.flags": "Flags",
"ui.findings.why": "Why",
"ui.findings.select": "Select",
"ui.findings.no_findings": "No findings to display.",
"ui.findings.no_match": "No findings match the current filters.",
"ui.sources_dashboard.title": "Sources Dashboard",
"ui.sources_dashboard.verifying": "Verifying...",
"ui.sources_dashboard.verify_24h": "Verify last 24h",
"ui.sources_dashboard.loading_aoc": "Loading AOC metrics...",
"ui.sources_dashboard.pass_fail_title": "AOC Pass/Fail",
"ui.sources_dashboard.pass_rate": "Pass Rate",
"ui.sources_dashboard.passed": "Passed",
"ui.sources_dashboard.failed": "Failed",
"ui.sources_dashboard.recent_violations": "Recent Violations",
"ui.sources_dashboard.no_violations": "No violations in time window",
"ui.sources_dashboard.throughput_title": "Ingest Throughput",
"ui.sources_dashboard.docs_per_min": "docs/min",
"ui.sources_dashboard.avg_ms": "avg ms",
"ui.sources_dashboard.p95_ms": "p95 ms",
"ui.sources_dashboard.queue": "queue",
"ui.sources_dashboard.errors": "errors",
"ui.sources_dashboard.verification_complete": "Verification Complete",
"ui.sources_dashboard.checked": "Checked:",
"ui.sources_dashboard.violations": "violation(s)",
"ui.sources_dashboard.field": "Field:",
"ui.sources_dashboard.expected": "expected:",
"ui.sources_dashboard.actual": "actual:",
"ui.sources_dashboard.cli_equivalent": "CLI equivalent:",
"ui.sources_dashboard.data_from": "Data from",
"ui.sources_dashboard.to": "to",
"ui.sources_dashboard.hour_window": "h window",
"ui.timeline.title": "Timeline",
"ui.timeline.event_timeline": "Event Timeline",
"ui.timeline.refresh_timeline": "Refresh timeline",
"ui.timeline.loading": "Loading timeline...",
"ui.timeline.empty_state": "Enter a correlation ID to view the event timeline",
"ui.timeline.critical_path": "Critical path analysis",
"ui.timeline.causal_lanes": "Event causal lanes",
"ui.timeline.load_more": "Load more events",
"ui.timeline.event_details": "Event details",
"ui.timeline.events": "events",
"ui.exception_center.title": "Exception Center",
"ui.exception_center.list_view": "List view",
"ui.exception_center.kanban_view": "Kanban view",
"ui.exception_center.new_exception": "+ New Exception",
"ui.exception_center.search_placeholder": "Search exceptions...",
"ui.exception_center.type_vulnerability": "vulnerability",
"ui.exception_center.type_license": "license",
"ui.exception_center.type_policy": "policy",
"ui.exception_center.type_entropy": "entropy",
"ui.exception_center.type_determinism": "determinism",
"ui.exception_center.expiring_soon": "Expiring soon",
"ui.exception_center.clear_filters": "Clear filters",
"ui.exception_center.audit_label": "[A]",
"ui.exception_center.audit_title": "View audit log",
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
"ui.exception_center.column_empty": "No exceptions",
"ui.exception_center.exceptions_suffix": "exceptions",
"ui.evidence_thread.back_to_list": "Back to list",
"ui.evidence_thread.title_default": "Evidence Thread",
"ui.evidence_thread.copy_digest": "Copy full digest",
"ui.evidence_thread.risk_label": "Risk:",
"ui.evidence_thread.nodes": "nodes",
"ui.evidence_thread.loading": "Loading evidence thread...",
"ui.evidence_thread.graph_tab": "Graph",
"ui.evidence_thread.timeline_tab": "Timeline",
"ui.evidence_thread.transcript_tab": "Transcript",
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
"ui.vulnerability_detail.eyebrow": "Vulnerability",
"ui.vulnerability_detail.cvss": "CVSS",
"ui.vulnerability_detail.impact_first": "Impact First",
"ui.vulnerability_detail.epss": "EPSS",
"ui.vulnerability_detail.kev": "KEV",
"ui.vulnerability_detail.kev_listed": "Listed",
"ui.vulnerability_detail.kev_not_listed": "Not listed",
"ui.vulnerability_detail.reachability": "Reachability",
"ui.vulnerability_detail.blast_radius": "Blast Radius",
"ui.vulnerability_detail.assets": "assets",
"ui.vulnerability_detail.binary_resolution": "Binary Resolution",
"ui.vulnerability_detail.evidence_suffix": "evidence",
"ui.vulnerability_detail.fingerprint_note": "This binary was identified as patched using fingerprint analysis, not just version matching.",
"ui.vulnerability_detail.affected_components": "Affected Components",
"ui.vulnerability_detail.fix": "fix",
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
"ui.vulnerability_detail.references": "References",
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
}

View File

@@ -0,0 +1,29 @@
{
"_meta": { "locale": "es-ES", "namespace": "platform", "version": "1.0" },
"platform.health.status_healthy": "Todos los sistemas operan con normalidad.",
"platform.health.status_degraded": "Algunos servicios presentan incidencias.",
"platform.health.status_unavailable": "La plataforma no esta disponible en este momento.",
"platform.quota.limit_exceeded": "Se supero el limite de cuota para {0}.",
"platform.quota.usage_warning": "El uso esta en {0}% del limite de cuota.",
"platform.quota.reset_at": "La cuota se restablece en {0}.",
"platform.onboarding.welcome": "Bienvenido a StellaOps.",
"platform.onboarding.step_authority": "Configura el proveedor de identidad.",
"platform.onboarding.step_registry": "Conecta el registro de contenedores.",
"platform.onboarding.step_environments": "Define los entornos de destino.",
"platform.onboarding.step_complete": "Configuracion completada. Todo listo.",
"platform.setup.required": "Se requiere una configuracion inicial antes de usar la plataforma.",
"platform.setup.in_progress": "La configuracion esta en curso.",
"platform.setup.complete": "La configuracion esta completa.",
"platform.context.region_not_found": "No se encontro la region {0}.",
"platform.context.environment_not_found": "No se encontro el entorno {0}.",
"platform.migration.started": "Migracion iniciada.",
"platform.migration.completed": "Migracion completada correctamente.",
"platform.migration.failed": "La migracion fallo: {0}."
}

View File

@@ -0,0 +1,275 @@
{
"_meta": { "locale": "es-ES", "namespace": "ui", "version": "1.0" },
"ui.loading.skeleton": "Loading...",
"ui.loading.spinner": "Please wait...",
"ui.loading.slow": "This is taking longer than expected...",
"ui.error.generic": "Something went wrong.",
"ui.error.network": "Network error. Check your connection.",
"ui.error.timeout": "Request timed out. Please try again.",
"ui.error.not_found": "The requested resource was not found.",
"ui.error.unauthorized": "You don't have permission to view this.",
"ui.error.server_error": "Server error. Please try again later.",
"ui.error.try_again": "Try again",
"ui.error.go_back": "Go back",
"ui.offline.banner": "You're offline.",
"ui.offline.description": "Some features may be unavailable.",
"ui.offline.reconnecting": "Reconnecting...",
"ui.offline.reconnected": "Back online.",
"ui.toast.success": "Success",
"ui.toast.info": "Info",
"ui.toast.warning": "Warning",
"ui.toast.error": "Error",
"ui.toast.dismiss": "Dismiss",
"ui.toast.undo": "Undo",
"ui.actions.save": "Save",
"ui.actions.saving": "Saving...",
"ui.actions.saved": "Saved",
"ui.actions.cancel": "Cancel",
"ui.actions.confirm": "Confirm",
"ui.actions.delete": "Delete",
"ui.actions.deleting": "Deleting...",
"ui.actions.deleted": "Deleted",
"ui.actions.submit": "Submit",
"ui.actions.submitting": "Submitting...",
"ui.actions.submitted": "Submitted",
"ui.actions.close": "Close",
"ui.actions.expand": "Expand",
"ui.actions.collapse": "Collapse",
"ui.actions.show_more": "Show more",
"ui.actions.show_less": "Show less",
"ui.actions.retry": "Retry",
"ui.actions.refresh": "Refresh",
"ui.actions.export": "Export",
"ui.actions.search": "Search",
"ui.actions.clear": "Clear",
"ui.actions.view": "View",
"ui.actions.dismiss": "Dismiss",
"ui.actions.show": "Show",
"ui.actions.hide": "Hide",
"ui.actions.sign_in": "Sign in",
"ui.actions.back_to_list": "Back to list",
"ui.actions.load_more": "Load more",
"ui.labels.all": "All",
"ui.labels.title": "Title",
"ui.labels.description": "Description",
"ui.labels.status": "Status",
"ui.labels.score": "Score",
"ui.labels.severity": "Severity",
"ui.labels.details": "Details",
"ui.labels.actions": "Actions",
"ui.labels.type": "Type",
"ui.labels.tags": "Tags",
"ui.labels.filters": "Filters",
"ui.labels.updated": "Updated",
"ui.labels.showing": "Showing",
"ui.labels.of": "of",
"ui.labels.total": "Total",
"ui.labels.not_applicable": "n/a",
"ui.labels.selected": "selected",
"ui.labels.last_updated": "Last updated:",
"ui.labels.expires": "Expires",
"ui.validation.required": "This field is required.",
"ui.validation.invalid": "Invalid value.",
"ui.validation.too_long": "Maximum {max} characters allowed.",
"ui.validation.too_short": "Minimum {min} characters required.",
"ui.validation.invalid_email": "Please enter a valid email address.",
"ui.validation.invalid_url": "Please enter a valid URL.",
"ui.a11y.loading": "Content is loading.",
"ui.a11y.loaded": "Content loaded.",
"ui.a11y.error": "An error occurred.",
"ui.a11y.expanded": "Expanded",
"ui.a11y.collapsed": "Collapsed",
"ui.a11y.selected": "Selected",
"ui.a11y.deselected": "Deselected",
"ui.a11y.required": "Required field",
"ui.a11y.optional": "Optional",
"ui.motion.reduced": "Animations reduced.",
"ui.motion.enabled": "Animations enabled.",
"ui.auth.fresh_active": "Fresh auth: Active",
"ui.auth.fresh_stale": "Fresh auth: Stale",
"ui.locale.label": "Idioma",
"ui.locale.en_us": "Ingles (EE. UU.)",
"ui.locale.de_de": "Aleman (Alemania)",
"ui.locale.bg_bg": "Bulgaro (Bulgaria)",
"ui.locale.ru_ru": "Ruso (Rusia)",
"ui.locale.es_es": "Espanol (Espana)",
"ui.locale.fr_fr": "Frances (Francia)",
"ui.locale.zh_tw": "Chino tradicional (Taiwan)",
"ui.locale.zh_cn": "Chino simplificado (China)",
"ui.locale.uk_ua": "Ukrainian (Ukraine)",
"ui.settings.language.title": "Idioma",
"ui.settings.language.subtitle": "Define tu idioma preferido de la consola.",
"ui.settings.language.description": "Los cambios se aplican de inmediato en la UI.",
"ui.settings.language.selector_label": "Idioma preferido",
"ui.settings.language.persisted": "Guardado para tu cuenta y reutilizado por CLI.",
"ui.settings.language.persisted_error": "Guardado localmente, pero fallo la sincronizacion de la cuenta.",
"ui.settings.language.sign_in_hint": "Inicia sesion para sincronizar esta preferencia con CLI.",
"ui.first_signal.label": "First signal",
"ui.first_signal.run_prefix": "Run:",
"ui.first_signal.live": "Live",
"ui.first_signal.polling": "Polling",
"ui.first_signal.range_prefix": "Range",
"ui.first_signal.range_separator": "\u2013",
"ui.first_signal.stage_separator": " \u00b7 ",
"ui.first_signal.waiting": "Waiting for first signal\u2026",
"ui.first_signal.not_available": "Signal not available yet.",
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
"ui.first_signal.failed": "Failed to load signal.",
"ui.first_signal.retry": "Retry",
"ui.first_signal.try_again": "Try again",
"ui.first_signal.kind.queued": "Queued",
"ui.first_signal.kind.started": "Started",
"ui.first_signal.kind.phase": "In progress",
"ui.first_signal.kind.blocked": "Blocked",
"ui.first_signal.kind.failed": "Failed",
"ui.first_signal.kind.succeeded": "Succeeded",
"ui.first_signal.kind.canceled": "Canceled",
"ui.first_signal.kind.unavailable": "Unavailable",
"ui.first_signal.kind.unknown": "Signal",
"ui.first_signal.stage.resolve": "Resolving",
"ui.first_signal.stage.fetch": "Fetching",
"ui.first_signal.stage.restore": "Restoring",
"ui.first_signal.stage.analyze": "Analyzing",
"ui.first_signal.stage.policy": "Evaluating policy",
"ui.first_signal.stage.report": "Generating report",
"ui.first_signal.stage.unknown": "Processing",
"ui.first_signal.aria.card_label": "First signal status",
"ui.severity.critical": "Critical",
"ui.severity.high": "High",
"ui.severity.medium": "Medium",
"ui.severity.low": "Low",
"ui.severity.info": "Info",
"ui.severity.none": "None",
"ui.release_orchestrator.title": "Release Orchestrator",
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
"ui.risk_dashboard.title": "Risk Profiles",
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
"ui.risk_dashboard.up_to_date": "Up to date",
"ui.risk_dashboard.last_computation": "Last Computation",
"ui.risk_dashboard.search_placeholder": "Title contains",
"ui.risk_dashboard.evaluated": "Evaluated",
"ui.risk_dashboard.risks_suffix": "risks.",
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
"ui.findings.title": "Findings",
"ui.findings.search_placeholder": "Search findings...",
"ui.findings.clear_filters": "Clear Filters",
"ui.findings.bulk_triage": "Bulk Triage",
"ui.findings.export_all": "Export all findings",
"ui.findings.export_selected": "Export selected findings",
"ui.findings.select_all": "Select all findings",
"ui.findings.trust": "Trust",
"ui.findings.advisory": "Advisory",
"ui.findings.package": "Package",
"ui.findings.flags": "Flags",
"ui.findings.why": "Why",
"ui.findings.select": "Select",
"ui.findings.no_findings": "No findings to display.",
"ui.findings.no_match": "No findings match the current filters.",
"ui.sources_dashboard.title": "Sources Dashboard",
"ui.sources_dashboard.verifying": "Verifying...",
"ui.sources_dashboard.verify_24h": "Verify last 24h",
"ui.sources_dashboard.loading_aoc": "Loading AOC metrics...",
"ui.sources_dashboard.pass_fail_title": "AOC Pass/Fail",
"ui.sources_dashboard.pass_rate": "Pass Rate",
"ui.sources_dashboard.passed": "Passed",
"ui.sources_dashboard.failed": "Failed",
"ui.sources_dashboard.recent_violations": "Recent Violations",
"ui.sources_dashboard.no_violations": "No violations in time window",
"ui.sources_dashboard.throughput_title": "Ingest Throughput",
"ui.sources_dashboard.docs_per_min": "docs/min",
"ui.sources_dashboard.avg_ms": "avg ms",
"ui.sources_dashboard.p95_ms": "p95 ms",
"ui.sources_dashboard.queue": "queue",
"ui.sources_dashboard.errors": "errors",
"ui.sources_dashboard.verification_complete": "Verification Complete",
"ui.sources_dashboard.checked": "Checked:",
"ui.sources_dashboard.violations": "violation(s)",
"ui.sources_dashboard.field": "Field:",
"ui.sources_dashboard.expected": "expected:",
"ui.sources_dashboard.actual": "actual:",
"ui.sources_dashboard.cli_equivalent": "CLI equivalent:",
"ui.sources_dashboard.data_from": "Data from",
"ui.sources_dashboard.to": "to",
"ui.sources_dashboard.hour_window": "h window",
"ui.timeline.title": "Timeline",
"ui.timeline.event_timeline": "Event Timeline",
"ui.timeline.refresh_timeline": "Refresh timeline",
"ui.timeline.loading": "Loading timeline...",
"ui.timeline.empty_state": "Enter a correlation ID to view the event timeline",
"ui.timeline.critical_path": "Critical path analysis",
"ui.timeline.causal_lanes": "Event causal lanes",
"ui.timeline.load_more": "Load more events",
"ui.timeline.event_details": "Event details",
"ui.timeline.events": "events",
"ui.exception_center.title": "Exception Center",
"ui.exception_center.list_view": "List view",
"ui.exception_center.kanban_view": "Kanban view",
"ui.exception_center.new_exception": "+ New Exception",
"ui.exception_center.search_placeholder": "Search exceptions...",
"ui.exception_center.type_vulnerability": "vulnerability",
"ui.exception_center.type_license": "license",
"ui.exception_center.type_policy": "policy",
"ui.exception_center.type_entropy": "entropy",
"ui.exception_center.type_determinism": "determinism",
"ui.exception_center.expiring_soon": "Expiring soon",
"ui.exception_center.clear_filters": "Clear filters",
"ui.exception_center.audit_label": "[A]",
"ui.exception_center.audit_title": "View audit log",
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
"ui.exception_center.column_empty": "No exceptions",
"ui.exception_center.exceptions_suffix": "exceptions",
"ui.evidence_thread.back_to_list": "Back to list",
"ui.evidence_thread.title_default": "Evidence Thread",
"ui.evidence_thread.copy_digest": "Copy full digest",
"ui.evidence_thread.risk_label": "Risk:",
"ui.evidence_thread.nodes": "nodes",
"ui.evidence_thread.loading": "Loading evidence thread...",
"ui.evidence_thread.graph_tab": "Graph",
"ui.evidence_thread.timeline_tab": "Timeline",
"ui.evidence_thread.transcript_tab": "Transcript",
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
"ui.vulnerability_detail.eyebrow": "Vulnerability",
"ui.vulnerability_detail.cvss": "CVSS",
"ui.vulnerability_detail.impact_first": "Impact First",
"ui.vulnerability_detail.epss": "EPSS",
"ui.vulnerability_detail.kev": "KEV",
"ui.vulnerability_detail.kev_listed": "Listed",
"ui.vulnerability_detail.kev_not_listed": "Not listed",
"ui.vulnerability_detail.reachability": "Reachability",
"ui.vulnerability_detail.blast_radius": "Blast Radius",
"ui.vulnerability_detail.assets": "assets",
"ui.vulnerability_detail.binary_resolution": "Binary Resolution",
"ui.vulnerability_detail.evidence_suffix": "evidence",
"ui.vulnerability_detail.fingerprint_note": "This binary was identified as patched using fingerprint analysis, not just version matching.",
"ui.vulnerability_detail.affected_components": "Affected Components",
"ui.vulnerability_detail.fix": "fix",
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
"ui.vulnerability_detail.references": "References",
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
}

View File

@@ -0,0 +1,29 @@
{
"_meta": { "locale": "fr-FR", "namespace": "platform", "version": "1.0" },
"platform.health.status_healthy": "Tous les systemes sont operationnels.",
"platform.health.status_degraded": "Certains services rencontrent des problemes.",
"platform.health.status_unavailable": "La plateforme est actuellement indisponible.",
"platform.quota.limit_exceeded": "La limite de quota est depassee pour {0}.",
"platform.quota.usage_warning": "L utilisation est a {0}% de la limite de quota.",
"platform.quota.reset_at": "Le quota sera reinitialise a {0}.",
"platform.onboarding.welcome": "Bienvenue sur StellaOps.",
"platform.onboarding.step_authority": "Configurez le fournisseur d identite.",
"platform.onboarding.step_registry": "Connectez le registre de conteneurs.",
"platform.onboarding.step_environments": "Definissez les environnements cibles.",
"platform.onboarding.step_complete": "Configuration terminee. Pret a demarrer.",
"platform.setup.required": "Une configuration initiale est requise avant utilisation.",
"platform.setup.in_progress": "La configuration est en cours.",
"platform.setup.complete": "La configuration est terminee.",
"platform.context.region_not_found": "Region {0} introuvable.",
"platform.context.environment_not_found": "Environnement {0} introuvable.",
"platform.migration.started": "Migration demarree.",
"platform.migration.completed": "Migration terminee avec succes.",
"platform.migration.failed": "Echec de la migration: {0}."
}

View File

@@ -0,0 +1,275 @@
{
"_meta": { "locale": "fr-FR", "namespace": "ui", "version": "1.0" },
"ui.loading.skeleton": "Loading...",
"ui.loading.spinner": "Please wait...",
"ui.loading.slow": "This is taking longer than expected...",
"ui.error.generic": "Something went wrong.",
"ui.error.network": "Network error. Check your connection.",
"ui.error.timeout": "Request timed out. Please try again.",
"ui.error.not_found": "The requested resource was not found.",
"ui.error.unauthorized": "You don't have permission to view this.",
"ui.error.server_error": "Server error. Please try again later.",
"ui.error.try_again": "Try again",
"ui.error.go_back": "Go back",
"ui.offline.banner": "You're offline.",
"ui.offline.description": "Some features may be unavailable.",
"ui.offline.reconnecting": "Reconnecting...",
"ui.offline.reconnected": "Back online.",
"ui.toast.success": "Success",
"ui.toast.info": "Info",
"ui.toast.warning": "Warning",
"ui.toast.error": "Error",
"ui.toast.dismiss": "Dismiss",
"ui.toast.undo": "Undo",
"ui.actions.save": "Save",
"ui.actions.saving": "Saving...",
"ui.actions.saved": "Saved",
"ui.actions.cancel": "Cancel",
"ui.actions.confirm": "Confirm",
"ui.actions.delete": "Delete",
"ui.actions.deleting": "Deleting...",
"ui.actions.deleted": "Deleted",
"ui.actions.submit": "Submit",
"ui.actions.submitting": "Submitting...",
"ui.actions.submitted": "Submitted",
"ui.actions.close": "Close",
"ui.actions.expand": "Expand",
"ui.actions.collapse": "Collapse",
"ui.actions.show_more": "Show more",
"ui.actions.show_less": "Show less",
"ui.actions.retry": "Retry",
"ui.actions.refresh": "Refresh",
"ui.actions.export": "Export",
"ui.actions.search": "Search",
"ui.actions.clear": "Clear",
"ui.actions.view": "View",
"ui.actions.dismiss": "Dismiss",
"ui.actions.show": "Show",
"ui.actions.hide": "Hide",
"ui.actions.sign_in": "Sign in",
"ui.actions.back_to_list": "Back to list",
"ui.actions.load_more": "Load more",
"ui.labels.all": "All",
"ui.labels.title": "Title",
"ui.labels.description": "Description",
"ui.labels.status": "Status",
"ui.labels.score": "Score",
"ui.labels.severity": "Severity",
"ui.labels.details": "Details",
"ui.labels.actions": "Actions",
"ui.labels.type": "Type",
"ui.labels.tags": "Tags",
"ui.labels.filters": "Filters",
"ui.labels.updated": "Updated",
"ui.labels.showing": "Showing",
"ui.labels.of": "of",
"ui.labels.total": "Total",
"ui.labels.not_applicable": "n/a",
"ui.labels.selected": "selected",
"ui.labels.last_updated": "Last updated:",
"ui.labels.expires": "Expires",
"ui.validation.required": "This field is required.",
"ui.validation.invalid": "Invalid value.",
"ui.validation.too_long": "Maximum {max} characters allowed.",
"ui.validation.too_short": "Minimum {min} characters required.",
"ui.validation.invalid_email": "Please enter a valid email address.",
"ui.validation.invalid_url": "Please enter a valid URL.",
"ui.a11y.loading": "Content is loading.",
"ui.a11y.loaded": "Content loaded.",
"ui.a11y.error": "An error occurred.",
"ui.a11y.expanded": "Expanded",
"ui.a11y.collapsed": "Collapsed",
"ui.a11y.selected": "Selected",
"ui.a11y.deselected": "Deselected",
"ui.a11y.required": "Required field",
"ui.a11y.optional": "Optional",
"ui.motion.reduced": "Animations reduced.",
"ui.motion.enabled": "Animations enabled.",
"ui.auth.fresh_active": "Fresh auth: Active",
"ui.auth.fresh_stale": "Fresh auth: Stale",
"ui.locale.label": "Langue",
"ui.locale.en_us": "Anglais (Etats-Unis)",
"ui.locale.de_de": "Allemand (Allemagne)",
"ui.locale.bg_bg": "Bulgare (Bulgarie)",
"ui.locale.ru_ru": "Russe (Russie)",
"ui.locale.es_es": "Espagnol (Espagne)",
"ui.locale.fr_fr": "Francais (France)",
"ui.locale.zh_tw": "Chinois traditionnel (Taiwan)",
"ui.locale.zh_cn": "Chinois simplifie (Chine)",
"ui.locale.uk_ua": "Ukrainian (Ukraine)",
"ui.settings.language.title": "Langue",
"ui.settings.language.subtitle": "Definissez votre langue de console preferee.",
"ui.settings.language.description": "Les changements sont appliques immediatement dans l UI.",
"ui.settings.language.selector_label": "Langue preferee",
"ui.settings.language.persisted": "Enregistre pour votre compte et reutilise par le CLI.",
"ui.settings.language.persisted_error": "Enregistre localement, mais la synchronisation du compte a echoue.",
"ui.settings.language.sign_in_hint": "Connectez-vous pour synchroniser cette preference avec le CLI.",
"ui.first_signal.label": "First signal",
"ui.first_signal.run_prefix": "Run:",
"ui.first_signal.live": "Live",
"ui.first_signal.polling": "Polling",
"ui.first_signal.range_prefix": "Range",
"ui.first_signal.range_separator": "\u2013",
"ui.first_signal.stage_separator": " \u00b7 ",
"ui.first_signal.waiting": "Waiting for first signal\u2026",
"ui.first_signal.not_available": "Signal not available yet.",
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
"ui.first_signal.failed": "Failed to load signal.",
"ui.first_signal.retry": "Retry",
"ui.first_signal.try_again": "Try again",
"ui.first_signal.kind.queued": "Queued",
"ui.first_signal.kind.started": "Started",
"ui.first_signal.kind.phase": "In progress",
"ui.first_signal.kind.blocked": "Blocked",
"ui.first_signal.kind.failed": "Failed",
"ui.first_signal.kind.succeeded": "Succeeded",
"ui.first_signal.kind.canceled": "Canceled",
"ui.first_signal.kind.unavailable": "Unavailable",
"ui.first_signal.kind.unknown": "Signal",
"ui.first_signal.stage.resolve": "Resolving",
"ui.first_signal.stage.fetch": "Fetching",
"ui.first_signal.stage.restore": "Restoring",
"ui.first_signal.stage.analyze": "Analyzing",
"ui.first_signal.stage.policy": "Evaluating policy",
"ui.first_signal.stage.report": "Generating report",
"ui.first_signal.stage.unknown": "Processing",
"ui.first_signal.aria.card_label": "First signal status",
"ui.severity.critical": "Critical",
"ui.severity.high": "High",
"ui.severity.medium": "Medium",
"ui.severity.low": "Low",
"ui.severity.info": "Info",
"ui.severity.none": "None",
"ui.release_orchestrator.title": "Release Orchestrator",
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
"ui.risk_dashboard.title": "Risk Profiles",
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
"ui.risk_dashboard.up_to_date": "Up to date",
"ui.risk_dashboard.last_computation": "Last Computation",
"ui.risk_dashboard.search_placeholder": "Title contains",
"ui.risk_dashboard.evaluated": "Evaluated",
"ui.risk_dashboard.risks_suffix": "risks.",
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
"ui.findings.title": "Findings",
"ui.findings.search_placeholder": "Search findings...",
"ui.findings.clear_filters": "Clear Filters",
"ui.findings.bulk_triage": "Bulk Triage",
"ui.findings.export_all": "Export all findings",
"ui.findings.export_selected": "Export selected findings",
"ui.findings.select_all": "Select all findings",
"ui.findings.trust": "Trust",
"ui.findings.advisory": "Advisory",
"ui.findings.package": "Package",
"ui.findings.flags": "Flags",
"ui.findings.why": "Why",
"ui.findings.select": "Select",
"ui.findings.no_findings": "No findings to display.",
"ui.findings.no_match": "No findings match the current filters.",
"ui.sources_dashboard.title": "Sources Dashboard",
"ui.sources_dashboard.verifying": "Verifying...",
"ui.sources_dashboard.verify_24h": "Verify last 24h",
"ui.sources_dashboard.loading_aoc": "Loading AOC metrics...",
"ui.sources_dashboard.pass_fail_title": "AOC Pass/Fail",
"ui.sources_dashboard.pass_rate": "Pass Rate",
"ui.sources_dashboard.passed": "Passed",
"ui.sources_dashboard.failed": "Failed",
"ui.sources_dashboard.recent_violations": "Recent Violations",
"ui.sources_dashboard.no_violations": "No violations in time window",
"ui.sources_dashboard.throughput_title": "Ingest Throughput",
"ui.sources_dashboard.docs_per_min": "docs/min",
"ui.sources_dashboard.avg_ms": "avg ms",
"ui.sources_dashboard.p95_ms": "p95 ms",
"ui.sources_dashboard.queue": "queue",
"ui.sources_dashboard.errors": "errors",
"ui.sources_dashboard.verification_complete": "Verification Complete",
"ui.sources_dashboard.checked": "Checked:",
"ui.sources_dashboard.violations": "violation(s)",
"ui.sources_dashboard.field": "Field:",
"ui.sources_dashboard.expected": "expected:",
"ui.sources_dashboard.actual": "actual:",
"ui.sources_dashboard.cli_equivalent": "CLI equivalent:",
"ui.sources_dashboard.data_from": "Data from",
"ui.sources_dashboard.to": "to",
"ui.sources_dashboard.hour_window": "h window",
"ui.timeline.title": "Timeline",
"ui.timeline.event_timeline": "Event Timeline",
"ui.timeline.refresh_timeline": "Refresh timeline",
"ui.timeline.loading": "Loading timeline...",
"ui.timeline.empty_state": "Enter a correlation ID to view the event timeline",
"ui.timeline.critical_path": "Critical path analysis",
"ui.timeline.causal_lanes": "Event causal lanes",
"ui.timeline.load_more": "Load more events",
"ui.timeline.event_details": "Event details",
"ui.timeline.events": "events",
"ui.exception_center.title": "Exception Center",
"ui.exception_center.list_view": "List view",
"ui.exception_center.kanban_view": "Kanban view",
"ui.exception_center.new_exception": "+ New Exception",
"ui.exception_center.search_placeholder": "Search exceptions...",
"ui.exception_center.type_vulnerability": "vulnerability",
"ui.exception_center.type_license": "license",
"ui.exception_center.type_policy": "policy",
"ui.exception_center.type_entropy": "entropy",
"ui.exception_center.type_determinism": "determinism",
"ui.exception_center.expiring_soon": "Expiring soon",
"ui.exception_center.clear_filters": "Clear filters",
"ui.exception_center.audit_label": "[A]",
"ui.exception_center.audit_title": "View audit log",
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
"ui.exception_center.column_empty": "No exceptions",
"ui.exception_center.exceptions_suffix": "exceptions",
"ui.evidence_thread.back_to_list": "Back to list",
"ui.evidence_thread.title_default": "Evidence Thread",
"ui.evidence_thread.copy_digest": "Copy full digest",
"ui.evidence_thread.risk_label": "Risk:",
"ui.evidence_thread.nodes": "nodes",
"ui.evidence_thread.loading": "Loading evidence thread...",
"ui.evidence_thread.graph_tab": "Graph",
"ui.evidence_thread.timeline_tab": "Timeline",
"ui.evidence_thread.transcript_tab": "Transcript",
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
"ui.vulnerability_detail.eyebrow": "Vulnerability",
"ui.vulnerability_detail.cvss": "CVSS",
"ui.vulnerability_detail.impact_first": "Impact First",
"ui.vulnerability_detail.epss": "EPSS",
"ui.vulnerability_detail.kev": "KEV",
"ui.vulnerability_detail.kev_listed": "Listed",
"ui.vulnerability_detail.kev_not_listed": "Not listed",
"ui.vulnerability_detail.reachability": "Reachability",
"ui.vulnerability_detail.blast_radius": "Blast Radius",
"ui.vulnerability_detail.assets": "assets",
"ui.vulnerability_detail.binary_resolution": "Binary Resolution",
"ui.vulnerability_detail.evidence_suffix": "evidence",
"ui.vulnerability_detail.fingerprint_note": "This binary was identified as patched using fingerprint analysis, not just version matching.",
"ui.vulnerability_detail.affected_components": "Affected Components",
"ui.vulnerability_detail.fix": "fix",
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
"ui.vulnerability_detail.references": "References",
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
}

View File

@@ -0,0 +1,29 @@
{
"_meta": { "locale": "ru-RU", "namespace": "platform", "version": "1.0" },
"platform.health.status_healthy": "Vse sistemy rabotayut normalno.",
"platform.health.status_degraded": "U nekotoryh servisov est problemy.",
"platform.health.status_unavailable": "Platforma vremenno nedostupna.",
"platform.quota.limit_exceeded": "Limit kvoty dlya {0} prevyshen.",
"platform.quota.usage_warning": "Ispolzovanie sostavlyaet {0}% ot limita kvoty.",
"platform.quota.reset_at": "Kvota budet sbroshena v {0}.",
"platform.onboarding.welcome": "Dobro pozhalovat v StellaOps.",
"platform.onboarding.step_authority": "Nastroyte postavshchika identichnosti.",
"platform.onboarding.step_registry": "Podklyuchite registry konteynerov.",
"platform.onboarding.step_environments": "Opredelite tselevye sredy.",
"platform.onboarding.step_complete": "Nastroyka zavershena. Mozhno nachinat rabotu.",
"platform.setup.required": "Pered ispolzovaniem platformy trebuetsya nachalnaya nastroyka.",
"platform.setup.in_progress": "Nastroyka vypolnyaetsya.",
"platform.setup.complete": "Nastroyka zavershena.",
"platform.context.region_not_found": "Region {0} ne naiden.",
"platform.context.environment_not_found": "Sreda {0} ne naidena.",
"platform.migration.started": "Migratsiya zapushchena.",
"platform.migration.completed": "Migratsiya uspeshno zavershena.",
"platform.migration.failed": "Migratsiya zavershilas oshibkoy: {0}."
}

View File

@@ -0,0 +1,275 @@
{
"_meta": { "locale": "ru-RU", "namespace": "ui", "version": "1.0" },
"ui.loading.skeleton": "Loading...",
"ui.loading.spinner": "Please wait...",
"ui.loading.slow": "This is taking longer than expected...",
"ui.error.generic": "Something went wrong.",
"ui.error.network": "Network error. Check your connection.",
"ui.error.timeout": "Request timed out. Please try again.",
"ui.error.not_found": "The requested resource was not found.",
"ui.error.unauthorized": "You don't have permission to view this.",
"ui.error.server_error": "Server error. Please try again later.",
"ui.error.try_again": "Try again",
"ui.error.go_back": "Go back",
"ui.offline.banner": "You're offline.",
"ui.offline.description": "Some features may be unavailable.",
"ui.offline.reconnecting": "Reconnecting...",
"ui.offline.reconnected": "Back online.",
"ui.toast.success": "Success",
"ui.toast.info": "Info",
"ui.toast.warning": "Warning",
"ui.toast.error": "Error",
"ui.toast.dismiss": "Dismiss",
"ui.toast.undo": "Undo",
"ui.actions.save": "Save",
"ui.actions.saving": "Saving...",
"ui.actions.saved": "Saved",
"ui.actions.cancel": "Cancel",
"ui.actions.confirm": "Confirm",
"ui.actions.delete": "Delete",
"ui.actions.deleting": "Deleting...",
"ui.actions.deleted": "Deleted",
"ui.actions.submit": "Submit",
"ui.actions.submitting": "Submitting...",
"ui.actions.submitted": "Submitted",
"ui.actions.close": "Close",
"ui.actions.expand": "Expand",
"ui.actions.collapse": "Collapse",
"ui.actions.show_more": "Show more",
"ui.actions.show_less": "Show less",
"ui.actions.retry": "Retry",
"ui.actions.refresh": "Refresh",
"ui.actions.export": "Export",
"ui.actions.search": "Search",
"ui.actions.clear": "Clear",
"ui.actions.view": "View",
"ui.actions.dismiss": "Dismiss",
"ui.actions.show": "Show",
"ui.actions.hide": "Hide",
"ui.actions.sign_in": "Sign in",
"ui.actions.back_to_list": "Back to list",
"ui.actions.load_more": "Load more",
"ui.labels.all": "All",
"ui.labels.title": "Title",
"ui.labels.description": "Description",
"ui.labels.status": "Status",
"ui.labels.score": "Score",
"ui.labels.severity": "Severity",
"ui.labels.details": "Details",
"ui.labels.actions": "Actions",
"ui.labels.type": "Type",
"ui.labels.tags": "Tags",
"ui.labels.filters": "Filters",
"ui.labels.updated": "Updated",
"ui.labels.showing": "Showing",
"ui.labels.of": "of",
"ui.labels.total": "Total",
"ui.labels.not_applicable": "n/a",
"ui.labels.selected": "selected",
"ui.labels.last_updated": "Last updated:",
"ui.labels.expires": "Expires",
"ui.validation.required": "This field is required.",
"ui.validation.invalid": "Invalid value.",
"ui.validation.too_long": "Maximum {max} characters allowed.",
"ui.validation.too_short": "Minimum {min} characters required.",
"ui.validation.invalid_email": "Please enter a valid email address.",
"ui.validation.invalid_url": "Please enter a valid URL.",
"ui.a11y.loading": "Content is loading.",
"ui.a11y.loaded": "Content loaded.",
"ui.a11y.error": "An error occurred.",
"ui.a11y.expanded": "Expanded",
"ui.a11y.collapsed": "Collapsed",
"ui.a11y.selected": "Selected",
"ui.a11y.deselected": "Deselected",
"ui.a11y.required": "Required field",
"ui.a11y.optional": "Optional",
"ui.motion.reduced": "Animations reduced.",
"ui.motion.enabled": "Animations enabled.",
"ui.auth.fresh_active": "Fresh auth: Active",
"ui.auth.fresh_stale": "Fresh auth: Stale",
"ui.locale.label": "Yazyk",
"ui.locale.en_us": "Angliyskiy (USA)",
"ui.locale.de_de": "Nemetskiy (Germaniya)",
"ui.locale.bg_bg": "Bolgarskiy (Bolgariya)",
"ui.locale.ru_ru": "Russkiy (Rossiya)",
"ui.locale.es_es": "Ispanskiy (Ispaniya)",
"ui.locale.fr_fr": "Frantsuzskiy (Frantsiya)",
"ui.locale.zh_tw": "Kitayskiy tradicionnyy (Taiwan)",
"ui.locale.zh_cn": "Kitayskiy uproshchennyy (Kitay)",
"ui.locale.uk_ua": "Ukrainian (Ukraine)",
"ui.settings.language.title": "Yazyk",
"ui.settings.language.subtitle": "Vyberite predpochtitelnyy yazyk konsoli.",
"ui.settings.language.description": "Izmeneniya primenyayutsya srazu v UI.",
"ui.settings.language.selector_label": "Predpochtitelnyy yazyk",
"ui.settings.language.persisted": "Sohraneno dlya vashego akkaunta i ispolzuetsya v CLI.",
"ui.settings.language.persisted_error": "Lokalno sohraneno, no sinkhronizatsiya akkaunta ne udalas.",
"ui.settings.language.sign_in_hint": "Vypolnite vkhod, chtoby sinkhronizirovat etu nastroiku s CLI.",
"ui.first_signal.label": "First signal",
"ui.first_signal.run_prefix": "Run:",
"ui.first_signal.live": "Live",
"ui.first_signal.polling": "Polling",
"ui.first_signal.range_prefix": "Range",
"ui.first_signal.range_separator": "\u2013",
"ui.first_signal.stage_separator": " \u00b7 ",
"ui.first_signal.waiting": "Waiting for first signal\u2026",
"ui.first_signal.not_available": "Signal not available yet.",
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
"ui.first_signal.failed": "Failed to load signal.",
"ui.first_signal.retry": "Retry",
"ui.first_signal.try_again": "Try again",
"ui.first_signal.kind.queued": "Queued",
"ui.first_signal.kind.started": "Started",
"ui.first_signal.kind.phase": "In progress",
"ui.first_signal.kind.blocked": "Blocked",
"ui.first_signal.kind.failed": "Failed",
"ui.first_signal.kind.succeeded": "Succeeded",
"ui.first_signal.kind.canceled": "Canceled",
"ui.first_signal.kind.unavailable": "Unavailable",
"ui.first_signal.kind.unknown": "Signal",
"ui.first_signal.stage.resolve": "Resolving",
"ui.first_signal.stage.fetch": "Fetching",
"ui.first_signal.stage.restore": "Restoring",
"ui.first_signal.stage.analyze": "Analyzing",
"ui.first_signal.stage.policy": "Evaluating policy",
"ui.first_signal.stage.report": "Generating report",
"ui.first_signal.stage.unknown": "Processing",
"ui.first_signal.aria.card_label": "First signal status",
"ui.severity.critical": "Critical",
"ui.severity.high": "High",
"ui.severity.medium": "Medium",
"ui.severity.low": "Low",
"ui.severity.info": "Info",
"ui.severity.none": "None",
"ui.release_orchestrator.title": "Release Orchestrator",
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
"ui.risk_dashboard.title": "Risk Profiles",
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
"ui.risk_dashboard.up_to_date": "Up to date",
"ui.risk_dashboard.last_computation": "Last Computation",
"ui.risk_dashboard.search_placeholder": "Title contains",
"ui.risk_dashboard.evaluated": "Evaluated",
"ui.risk_dashboard.risks_suffix": "risks.",
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
"ui.findings.title": "Findings",
"ui.findings.search_placeholder": "Search findings...",
"ui.findings.clear_filters": "Clear Filters",
"ui.findings.bulk_triage": "Bulk Triage",
"ui.findings.export_all": "Export all findings",
"ui.findings.export_selected": "Export selected findings",
"ui.findings.select_all": "Select all findings",
"ui.findings.trust": "Trust",
"ui.findings.advisory": "Advisory",
"ui.findings.package": "Package",
"ui.findings.flags": "Flags",
"ui.findings.why": "Why",
"ui.findings.select": "Select",
"ui.findings.no_findings": "No findings to display.",
"ui.findings.no_match": "No findings match the current filters.",
"ui.sources_dashboard.title": "Sources Dashboard",
"ui.sources_dashboard.verifying": "Verifying...",
"ui.sources_dashboard.verify_24h": "Verify last 24h",
"ui.sources_dashboard.loading_aoc": "Loading AOC metrics...",
"ui.sources_dashboard.pass_fail_title": "AOC Pass/Fail",
"ui.sources_dashboard.pass_rate": "Pass Rate",
"ui.sources_dashboard.passed": "Passed",
"ui.sources_dashboard.failed": "Failed",
"ui.sources_dashboard.recent_violations": "Recent Violations",
"ui.sources_dashboard.no_violations": "No violations in time window",
"ui.sources_dashboard.throughput_title": "Ingest Throughput",
"ui.sources_dashboard.docs_per_min": "docs/min",
"ui.sources_dashboard.avg_ms": "avg ms",
"ui.sources_dashboard.p95_ms": "p95 ms",
"ui.sources_dashboard.queue": "queue",
"ui.sources_dashboard.errors": "errors",
"ui.sources_dashboard.verification_complete": "Verification Complete",
"ui.sources_dashboard.checked": "Checked:",
"ui.sources_dashboard.violations": "violation(s)",
"ui.sources_dashboard.field": "Field:",
"ui.sources_dashboard.expected": "expected:",
"ui.sources_dashboard.actual": "actual:",
"ui.sources_dashboard.cli_equivalent": "CLI equivalent:",
"ui.sources_dashboard.data_from": "Data from",
"ui.sources_dashboard.to": "to",
"ui.sources_dashboard.hour_window": "h window",
"ui.timeline.title": "Timeline",
"ui.timeline.event_timeline": "Event Timeline",
"ui.timeline.refresh_timeline": "Refresh timeline",
"ui.timeline.loading": "Loading timeline...",
"ui.timeline.empty_state": "Enter a correlation ID to view the event timeline",
"ui.timeline.critical_path": "Critical path analysis",
"ui.timeline.causal_lanes": "Event causal lanes",
"ui.timeline.load_more": "Load more events",
"ui.timeline.event_details": "Event details",
"ui.timeline.events": "events",
"ui.exception_center.title": "Exception Center",
"ui.exception_center.list_view": "List view",
"ui.exception_center.kanban_view": "Kanban view",
"ui.exception_center.new_exception": "+ New Exception",
"ui.exception_center.search_placeholder": "Search exceptions...",
"ui.exception_center.type_vulnerability": "vulnerability",
"ui.exception_center.type_license": "license",
"ui.exception_center.type_policy": "policy",
"ui.exception_center.type_entropy": "entropy",
"ui.exception_center.type_determinism": "determinism",
"ui.exception_center.expiring_soon": "Expiring soon",
"ui.exception_center.clear_filters": "Clear filters",
"ui.exception_center.audit_label": "[A]",
"ui.exception_center.audit_title": "View audit log",
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
"ui.exception_center.column_empty": "No exceptions",
"ui.exception_center.exceptions_suffix": "exceptions",
"ui.evidence_thread.back_to_list": "Back to list",
"ui.evidence_thread.title_default": "Evidence Thread",
"ui.evidence_thread.copy_digest": "Copy full digest",
"ui.evidence_thread.risk_label": "Risk:",
"ui.evidence_thread.nodes": "nodes",
"ui.evidence_thread.loading": "Loading evidence thread...",
"ui.evidence_thread.graph_tab": "Graph",
"ui.evidence_thread.timeline_tab": "Timeline",
"ui.evidence_thread.transcript_tab": "Transcript",
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
"ui.vulnerability_detail.eyebrow": "Vulnerability",
"ui.vulnerability_detail.cvss": "CVSS",
"ui.vulnerability_detail.impact_first": "Impact First",
"ui.vulnerability_detail.epss": "EPSS",
"ui.vulnerability_detail.kev": "KEV",
"ui.vulnerability_detail.kev_listed": "Listed",
"ui.vulnerability_detail.kev_not_listed": "Not listed",
"ui.vulnerability_detail.reachability": "Reachability",
"ui.vulnerability_detail.blast_radius": "Blast Radius",
"ui.vulnerability_detail.assets": "assets",
"ui.vulnerability_detail.binary_resolution": "Binary Resolution",
"ui.vulnerability_detail.evidence_suffix": "evidence",
"ui.vulnerability_detail.fingerprint_note": "This binary was identified as patched using fingerprint analysis, not just version matching.",
"ui.vulnerability_detail.affected_components": "Affected Components",
"ui.vulnerability_detail.fix": "fix",
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
"ui.vulnerability_detail.references": "References",
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
}

View File

@@ -0,0 +1,28 @@
{
"_meta": { "locale": "uk-UA", "namespace": "platform", "version": "1.0" },
"platform.health.status_healthy": "Usi systemy pratsiuiut normalno.",
"platform.health.status_degraded": "Deiaki servisy maiut problemy.",
"platform.health.status_unavailable": "Platforma tymchasovo nedostupna.",
"platform.quota.limit_exceeded": "Perevyshcheno limit kvoty dlia {0}.",
"platform.quota.usage_warning": "Vykorystannia stanovyt {0}% vid limitu kvoty.",
"platform.quota.reset_at": "Kvota bude skynuta o {0}.",
"platform.onboarding.welcome": "Laskavo prosymo do StellaOps.",
"platform.onboarding.step_authority": "Nalashtuite providera identyfikatsii.",
"platform.onboarding.step_registry": "Pidkliuchit reiestr kontejneriv.",
"platform.onboarding.step_environments": "Vyznachit cilovi seredovyshcha.",
"platform.onboarding.step_complete": "Nalashtuvannia zaversheno. Mozhna pochaty robotu.",
"platform.setup.required": "Pered vykorystanniam platformy potribne pochatkove nalashtuvannia.",
"platform.setup.in_progress": "Nalashtuvannia vykonuietsia.",
"platform.setup.complete": "Nalashtuvannia zaversheno.",
"platform.context.region_not_found": "Region {0} ne znahdeno.",
"platform.context.environment_not_found": "Seredovyshche {0} ne znahdeno.",
"platform.migration.started": "Mihratsiiu zapushcheno.",
"platform.migration.completed": "Mihratsiiu uspishno zaversheno.",
"platform.migration.failed": "Pomylka mihratsii: {0}."
}

View File

@@ -0,0 +1,275 @@
{
"_meta": { "locale": "uk-UA", "namespace": "ui", "version": "1.0" },
"ui.loading.skeleton": "Loading...",
"ui.loading.spinner": "Please wait...",
"ui.loading.slow": "This is taking longer than expected...",
"ui.error.generic": "Something went wrong.",
"ui.error.network": "Network error. Check your connection.",
"ui.error.timeout": "Request timed out. Please try again.",
"ui.error.not_found": "The requested resource was not found.",
"ui.error.unauthorized": "You don't have permission to view this.",
"ui.error.server_error": "Server error. Please try again later.",
"ui.error.try_again": "Try again",
"ui.error.go_back": "Go back",
"ui.offline.banner": "You're offline.",
"ui.offline.description": "Some features may be unavailable.",
"ui.offline.reconnecting": "Reconnecting...",
"ui.offline.reconnected": "Back online.",
"ui.toast.success": "Success",
"ui.toast.info": "Info",
"ui.toast.warning": "Warning",
"ui.toast.error": "Error",
"ui.toast.dismiss": "Dismiss",
"ui.toast.undo": "Undo",
"ui.actions.save": "Save",
"ui.actions.saving": "Saving...",
"ui.actions.saved": "Saved",
"ui.actions.cancel": "Cancel",
"ui.actions.confirm": "Confirm",
"ui.actions.delete": "Delete",
"ui.actions.deleting": "Deleting...",
"ui.actions.deleted": "Deleted",
"ui.actions.submit": "Submit",
"ui.actions.submitting": "Submitting...",
"ui.actions.submitted": "Submitted",
"ui.actions.close": "Close",
"ui.actions.expand": "Expand",
"ui.actions.collapse": "Collapse",
"ui.actions.show_more": "Show more",
"ui.actions.show_less": "Show less",
"ui.actions.retry": "Retry",
"ui.actions.refresh": "Refresh",
"ui.actions.export": "Export",
"ui.actions.search": "Search",
"ui.actions.clear": "Clear",
"ui.actions.view": "View",
"ui.actions.dismiss": "Dismiss",
"ui.actions.show": "Show",
"ui.actions.hide": "Hide",
"ui.actions.sign_in": "Sign in",
"ui.actions.back_to_list": "Back to list",
"ui.actions.load_more": "Load more",
"ui.labels.all": "All",
"ui.labels.title": "Title",
"ui.labels.description": "Description",
"ui.labels.status": "Status",
"ui.labels.score": "Score",
"ui.labels.severity": "Severity",
"ui.labels.details": "Details",
"ui.labels.actions": "Actions",
"ui.labels.type": "Type",
"ui.labels.tags": "Tags",
"ui.labels.filters": "Filters",
"ui.labels.updated": "Updated",
"ui.labels.showing": "Showing",
"ui.labels.of": "of",
"ui.labels.total": "Total",
"ui.labels.not_applicable": "n/a",
"ui.labels.selected": "selected",
"ui.labels.last_updated": "Last updated:",
"ui.labels.expires": "Expires",
"ui.validation.required": "This field is required.",
"ui.validation.invalid": "Invalid value.",
"ui.validation.too_long": "Maximum {max} characters allowed.",
"ui.validation.too_short": "Minimum {min} characters required.",
"ui.validation.invalid_email": "Please enter a valid email address.",
"ui.validation.invalid_url": "Please enter a valid URL.",
"ui.a11y.loading": "Content is loading.",
"ui.a11y.loaded": "Content loaded.",
"ui.a11y.error": "An error occurred.",
"ui.a11y.expanded": "Expanded",
"ui.a11y.collapsed": "Collapsed",
"ui.a11y.selected": "Selected",
"ui.a11y.deselected": "Deselected",
"ui.a11y.required": "Required field",
"ui.a11y.optional": "Optional",
"ui.motion.reduced": "Animations reduced.",
"ui.motion.enabled": "Animations enabled.",
"ui.auth.fresh_active": "Fresh auth: Active",
"ui.auth.fresh_stale": "Fresh auth: Stale",
"ui.locale.label": "Mova",
"ui.locale.en_us": "Angliiska (SSHA)",
"ui.locale.de_de": "Nimetska (Nimechchyna)",
"ui.locale.bg_bg": "Bolgarska (Bolhariia)",
"ui.locale.ru_ru": "Rosiiska (Rosiia)",
"ui.locale.es_es": "Ispanska (Ispaniia)",
"ui.locale.fr_fr": "Frantsuzka (Frantsiia)",
"ui.locale.zh_tw": "Kytaiska tradytsiina (Taivan)",
"ui.locale.zh_cn": "Kytaiska sproshchena (Kytai)",
"ui.locale.uk_ua": "Ukrainska (Ukraina)",
"ui.settings.language.title": "Mova",
"ui.settings.language.subtitle": "Vstanovit bazhanu movu konsoli.",
"ui.settings.language.description": "Zminy zastosovuiutsia v UI odrazu.",
"ui.settings.language.selector_label": "Bazhana mova",
"ui.settings.language.persisted": "Zberezheno dlia vashoho oblikovoho zapysu ta povtorno vykorystovuietsia v CLI.",
"ui.settings.language.persisted_error": "Lokalno zberezheno, ale synkhronizatsiia oblikovoho zapysu ne vdlasia.",
"ui.settings.language.sign_in_hint": "Uvijdit, shchob synkhronizuvaty tsiu nalashtunku z CLI.",
"ui.first_signal.label": "First signal",
"ui.first_signal.run_prefix": "Run:",
"ui.first_signal.live": "Live",
"ui.first_signal.polling": "Polling",
"ui.first_signal.range_prefix": "Range",
"ui.first_signal.range_separator": "\u2013",
"ui.first_signal.stage_separator": " \u00b7 ",
"ui.first_signal.waiting": "Waiting for first signal\u2026",
"ui.first_signal.not_available": "Signal not available yet.",
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
"ui.first_signal.failed": "Failed to load signal.",
"ui.first_signal.retry": "Retry",
"ui.first_signal.try_again": "Try again",
"ui.first_signal.kind.queued": "Queued",
"ui.first_signal.kind.started": "Started",
"ui.first_signal.kind.phase": "In progress",
"ui.first_signal.kind.blocked": "Blocked",
"ui.first_signal.kind.failed": "Failed",
"ui.first_signal.kind.succeeded": "Succeeded",
"ui.first_signal.kind.canceled": "Canceled",
"ui.first_signal.kind.unavailable": "Unavailable",
"ui.first_signal.kind.unknown": "Signal",
"ui.first_signal.stage.resolve": "Resolving",
"ui.first_signal.stage.fetch": "Fetching",
"ui.first_signal.stage.restore": "Restoring",
"ui.first_signal.stage.analyze": "Analyzing",
"ui.first_signal.stage.policy": "Evaluating policy",
"ui.first_signal.stage.report": "Generating report",
"ui.first_signal.stage.unknown": "Processing",
"ui.first_signal.aria.card_label": "First signal status",
"ui.severity.critical": "Critical",
"ui.severity.high": "High",
"ui.severity.medium": "Medium",
"ui.severity.low": "Low",
"ui.severity.info": "Info",
"ui.severity.none": "None",
"ui.release_orchestrator.title": "Release Orchestrator",
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
"ui.risk_dashboard.title": "Risk Profiles",
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
"ui.risk_dashboard.up_to_date": "Up to date",
"ui.risk_dashboard.last_computation": "Last Computation",
"ui.risk_dashboard.search_placeholder": "Title contains",
"ui.risk_dashboard.evaluated": "Evaluated",
"ui.risk_dashboard.risks_suffix": "risks.",
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
"ui.findings.title": "Findings",
"ui.findings.search_placeholder": "Search findings...",
"ui.findings.clear_filters": "Clear Filters",
"ui.findings.bulk_triage": "Bulk Triage",
"ui.findings.export_all": "Export all findings",
"ui.findings.export_selected": "Export selected findings",
"ui.findings.select_all": "Select all findings",
"ui.findings.trust": "Trust",
"ui.findings.advisory": "Advisory",
"ui.findings.package": "Package",
"ui.findings.flags": "Flags",
"ui.findings.why": "Why",
"ui.findings.select": "Select",
"ui.findings.no_findings": "No findings to display.",
"ui.findings.no_match": "No findings match the current filters.",
"ui.sources_dashboard.title": "Sources Dashboard",
"ui.sources_dashboard.verifying": "Verifying...",
"ui.sources_dashboard.verify_24h": "Verify last 24h",
"ui.sources_dashboard.loading_aoc": "Loading AOC metrics...",
"ui.sources_dashboard.pass_fail_title": "AOC Pass/Fail",
"ui.sources_dashboard.pass_rate": "Pass Rate",
"ui.sources_dashboard.passed": "Passed",
"ui.sources_dashboard.failed": "Failed",
"ui.sources_dashboard.recent_violations": "Recent Violations",
"ui.sources_dashboard.no_violations": "No violations in time window",
"ui.sources_dashboard.throughput_title": "Ingest Throughput",
"ui.sources_dashboard.docs_per_min": "docs/min",
"ui.sources_dashboard.avg_ms": "avg ms",
"ui.sources_dashboard.p95_ms": "p95 ms",
"ui.sources_dashboard.queue": "queue",
"ui.sources_dashboard.errors": "errors",
"ui.sources_dashboard.verification_complete": "Verification Complete",
"ui.sources_dashboard.checked": "Checked:",
"ui.sources_dashboard.violations": "violation(s)",
"ui.sources_dashboard.field": "Field:",
"ui.sources_dashboard.expected": "expected:",
"ui.sources_dashboard.actual": "actual:",
"ui.sources_dashboard.cli_equivalent": "CLI equivalent:",
"ui.sources_dashboard.data_from": "Data from",
"ui.sources_dashboard.to": "to",
"ui.sources_dashboard.hour_window": "h window",
"ui.timeline.title": "Timeline",
"ui.timeline.event_timeline": "Event Timeline",
"ui.timeline.refresh_timeline": "Refresh timeline",
"ui.timeline.loading": "Loading timeline...",
"ui.timeline.empty_state": "Enter a correlation ID to view the event timeline",
"ui.timeline.critical_path": "Critical path analysis",
"ui.timeline.causal_lanes": "Event causal lanes",
"ui.timeline.load_more": "Load more events",
"ui.timeline.event_details": "Event details",
"ui.timeline.events": "events",
"ui.exception_center.title": "Exception Center",
"ui.exception_center.list_view": "List view",
"ui.exception_center.kanban_view": "Kanban view",
"ui.exception_center.new_exception": "+ New Exception",
"ui.exception_center.search_placeholder": "Search exceptions...",
"ui.exception_center.type_vulnerability": "vulnerability",
"ui.exception_center.type_license": "license",
"ui.exception_center.type_policy": "policy",
"ui.exception_center.type_entropy": "entropy",
"ui.exception_center.type_determinism": "determinism",
"ui.exception_center.expiring_soon": "Expiring soon",
"ui.exception_center.clear_filters": "Clear filters",
"ui.exception_center.audit_label": "[A]",
"ui.exception_center.audit_title": "View audit log",
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
"ui.exception_center.column_empty": "No exceptions",
"ui.exception_center.exceptions_suffix": "exceptions",
"ui.evidence_thread.back_to_list": "Back to list",
"ui.evidence_thread.title_default": "Evidence Thread",
"ui.evidence_thread.copy_digest": "Copy full digest",
"ui.evidence_thread.risk_label": "Risk:",
"ui.evidence_thread.nodes": "nodes",
"ui.evidence_thread.loading": "Loading evidence thread...",
"ui.evidence_thread.graph_tab": "Graph",
"ui.evidence_thread.timeline_tab": "Timeline",
"ui.evidence_thread.transcript_tab": "Transcript",
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
"ui.vulnerability_detail.eyebrow": "Vulnerability",
"ui.vulnerability_detail.cvss": "CVSS",
"ui.vulnerability_detail.impact_first": "Impact First",
"ui.vulnerability_detail.epss": "EPSS",
"ui.vulnerability_detail.kev": "KEV",
"ui.vulnerability_detail.kev_listed": "Listed",
"ui.vulnerability_detail.kev_not_listed": "Not listed",
"ui.vulnerability_detail.reachability": "Reachability",
"ui.vulnerability_detail.blast_radius": "Blast Radius",
"ui.vulnerability_detail.assets": "assets",
"ui.vulnerability_detail.binary_resolution": "Binary Resolution",
"ui.vulnerability_detail.evidence_suffix": "evidence",
"ui.vulnerability_detail.fingerprint_note": "This binary was identified as patched using fingerprint analysis, not just version matching.",
"ui.vulnerability_detail.affected_components": "Affected Components",
"ui.vulnerability_detail.fix": "fix",
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
"ui.vulnerability_detail.references": "References",
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
}

View File

@@ -0,0 +1,29 @@
{
"_meta": { "locale": "zh-CN", "namespace": "platform", "version": "1.0" },
"platform.health.status_healthy": "Suoyou xitong jun yunxing zhengchang.",
"platform.health.status_degraded": "Bufen fuwu cunzai wenti.",
"platform.health.status_unavailable": "Dangqian pingtai zan bu ke yong.",
"platform.quota.limit_exceeded": "{0} de peie xianzhi yi chaoguo.",
"platform.quota.usage_warning": "Shiyongliang yi daoda peie shangxian de {0}%.",
"platform.quota.reset_at": "Peie jiang zai {0} chongzhi.",
"platform.onboarding.welcome": "Huanying shiyong StellaOps.",
"platform.onboarding.step_authority": "Qing peizhi shenfen tigongfang.",
"platform.onboarding.step_registry": "Qing lianjie rongqi cangku.",
"platform.onboarding.step_environments": "Qing dingyi mubiao huanjing.",
"platform.onboarding.step_complete": "Shezhi yi wancheng. Keyi kaishi.",
"platform.setup.required": "Shiyong pingtai qian bixu xian wancheng chushi shezhi.",
"platform.setup.in_progress": "Shezhi zhengzai jinxing.",
"platform.setup.complete": "Shezhi yi wancheng.",
"platform.context.region_not_found": "Wei zhaodao quyu {0}.",
"platform.context.environment_not_found": "Wei zhaodao huanjing {0}.",
"platform.migration.started": "Qianyi yi qidong.",
"platform.migration.completed": "Qianyi chenggong wancheng.",
"platform.migration.failed": "Qianyi shibai: {0}."
}

View File

@@ -0,0 +1,275 @@
{
"_meta": { "locale": "zh-CN", "namespace": "ui", "version": "1.0" },
"ui.loading.skeleton": "Loading...",
"ui.loading.spinner": "Please wait...",
"ui.loading.slow": "This is taking longer than expected...",
"ui.error.generic": "Something went wrong.",
"ui.error.network": "Network error. Check your connection.",
"ui.error.timeout": "Request timed out. Please try again.",
"ui.error.not_found": "The requested resource was not found.",
"ui.error.unauthorized": "You don't have permission to view this.",
"ui.error.server_error": "Server error. Please try again later.",
"ui.error.try_again": "Try again",
"ui.error.go_back": "Go back",
"ui.offline.banner": "You're offline.",
"ui.offline.description": "Some features may be unavailable.",
"ui.offline.reconnecting": "Reconnecting...",
"ui.offline.reconnected": "Back online.",
"ui.toast.success": "Success",
"ui.toast.info": "Info",
"ui.toast.warning": "Warning",
"ui.toast.error": "Error",
"ui.toast.dismiss": "Dismiss",
"ui.toast.undo": "Undo",
"ui.actions.save": "Save",
"ui.actions.saving": "Saving...",
"ui.actions.saved": "Saved",
"ui.actions.cancel": "Cancel",
"ui.actions.confirm": "Confirm",
"ui.actions.delete": "Delete",
"ui.actions.deleting": "Deleting...",
"ui.actions.deleted": "Deleted",
"ui.actions.submit": "Submit",
"ui.actions.submitting": "Submitting...",
"ui.actions.submitted": "Submitted",
"ui.actions.close": "Close",
"ui.actions.expand": "Expand",
"ui.actions.collapse": "Collapse",
"ui.actions.show_more": "Show more",
"ui.actions.show_less": "Show less",
"ui.actions.retry": "Retry",
"ui.actions.refresh": "Refresh",
"ui.actions.export": "Export",
"ui.actions.search": "Search",
"ui.actions.clear": "Clear",
"ui.actions.view": "View",
"ui.actions.dismiss": "Dismiss",
"ui.actions.show": "Show",
"ui.actions.hide": "Hide",
"ui.actions.sign_in": "Sign in",
"ui.actions.back_to_list": "Back to list",
"ui.actions.load_more": "Load more",
"ui.labels.all": "All",
"ui.labels.title": "Title",
"ui.labels.description": "Description",
"ui.labels.status": "Status",
"ui.labels.score": "Score",
"ui.labels.severity": "Severity",
"ui.labels.details": "Details",
"ui.labels.actions": "Actions",
"ui.labels.type": "Type",
"ui.labels.tags": "Tags",
"ui.labels.filters": "Filters",
"ui.labels.updated": "Updated",
"ui.labels.showing": "Showing",
"ui.labels.of": "of",
"ui.labels.total": "Total",
"ui.labels.not_applicable": "n/a",
"ui.labels.selected": "selected",
"ui.labels.last_updated": "Last updated:",
"ui.labels.expires": "Expires",
"ui.validation.required": "This field is required.",
"ui.validation.invalid": "Invalid value.",
"ui.validation.too_long": "Maximum {max} characters allowed.",
"ui.validation.too_short": "Minimum {min} characters required.",
"ui.validation.invalid_email": "Please enter a valid email address.",
"ui.validation.invalid_url": "Please enter a valid URL.",
"ui.a11y.loading": "Content is loading.",
"ui.a11y.loaded": "Content loaded.",
"ui.a11y.error": "An error occurred.",
"ui.a11y.expanded": "Expanded",
"ui.a11y.collapsed": "Collapsed",
"ui.a11y.selected": "Selected",
"ui.a11y.deselected": "Deselected",
"ui.a11y.required": "Required field",
"ui.a11y.optional": "Optional",
"ui.motion.reduced": "Animations reduced.",
"ui.motion.enabled": "Animations enabled.",
"ui.auth.fresh_active": "Fresh auth: Active",
"ui.auth.fresh_stale": "Fresh auth: Stale",
"ui.locale.label": "Language",
"ui.locale.en_us": "English (US)",
"ui.locale.de_de": "German (Germany)",
"ui.locale.bg_bg": "Bulgarian (Bulgaria)",
"ui.locale.ru_ru": "Russian (Russia)",
"ui.locale.es_es": "Spanish (Spain)",
"ui.locale.fr_fr": "French (France)",
"ui.locale.zh_tw": "Chinese Traditional (Taiwan)",
"ui.locale.zh_cn": "Chinese Simplified (China)",
"ui.locale.uk_ua": "Ukrainian (Ukraine)",
"ui.settings.language.title": "Yuyan",
"ui.settings.language.subtitle": "Shezhi nin shouxuan de kongzhi tai yuyan.",
"ui.settings.language.description": "Genggai hui liji yingyong dao UI.",
"ui.settings.language.selector_label": "Shouxuan yuyan",
"ui.settings.language.persisted": "Yi baocun dao nin de zhanghu bing zai CLI zhong chongyong.",
"ui.settings.language.persisted_error": "Yi ben di baocun, dan zhanghu tongbu shibai.",
"ui.settings.language.sign_in_hint": "Qing denglu yi jiang ci pianhao tongbu dao CLI.",
"ui.first_signal.label": "First signal",
"ui.first_signal.run_prefix": "Run:",
"ui.first_signal.live": "Live",
"ui.first_signal.polling": "Polling",
"ui.first_signal.range_prefix": "Range",
"ui.first_signal.range_separator": "\u2013",
"ui.first_signal.stage_separator": " \u00b7 ",
"ui.first_signal.waiting": "Waiting for first signal\u2026",
"ui.first_signal.not_available": "Signal not available yet.",
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
"ui.first_signal.failed": "Failed to load signal.",
"ui.first_signal.retry": "Retry",
"ui.first_signal.try_again": "Try again",
"ui.first_signal.kind.queued": "Queued",
"ui.first_signal.kind.started": "Started",
"ui.first_signal.kind.phase": "In progress",
"ui.first_signal.kind.blocked": "Blocked",
"ui.first_signal.kind.failed": "Failed",
"ui.first_signal.kind.succeeded": "Succeeded",
"ui.first_signal.kind.canceled": "Canceled",
"ui.first_signal.kind.unavailable": "Unavailable",
"ui.first_signal.kind.unknown": "Signal",
"ui.first_signal.stage.resolve": "Resolving",
"ui.first_signal.stage.fetch": "Fetching",
"ui.first_signal.stage.restore": "Restoring",
"ui.first_signal.stage.analyze": "Analyzing",
"ui.first_signal.stage.policy": "Evaluating policy",
"ui.first_signal.stage.report": "Generating report",
"ui.first_signal.stage.unknown": "Processing",
"ui.first_signal.aria.card_label": "First signal status",
"ui.severity.critical": "Critical",
"ui.severity.high": "High",
"ui.severity.medium": "Medium",
"ui.severity.low": "Low",
"ui.severity.info": "Info",
"ui.severity.none": "None",
"ui.release_orchestrator.title": "Release Orchestrator",
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
"ui.risk_dashboard.title": "Risk Profiles",
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
"ui.risk_dashboard.up_to_date": "Up to date",
"ui.risk_dashboard.last_computation": "Last Computation",
"ui.risk_dashboard.search_placeholder": "Title contains",
"ui.risk_dashboard.evaluated": "Evaluated",
"ui.risk_dashboard.risks_suffix": "risks.",
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
"ui.findings.title": "Findings",
"ui.findings.search_placeholder": "Search findings...",
"ui.findings.clear_filters": "Clear Filters",
"ui.findings.bulk_triage": "Bulk Triage",
"ui.findings.export_all": "Export all findings",
"ui.findings.export_selected": "Export selected findings",
"ui.findings.select_all": "Select all findings",
"ui.findings.trust": "Trust",
"ui.findings.advisory": "Advisory",
"ui.findings.package": "Package",
"ui.findings.flags": "Flags",
"ui.findings.why": "Why",
"ui.findings.select": "Select",
"ui.findings.no_findings": "No findings to display.",
"ui.findings.no_match": "No findings match the current filters.",
"ui.sources_dashboard.title": "Sources Dashboard",
"ui.sources_dashboard.verifying": "Verifying...",
"ui.sources_dashboard.verify_24h": "Verify last 24h",
"ui.sources_dashboard.loading_aoc": "Loading AOC metrics...",
"ui.sources_dashboard.pass_fail_title": "AOC Pass/Fail",
"ui.sources_dashboard.pass_rate": "Pass Rate",
"ui.sources_dashboard.passed": "Passed",
"ui.sources_dashboard.failed": "Failed",
"ui.sources_dashboard.recent_violations": "Recent Violations",
"ui.sources_dashboard.no_violations": "No violations in time window",
"ui.sources_dashboard.throughput_title": "Ingest Throughput",
"ui.sources_dashboard.docs_per_min": "docs/min",
"ui.sources_dashboard.avg_ms": "avg ms",
"ui.sources_dashboard.p95_ms": "p95 ms",
"ui.sources_dashboard.queue": "queue",
"ui.sources_dashboard.errors": "errors",
"ui.sources_dashboard.verification_complete": "Verification Complete",
"ui.sources_dashboard.checked": "Checked:",
"ui.sources_dashboard.violations": "violation(s)",
"ui.sources_dashboard.field": "Field:",
"ui.sources_dashboard.expected": "expected:",
"ui.sources_dashboard.actual": "actual:",
"ui.sources_dashboard.cli_equivalent": "CLI equivalent:",
"ui.sources_dashboard.data_from": "Data from",
"ui.sources_dashboard.to": "to",
"ui.sources_dashboard.hour_window": "h window",
"ui.timeline.title": "Timeline",
"ui.timeline.event_timeline": "Event Timeline",
"ui.timeline.refresh_timeline": "Refresh timeline",
"ui.timeline.loading": "Loading timeline...",
"ui.timeline.empty_state": "Enter a correlation ID to view the event timeline",
"ui.timeline.critical_path": "Critical path analysis",
"ui.timeline.causal_lanes": "Event causal lanes",
"ui.timeline.load_more": "Load more events",
"ui.timeline.event_details": "Event details",
"ui.timeline.events": "events",
"ui.exception_center.title": "Exception Center",
"ui.exception_center.list_view": "List view",
"ui.exception_center.kanban_view": "Kanban view",
"ui.exception_center.new_exception": "+ New Exception",
"ui.exception_center.search_placeholder": "Search exceptions...",
"ui.exception_center.type_vulnerability": "vulnerability",
"ui.exception_center.type_license": "license",
"ui.exception_center.type_policy": "policy",
"ui.exception_center.type_entropy": "entropy",
"ui.exception_center.type_determinism": "determinism",
"ui.exception_center.expiring_soon": "Expiring soon",
"ui.exception_center.clear_filters": "Clear filters",
"ui.exception_center.audit_label": "[A]",
"ui.exception_center.audit_title": "View audit log",
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
"ui.exception_center.column_empty": "No exceptions",
"ui.exception_center.exceptions_suffix": "exceptions",
"ui.evidence_thread.back_to_list": "Back to list",
"ui.evidence_thread.title_default": "Evidence Thread",
"ui.evidence_thread.copy_digest": "Copy full digest",
"ui.evidence_thread.risk_label": "Risk:",
"ui.evidence_thread.nodes": "nodes",
"ui.evidence_thread.loading": "Loading evidence thread...",
"ui.evidence_thread.graph_tab": "Graph",
"ui.evidence_thread.timeline_tab": "Timeline",
"ui.evidence_thread.transcript_tab": "Transcript",
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
"ui.vulnerability_detail.eyebrow": "Vulnerability",
"ui.vulnerability_detail.cvss": "CVSS",
"ui.vulnerability_detail.impact_first": "Impact First",
"ui.vulnerability_detail.epss": "EPSS",
"ui.vulnerability_detail.kev": "KEV",
"ui.vulnerability_detail.kev_listed": "Listed",
"ui.vulnerability_detail.kev_not_listed": "Not listed",
"ui.vulnerability_detail.reachability": "Reachability",
"ui.vulnerability_detail.blast_radius": "Blast Radius",
"ui.vulnerability_detail.assets": "assets",
"ui.vulnerability_detail.binary_resolution": "Binary Resolution",
"ui.vulnerability_detail.evidence_suffix": "evidence",
"ui.vulnerability_detail.fingerprint_note": "This binary was identified as patched using fingerprint analysis, not just version matching.",
"ui.vulnerability_detail.affected_components": "Affected Components",
"ui.vulnerability_detail.fix": "fix",
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
"ui.vulnerability_detail.references": "References",
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
}

View File

@@ -0,0 +1,29 @@
{
"_meta": { "locale": "zh-TW", "namespace": "platform", "version": "1.0" },
"platform.health.status_healthy": "Suoyou xitong jun yunxing zhengchang.",
"platform.health.status_degraded": "Bufen fuwu zhengzai fasheng wenti.",
"platform.health.status_unavailable": "Muqian pingtai zan shi wufa shiyong.",
"platform.quota.limit_exceeded": "{0} de peie xianzhi yi chaochu.",
"platform.quota.usage_warning": "Shiyongliang yi daoda peie shangxian de {0}%.",
"platform.quota.reset_at": "Peie jiang zai {0} chongzhi.",
"platform.onboarding.welcome": "Huanying shiyong StellaOps.",
"platform.onboarding.step_authority": "Qing shezhi shenfen tigongzhe.",
"platform.onboarding.step_registry": "Qing lianjie rongqi cangku.",
"platform.onboarding.step_environments": "Qing dingyi mubiao huanjing.",
"platform.onboarding.step_complete": "Shezhi yi wancheng. Keyi kaishi.",
"platform.setup.required": "Shiyong pingtai qian bixu xian wancheng chushi shezhi.",
"platform.setup.in_progress": "Shezhi jinxing zhong.",
"platform.setup.complete": "Shezhi yi wancheng.",
"platform.context.region_not_found": "Zhao bu dao quyu {0}.",
"platform.context.environment_not_found": "Zhao bu dao huanjing {0}.",
"platform.migration.started": "Qianyi yi kaishi.",
"platform.migration.completed": "Qianyi chenggong wancheng.",
"platform.migration.failed": "Qianyi shi bai: {0}."
}

View File

@@ -0,0 +1,275 @@
{
"_meta": { "locale": "zh-TW", "namespace": "ui", "version": "1.0" },
"ui.loading.skeleton": "Loading...",
"ui.loading.spinner": "Please wait...",
"ui.loading.slow": "This is taking longer than expected...",
"ui.error.generic": "Something went wrong.",
"ui.error.network": "Network error. Check your connection.",
"ui.error.timeout": "Request timed out. Please try again.",
"ui.error.not_found": "The requested resource was not found.",
"ui.error.unauthorized": "You don't have permission to view this.",
"ui.error.server_error": "Server error. Please try again later.",
"ui.error.try_again": "Try again",
"ui.error.go_back": "Go back",
"ui.offline.banner": "You're offline.",
"ui.offline.description": "Some features may be unavailable.",
"ui.offline.reconnecting": "Reconnecting...",
"ui.offline.reconnected": "Back online.",
"ui.toast.success": "Success",
"ui.toast.info": "Info",
"ui.toast.warning": "Warning",
"ui.toast.error": "Error",
"ui.toast.dismiss": "Dismiss",
"ui.toast.undo": "Undo",
"ui.actions.save": "Save",
"ui.actions.saving": "Saving...",
"ui.actions.saved": "Saved",
"ui.actions.cancel": "Cancel",
"ui.actions.confirm": "Confirm",
"ui.actions.delete": "Delete",
"ui.actions.deleting": "Deleting...",
"ui.actions.deleted": "Deleted",
"ui.actions.submit": "Submit",
"ui.actions.submitting": "Submitting...",
"ui.actions.submitted": "Submitted",
"ui.actions.close": "Close",
"ui.actions.expand": "Expand",
"ui.actions.collapse": "Collapse",
"ui.actions.show_more": "Show more",
"ui.actions.show_less": "Show less",
"ui.actions.retry": "Retry",
"ui.actions.refresh": "Refresh",
"ui.actions.export": "Export",
"ui.actions.search": "Search",
"ui.actions.clear": "Clear",
"ui.actions.view": "View",
"ui.actions.dismiss": "Dismiss",
"ui.actions.show": "Show",
"ui.actions.hide": "Hide",
"ui.actions.sign_in": "Sign in",
"ui.actions.back_to_list": "Back to list",
"ui.actions.load_more": "Load more",
"ui.labels.all": "All",
"ui.labels.title": "Title",
"ui.labels.description": "Description",
"ui.labels.status": "Status",
"ui.labels.score": "Score",
"ui.labels.severity": "Severity",
"ui.labels.details": "Details",
"ui.labels.actions": "Actions",
"ui.labels.type": "Type",
"ui.labels.tags": "Tags",
"ui.labels.filters": "Filters",
"ui.labels.updated": "Updated",
"ui.labels.showing": "Showing",
"ui.labels.of": "of",
"ui.labels.total": "Total",
"ui.labels.not_applicable": "n/a",
"ui.labels.selected": "selected",
"ui.labels.last_updated": "Last updated:",
"ui.labels.expires": "Expires",
"ui.validation.required": "This field is required.",
"ui.validation.invalid": "Invalid value.",
"ui.validation.too_long": "Maximum {max} characters allowed.",
"ui.validation.too_short": "Minimum {min} characters required.",
"ui.validation.invalid_email": "Please enter a valid email address.",
"ui.validation.invalid_url": "Please enter a valid URL.",
"ui.a11y.loading": "Content is loading.",
"ui.a11y.loaded": "Content loaded.",
"ui.a11y.error": "An error occurred.",
"ui.a11y.expanded": "Expanded",
"ui.a11y.collapsed": "Collapsed",
"ui.a11y.selected": "Selected",
"ui.a11y.deselected": "Deselected",
"ui.a11y.required": "Required field",
"ui.a11y.optional": "Optional",
"ui.motion.reduced": "Animations reduced.",
"ui.motion.enabled": "Animations enabled.",
"ui.auth.fresh_active": "Fresh auth: Active",
"ui.auth.fresh_stale": "Fresh auth: Stale",
"ui.locale.label": "Language",
"ui.locale.en_us": "English (US)",
"ui.locale.de_de": "German (Germany)",
"ui.locale.bg_bg": "Bulgarian (Bulgaria)",
"ui.locale.ru_ru": "Russian (Russia)",
"ui.locale.es_es": "Spanish (Spain)",
"ui.locale.fr_fr": "French (France)",
"ui.locale.zh_tw": "Chinese Traditional (Taiwan)",
"ui.locale.zh_cn": "Chinese Simplified (China)",
"ui.locale.uk_ua": "Ukrainian (Ukraine)",
"ui.settings.language.title": "Yuyan",
"ui.settings.language.subtitle": "Shezhi nin pianhao de kongzhi tai yuyan.",
"ui.settings.language.description": "Biangeng hui liji shengxiao zai UI.",
"ui.settings.language.selector_label": "Pianhao yuyan",
"ui.settings.language.persisted": "Yijing baocun dao zhanghu bing gong CLI chongyong.",
"ui.settings.language.persisted_error": "Yijing benji baocun, dan zhanghu tongbu shibai.",
"ui.settings.language.sign_in_hint": "Qing dengru yi jiang ci pianhao tongbu dao CLI.",
"ui.first_signal.label": "First signal",
"ui.first_signal.run_prefix": "Run:",
"ui.first_signal.live": "Live",
"ui.first_signal.polling": "Polling",
"ui.first_signal.range_prefix": "Range",
"ui.first_signal.range_separator": "\u2013",
"ui.first_signal.stage_separator": " \u00b7 ",
"ui.first_signal.waiting": "Waiting for first signal\u2026",
"ui.first_signal.not_available": "Signal not available yet.",
"ui.first_signal.offline": "Offline. Last known signal may be stale.",
"ui.first_signal.failed": "Failed to load signal.",
"ui.first_signal.retry": "Retry",
"ui.first_signal.try_again": "Try again",
"ui.first_signal.kind.queued": "Queued",
"ui.first_signal.kind.started": "Started",
"ui.first_signal.kind.phase": "In progress",
"ui.first_signal.kind.blocked": "Blocked",
"ui.first_signal.kind.failed": "Failed",
"ui.first_signal.kind.succeeded": "Succeeded",
"ui.first_signal.kind.canceled": "Canceled",
"ui.first_signal.kind.unavailable": "Unavailable",
"ui.first_signal.kind.unknown": "Signal",
"ui.first_signal.stage.resolve": "Resolving",
"ui.first_signal.stage.fetch": "Fetching",
"ui.first_signal.stage.restore": "Restoring",
"ui.first_signal.stage.analyze": "Analyzing",
"ui.first_signal.stage.policy": "Evaluating policy",
"ui.first_signal.stage.report": "Generating report",
"ui.first_signal.stage.unknown": "Processing",
"ui.first_signal.aria.card_label": "First signal status",
"ui.severity.critical": "Critical",
"ui.severity.high": "High",
"ui.severity.medium": "Medium",
"ui.severity.low": "Low",
"ui.severity.info": "Info",
"ui.severity.none": "None",
"ui.release_orchestrator.title": "Release Orchestrator",
"ui.release_orchestrator.subtitle": "Pipeline overview and release management",
"ui.release_orchestrator.pipeline_runs": "Pipeline Runs",
"ui.release_orchestrator.refresh_dashboard": "Refresh dashboard",
"ui.risk_dashboard.eyebrow": "Gateway \u00b7 Risk",
"ui.risk_dashboard.title": "Risk Profiles",
"ui.risk_dashboard.subtitle": "Tenant-scoped risk posture with deterministic ordering.",
"ui.risk_dashboard.up_to_date": "Up to date",
"ui.risk_dashboard.last_computation": "Last Computation",
"ui.risk_dashboard.search_placeholder": "Title contains",
"ui.risk_dashboard.evaluated": "Evaluated",
"ui.risk_dashboard.risks_suffix": "risks.",
"ui.risk_dashboard.error_unable_to_load": "Unable to load risk profiles.",
"ui.risk_dashboard.no_risks_found": "No risks found for current filters.",
"ui.risk_dashboard.loading_risks": "Loading risks\u2026",
"ui.findings.title": "Findings",
"ui.findings.search_placeholder": "Search findings...",
"ui.findings.clear_filters": "Clear Filters",
"ui.findings.bulk_triage": "Bulk Triage",
"ui.findings.export_all": "Export all findings",
"ui.findings.export_selected": "Export selected findings",
"ui.findings.select_all": "Select all findings",
"ui.findings.trust": "Trust",
"ui.findings.advisory": "Advisory",
"ui.findings.package": "Package",
"ui.findings.flags": "Flags",
"ui.findings.why": "Why",
"ui.findings.select": "Select",
"ui.findings.no_findings": "No findings to display.",
"ui.findings.no_match": "No findings match the current filters.",
"ui.sources_dashboard.title": "Sources Dashboard",
"ui.sources_dashboard.verifying": "Verifying...",
"ui.sources_dashboard.verify_24h": "Verify last 24h",
"ui.sources_dashboard.loading_aoc": "Loading AOC metrics...",
"ui.sources_dashboard.pass_fail_title": "AOC Pass/Fail",
"ui.sources_dashboard.pass_rate": "Pass Rate",
"ui.sources_dashboard.passed": "Passed",
"ui.sources_dashboard.failed": "Failed",
"ui.sources_dashboard.recent_violations": "Recent Violations",
"ui.sources_dashboard.no_violations": "No violations in time window",
"ui.sources_dashboard.throughput_title": "Ingest Throughput",
"ui.sources_dashboard.docs_per_min": "docs/min",
"ui.sources_dashboard.avg_ms": "avg ms",
"ui.sources_dashboard.p95_ms": "p95 ms",
"ui.sources_dashboard.queue": "queue",
"ui.sources_dashboard.errors": "errors",
"ui.sources_dashboard.verification_complete": "Verification Complete",
"ui.sources_dashboard.checked": "Checked:",
"ui.sources_dashboard.violations": "violation(s)",
"ui.sources_dashboard.field": "Field:",
"ui.sources_dashboard.expected": "expected:",
"ui.sources_dashboard.actual": "actual:",
"ui.sources_dashboard.cli_equivalent": "CLI equivalent:",
"ui.sources_dashboard.data_from": "Data from",
"ui.sources_dashboard.to": "to",
"ui.sources_dashboard.hour_window": "h window",
"ui.timeline.title": "Timeline",
"ui.timeline.event_timeline": "Event Timeline",
"ui.timeline.refresh_timeline": "Refresh timeline",
"ui.timeline.loading": "Loading timeline...",
"ui.timeline.empty_state": "Enter a correlation ID to view the event timeline",
"ui.timeline.critical_path": "Critical path analysis",
"ui.timeline.causal_lanes": "Event causal lanes",
"ui.timeline.load_more": "Load more events",
"ui.timeline.event_details": "Event details",
"ui.timeline.events": "events",
"ui.exception_center.title": "Exception Center",
"ui.exception_center.list_view": "List view",
"ui.exception_center.kanban_view": "Kanban view",
"ui.exception_center.new_exception": "+ New Exception",
"ui.exception_center.search_placeholder": "Search exceptions...",
"ui.exception_center.type_vulnerability": "vulnerability",
"ui.exception_center.type_license": "license",
"ui.exception_center.type_policy": "policy",
"ui.exception_center.type_entropy": "entropy",
"ui.exception_center.type_determinism": "determinism",
"ui.exception_center.expiring_soon": "Expiring soon",
"ui.exception_center.clear_filters": "Clear filters",
"ui.exception_center.audit_label": "[A]",
"ui.exception_center.audit_title": "View audit log",
"ui.exception_center.no_exceptions": "No exceptions match the current filters",
"ui.exception_center.column_empty": "No exceptions",
"ui.exception_center.exceptions_suffix": "exceptions",
"ui.evidence_thread.back_to_list": "Back to list",
"ui.evidence_thread.title_default": "Evidence Thread",
"ui.evidence_thread.copy_digest": "Copy full digest",
"ui.evidence_thread.risk_label": "Risk:",
"ui.evidence_thread.nodes": "nodes",
"ui.evidence_thread.loading": "Loading evidence thread...",
"ui.evidence_thread.graph_tab": "Graph",
"ui.evidence_thread.timeline_tab": "Timeline",
"ui.evidence_thread.transcript_tab": "Transcript",
"ui.evidence_thread.not_found": "No evidence thread found for this artifact.",
"ui.vulnerability_detail.eyebrow": "Vulnerability",
"ui.vulnerability_detail.cvss": "CVSS",
"ui.vulnerability_detail.impact_first": "Impact First",
"ui.vulnerability_detail.epss": "EPSS",
"ui.vulnerability_detail.kev": "KEV",
"ui.vulnerability_detail.kev_listed": "Listed",
"ui.vulnerability_detail.kev_not_listed": "Not listed",
"ui.vulnerability_detail.reachability": "Reachability",
"ui.vulnerability_detail.blast_radius": "Blast Radius",
"ui.vulnerability_detail.assets": "assets",
"ui.vulnerability_detail.binary_resolution": "Binary Resolution",
"ui.vulnerability_detail.evidence_suffix": "evidence",
"ui.vulnerability_detail.fingerprint_note": "This binary was identified as patched using fingerprint analysis, not just version matching.",
"ui.vulnerability_detail.affected_components": "Affected Components",
"ui.vulnerability_detail.fix": "fix",
"ui.vulnerability_detail.evidence_tree": "Evidence Tree and Citation Links",
"ui.vulnerability_detail.evidence_explorer": "evidence explorer",
"ui.vulnerability_detail.references": "References",
"ui.vulnerability_detail.back_to_risk": "Back to Risk"
}

View File

@@ -23,6 +23,10 @@ public partial class PlatformDbContext : DbContext
public virtual DbSet<UiContextPreference> UiContextPreferences { get; set; }
public virtual DbSet<PlatformTranslation> Translations { get; set; }
public virtual DbSet<IdentityProviderConfig> IdentityProviderConfigs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var schemaName = _schemaName;
@@ -111,6 +115,88 @@ public partial class PlatformDbContext : DbContext
.HasColumnName("updated_by");
});
modelBuilder.Entity<PlatformTranslation>(entity =>
{
entity.HasKey(e => e.Id).HasName("translations_pkey");
entity.ToTable("translations", schemaName);
entity.HasIndex(e => new { e.TenantId, e.Locale, e.Key }, "ux_translations_tenant_locale_key")
.IsUnique();
entity.HasIndex(e => new { e.TenantId, e.Locale }, "ix_translations_tenant_locale");
entity.Property(e => e.Id)
.HasColumnName("id")
.UseIdentityAlwaysColumn();
entity.Property(e => e.Locale)
.HasMaxLength(10)
.HasColumnName("locale");
entity.Property(e => e.Key)
.HasMaxLength(512)
.HasColumnName("key");
entity.Property(e => e.Value)
.HasColumnName("value");
entity.Property(e => e.TenantId)
.HasMaxLength(128)
.HasDefaultValue("_system")
.HasColumnName("tenant_id");
entity.Property(e => e.UpdatedBy)
.HasMaxLength(256)
.HasColumnName("updated_by");
entity.Property(e => e.UpdatedAt)
.HasDefaultValueSql("now()")
.HasColumnName("updated_at");
});
modelBuilder.Entity<IdentityProviderConfig>(entity =>
{
entity.HasKey(e => e.Id).HasName("identity_provider_configs_pkey");
entity.ToTable("identity_provider_configs", schemaName);
entity.HasIndex(e => new { e.TenantId, e.Name }, "ux_idp_configs_tenant_name")
.IsUnique();
entity.HasIndex(e => new { e.TenantId, e.Type }, "ix_idp_configs_tenant_type");
entity.Property(e => e.Id)
.HasColumnName("id")
.HasDefaultValueSql("gen_random_uuid()");
entity.Property(e => e.TenantId)
.HasMaxLength(128)
.HasColumnName("tenant_id");
entity.Property(e => e.Name)
.HasMaxLength(256)
.HasColumnName("name");
entity.Property(e => e.Type)
.HasMaxLength(50)
.HasColumnName("type");
entity.Property(e => e.Enabled)
.HasDefaultValue(true)
.HasColumnName("enabled");
entity.Property(e => e.ConfigurationJson)
.HasColumnType("jsonb")
.HasColumnName("configuration_json");
entity.Property(e => e.Description)
.HasMaxLength(1024)
.HasColumnName("description");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("now()")
.HasColumnName("created_at");
entity.Property(e => e.UpdatedAt)
.HasDefaultValueSql("now()")
.HasColumnName("updated_at");
entity.Property(e => e.CreatedBy)
.HasMaxLength(256)
.HasDefaultValueSql("'system'")
.HasColumnName("created_by");
entity.Property(e => e.UpdatedBy)
.HasMaxLength(256)
.HasDefaultValueSql("'system'")
.HasColumnName("updated_by");
});
OnModelCreatingPartial(modelBuilder);
}

View File

@@ -0,0 +1,28 @@
using System;
namespace StellaOps.Platform.Database.EfCore.Models;
public partial class IdentityProviderConfig
{
public Guid Id { get; set; }
public string TenantId { get; set; } = null!;
public string Name { get; set; } = null!;
public string Type { get; set; } = null!;
public bool Enabled { get; set; }
public string ConfigurationJson { get; set; } = null!;
public string? Description { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public string CreatedBy { get; set; } = null!;
public string UpdatedBy { get; set; } = null!;
}

View File

@@ -0,0 +1,20 @@
using System;
namespace StellaOps.Platform.Database.EfCore.Models;
public partial class PlatformTranslation
{
public long Id { get; set; }
public string Locale { get; set; } = null!;
public string Key { get; set; } = null!;
public string Value { get; set; } = null!;
public string TenantId { get; set; } = "_system";
public string UpdatedBy { get; set; } = null!;
public DateTime UpdatedAt { get; set; }
}

View File

@@ -0,0 +1,18 @@
-- SPRINT_20260224_001 / LOC-002
-- Platform localization overrides table used by /platform/i18n and /api/v1/platform/localization/*.
CREATE SCHEMA IF NOT EXISTS platform;
CREATE TABLE IF NOT EXISTS platform.translations (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
locale VARCHAR(10) NOT NULL,
key VARCHAR(512) NOT NULL,
value TEXT NOT NULL,
tenant_id VARCHAR(128) NOT NULL DEFAULT '_system',
updated_by VARCHAR(256) NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT ux_translations_tenant_locale_key UNIQUE (tenant_id, locale, key)
);
CREATE INDEX IF NOT EXISTS ix_translations_tenant_locale
ON platform.translations (tenant_id, locale);

View File

@@ -0,0 +1,347 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http.Json;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class IdentityProviderEndpointsTests : IClassFixture<PlatformWebApplicationFactory>
{
private readonly PlatformWebApplicationFactory factory;
public IdentityProviderEndpointsTests(PlatformWebApplicationFactory factory)
{
this.factory = factory;
}
private HttpClient CreateClient(string tenantId = "tenant-idp", string actorId = "actor-idp")
{
var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId);
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", actorId);
return client;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task List_ReturnsEmptyForNewTenant()
{
using var client = CreateClient("tenant-idp-empty");
var items = await client.GetFromJsonAsync<List<IdentityProviderConfigDto>>(
"/api/v1/platform/identity-providers",
TestContext.Current.CancellationToken);
Assert.NotNull(items);
Assert.Empty(items!);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CrudLifecycle_LdapProvider()
{
using var client = CreateClient("tenant-idp-crud");
// Create
var createRequest = new CreateIdentityProviderRequest(
"test-ldap",
"ldap",
true,
new Dictionary<string, string?>
{
["host"] = "ldap.example.com",
["port"] = "389",
["bindDn"] = "cn=admin,dc=example,dc=com",
["bindPassword"] = "secret",
["searchBase"] = "dc=example,dc=com"
},
"Test LDAP provider");
var createResponse = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
createRequest,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
var created = await createResponse.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(created);
Assert.Equal("test-ldap", created!.Name);
Assert.Equal("ldap", created.Type);
Assert.True(created.Enabled);
Assert.Equal("ldap.example.com", created.Configuration["host"]);
// Read
var getResponse = await client.GetFromJsonAsync<IdentityProviderConfigDto>(
$"/api/v1/platform/identity-providers/{created.Id}",
TestContext.Current.CancellationToken);
Assert.NotNull(getResponse);
Assert.Equal(created.Id, getResponse!.Id);
// Update
var updateRequest = new UpdateIdentityProviderRequest(
null,
new Dictionary<string, string?>
{
["host"] = "ldap2.example.com",
["port"] = "636",
["bindDn"] = "cn=admin,dc=example,dc=com",
["bindPassword"] = "new-secret",
["searchBase"] = "dc=example,dc=com"
},
"Updated LDAP");
var updateResponse = await client.PutAsJsonAsync(
$"/api/v1/platform/identity-providers/{created.Id}",
updateRequest,
TestContext.Current.CancellationToken);
updateResponse.EnsureSuccessStatusCode();
var updated = await updateResponse.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal("ldap2.example.com", updated!.Configuration["host"]);
Assert.Equal("Updated LDAP", updated.Description);
// List
var items = await client.GetFromJsonAsync<List<IdentityProviderConfigDto>>(
"/api/v1/platform/identity-providers",
TestContext.Current.CancellationToken);
Assert.Single(items!);
// Delete
var deleteResponse = await client.DeleteAsync(
$"/api/v1/platform/identity-providers/{created.Id}",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
// Verify deleted
var afterDelete = await client.GetAsync(
$"/api/v1/platform/identity-providers/{created.Id}",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, afterDelete.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Create_ValidationError_MissingRequiredFields()
{
using var client = CreateClient("tenant-idp-validation");
var request = new CreateIdentityProviderRequest(
"invalid-ldap",
"ldap",
true,
new Dictionary<string, string?>
{
["host"] = "ldap.example.com"
// Missing port, bindDn, bindPassword, searchBase
},
null);
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
request,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Create_ValidationError_InvalidType()
{
using var client = CreateClient("tenant-idp-type");
var request = new CreateIdentityProviderRequest(
"invalid-type",
"kerberos",
true,
new Dictionary<string, string?>(),
null);
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
request,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Create_DuplicateName_ReturnsBadRequest()
{
using var client = CreateClient("tenant-idp-dup");
var request = new CreateIdentityProviderRequest(
"duplicate-provider",
"standard",
true,
new Dictionary<string, string?>(),
null);
var first = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
request,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, first.StatusCode);
var second = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
request,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, second.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EnableDisable_TogglesState()
{
using var client = CreateClient("tenant-idp-toggle");
var createRequest = new CreateIdentityProviderRequest(
"toggle-provider",
"standard",
true,
new Dictionary<string, string?>(),
null);
var createResponse = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
createRequest,
TestContext.Current.CancellationToken);
var created = await createResponse.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
// Disable
var disableResponse = await client.PostAsync(
$"/api/v1/platform/identity-providers/{created!.Id}/disable",
null,
TestContext.Current.CancellationToken);
disableResponse.EnsureSuccessStatusCode();
var disabled = await disableResponse.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.False(disabled!.Enabled);
// Enable
var enableResponse = await client.PostAsync(
$"/api/v1/platform/identity-providers/{created.Id}/enable",
null,
TestContext.Current.CancellationToken);
enableResponse.EnsureSuccessStatusCode();
var enabled = await enableResponse.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.True(enabled!.Enabled);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TestConnection_StandardProvider_AlwaysSucceeds()
{
using var client = CreateClient("tenant-idp-test");
var request = new TestConnectionRequest(
"standard",
new Dictionary<string, string?>());
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers/test-connection",
request,
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TestConnectionResult>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.True(result!.Success);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetTypes_ReturnsAllProviderTypes()
{
using var client = CreateClient("tenant-idp-types");
var types = await client.GetFromJsonAsync<List<IdentityProviderTypeSchema>>(
"/api/v1/platform/identity-providers/types",
TestContext.Current.CancellationToken);
Assert.NotNull(types);
Assert.Equal(4, types!.Count);
Assert.Contains(types, t => t.Type == "standard");
Assert.Contains(types, t => t.Type == "ldap");
Assert.Contains(types, t => t.Type == "saml");
Assert.Contains(types, t => t.Type == "oidc");
var ldap = types.Find(t => t.Type == "ldap");
Assert.NotNull(ldap);
Assert.Contains(ldap!.RequiredFields, f => f.Name == "host");
Assert.Contains(ldap.RequiredFields, f => f.Name == "bindDn");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TenantIsolation_CannotSeeOtherTenantProviders()
{
using var clientA = CreateClient("tenant-idp-a", "actor-a");
using var clientB = CreateClient("tenant-idp-b", "actor-b");
var requestA = new CreateIdentityProviderRequest(
"tenant-a-provider",
"standard",
true,
new Dictionary<string, string?>(),
null);
var createA = await clientA.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
requestA,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, createA.StatusCode);
var created = await createA.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
// Tenant B cannot see tenant A's provider
var listB = await clientB.GetFromJsonAsync<List<IdentityProviderConfigDto>>(
"/api/v1/platform/identity-providers",
TestContext.Current.CancellationToken);
Assert.Empty(listB!);
// Tenant B cannot get tenant A's provider by ID
var getB = await clientB.GetAsync(
$"/api/v1/platform/identity-providers/{created!.Id}",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, getB.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Health_ReturnsStatusForProvider()
{
using var client = CreateClient("tenant-idp-health");
var createRequest = new CreateIdentityProviderRequest(
"health-check-provider",
"standard",
true,
new Dictionary<string, string?>(),
null);
var createResponse = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
createRequest,
TestContext.Current.CancellationToken);
var created = await createResponse.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
var healthResponse = await client.GetFromJsonAsync<TestConnectionResult>(
$"/api/v1/platform/identity-providers/{created!.Id}/health",
TestContext.Current.CancellationToken);
Assert.NotNull(healthResponse);
Assert.True(healthResponse!.Success);
}
}

View File

@@ -0,0 +1,259 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Platform.WebService.Tests.Integration;
/// <summary>
/// Integration tests that require real IDP containers (OpenLDAP + Keycloak).
/// Run: docker compose -f devops/compose/docker-compose.idp-testing.yml --profile idp up -d
/// Execute: dotnet test --filter "FullyQualifiedName~IdentityProviderContainerTests"
/// </summary>
[Trait("Category", TestCategories.Integration)]
[Collection("IdpContainerTests")]
public sealed class IdentityProviderContainerTests : IClassFixture<PlatformWebApplicationFactory>
{
private const string LdapHost = "localhost";
private const int LdapPort = 3389;
private const string KeycloakBaseUrl = "http://localhost:8280";
private readonly PlatformWebApplicationFactory factory;
public IdentityProviderContainerTests(PlatformWebApplicationFactory factory)
{
this.factory = factory;
}
private HttpClient CreateClient()
{
var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-container-test");
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "actor-container-test");
return client;
}
[Fact(Skip = "Requires docker compose idp containers")]
public async Task TestConnection_Ldap_CorrectCredentials_Succeeds()
{
using var client = CreateClient();
var request = new TestConnectionRequest(
"ldap",
new Dictionary<string, string?>
{
["host"] = LdapHost,
["port"] = LdapPort.ToString(),
["bindDn"] = "cn=admin,dc=stellaops,dc=test",
["bindPassword"] = "admin-secret",
["searchBase"] = "dc=stellaops,dc=test"
});
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers/test-connection",
request,
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TestConnectionResult>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.True(result!.Success);
Assert.NotNull(result.LatencyMs);
Assert.True(result.LatencyMs > 0);
}
[Fact(Skip = "Requires docker compose idp containers")]
public async Task TestConnection_Ldap_WrongCredentials_Fails()
{
using var client = CreateClient();
// TCP connect will succeed but bind would fail
// (our current test only does TCP connect, so this tests unreachable host)
var request = new TestConnectionRequest(
"ldap",
new Dictionary<string, string?>
{
["host"] = "198.51.100.1", // RFC 5737 TEST-NET-2
["port"] = "389",
["bindDn"] = "cn=wrong,dc=stellaops,dc=test",
["bindPassword"] = "wrong-password",
["searchBase"] = "dc=stellaops,dc=test"
});
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers/test-connection",
request,
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TestConnectionResult>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.False(result!.Success);
}
[Fact(Skip = "Requires docker compose idp containers")]
public async Task TestConnection_SamlMetadata_Succeeds()
{
using var client = CreateClient();
var metadataUrl = $"{KeycloakBaseUrl}/realms/stellaops/protocol/saml/descriptor";
var request = new TestConnectionRequest(
"saml",
new Dictionary<string, string?>
{
["spEntityId"] = "stellaops-saml-sp",
["idpEntityId"] = $"{KeycloakBaseUrl}/realms/stellaops",
["idpMetadataUrl"] = metadataUrl
});
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers/test-connection",
request,
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TestConnectionResult>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.True(result!.Success);
Assert.Contains("metadata", result.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact(Skip = "Requires docker compose idp containers")]
public async Task TestConnection_OidcDiscovery_Succeeds()
{
using var client = CreateClient();
var request = new TestConnectionRequest(
"oidc",
new Dictionary<string, string?>
{
["authority"] = $"{KeycloakBaseUrl}/realms/stellaops",
["clientId"] = "stellaops-oidc-client",
["clientSecret"] = "stellaops-oidc-test-secret"
});
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers/test-connection",
request,
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TestConnectionResult>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.True(result!.Success);
Assert.Contains("discovery", result.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact(Skip = "Requires docker compose idp containers")]
public async Task TestConnection_UnreachableHost_TimesOut()
{
using var client = CreateClient();
var request = new TestConnectionRequest(
"ldap",
new Dictionary<string, string?>
{
["host"] = "198.51.100.1", // TEST-NET-2 -- should be unreachable
["port"] = "389",
["bindDn"] = "cn=admin,dc=test",
["bindPassword"] = "secret",
["searchBase"] = "dc=test"
});
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers/test-connection",
request,
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TestConnectionResult>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.False(result!.Success);
Assert.Contains("failed", result.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact(Skip = "Requires docker compose idp containers")]
public async Task FullCrudLifecycle_WithHealthCheck()
{
using var client = CreateClient();
// Create LDAP provider
var createRequest = new CreateIdentityProviderRequest(
"container-test-ldap",
"ldap",
true,
new Dictionary<string, string?>
{
["host"] = LdapHost,
["port"] = LdapPort.ToString(),
["bindDn"] = "cn=admin,dc=stellaops,dc=test",
["bindPassword"] = "admin-secret",
["searchBase"] = "dc=stellaops,dc=test"
},
"Container integration test LDAP provider");
var createResponse = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
createRequest,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
var created = await createResponse.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(created);
// Health check
var healthResponse = await client.GetFromJsonAsync<TestConnectionResult>(
$"/api/v1/platform/identity-providers/{created!.Id}/health",
TestContext.Current.CancellationToken);
Assert.NotNull(healthResponse);
Assert.True(healthResponse!.Success);
// Update
var updateRequest = new UpdateIdentityProviderRequest(
null,
new Dictionary<string, string?>
{
["host"] = LdapHost,
["port"] = LdapPort.ToString(),
["bindDn"] = "cn=admin,dc=stellaops,dc=test",
["bindPassword"] = "admin-secret",
["searchBase"] = "ou=users,dc=stellaops,dc=test"
},
"Updated container test LDAP provider");
var updateResponse = await client.PutAsJsonAsync(
$"/api/v1/platform/identity-providers/{created.Id}",
updateRequest,
TestContext.Current.CancellationToken);
updateResponse.EnsureSuccessStatusCode();
// List
var list = await client.GetFromJsonAsync<List<IdentityProviderConfigDto>>(
"/api/v1/platform/identity-providers",
TestContext.Current.CancellationToken);
Assert.Contains(list!, p => p.Name == "container-test-ldap");
// Delete
var deleteResponse = await client.DeleteAsync(
$"/api/v1/platform/identity-providers/{created.Id}",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
}
}

View File

@@ -0,0 +1,185 @@
using System.Net.Http.Json;
using System.Text.Json.Nodes;
using StellaOps.TestKit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class LocalizationEndpointsTests : IClassFixture<PlatformWebApplicationFactory>
{
private readonly PlatformWebApplicationFactory _factory;
public LocalizationEndpointsTests(PlatformWebApplicationFactory factory)
{
_factory = factory;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task UiBundleEndpoint_ReturnsDefaultMergedBundle()
{
using var client = _factory.CreateClient();
using var response = await client.GetAsync(
"/platform/i18n/en-US.json",
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
Assert.Equal("public, max-age=300", response.Headers.CacheControl?.ToString());
var bundle = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(bundle);
Assert.True(bundle!.ContainsKey("common.actions.save"));
Assert.True(bundle.ContainsKey("ui.actions.save"));
Assert.False(string.IsNullOrWhiteSpace(bundle["common.actions.save"]));
Assert.False(string.IsNullOrWhiteSpace(bundle["ui.actions.save"]));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task UpsertOverride_IsReturnedFromUiBundle()
{
using var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-localization");
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-localization");
client.DefaultRequestHeaders.Add("X-Actor", "test-actor");
var upsertPayload = new
{
locale = "en-US",
strings = new Dictionary<string, string>
{
["ui.actions.save"] = "Speichern"
}
};
using var upsertResponse = await client.PutAsJsonAsync(
"/api/v1/platform/localization/bundles",
upsertPayload,
TestContext.Current.CancellationToken);
upsertResponse.EnsureSuccessStatusCode();
using var bundleResponse = await client.GetAsync(
"/platform/i18n/en-US.json",
TestContext.Current.CancellationToken);
bundleResponse.EnsureSuccessStatusCode();
var bundle = await bundleResponse.Content.ReadFromJsonAsync<Dictionary<string, string>>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(bundle);
Assert.Equal("Speichern", bundle!["ui.actions.save"]);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task UiBundles_IncludeCommonLayerForAllSupportedLocales()
{
using var client = _factory.CreateClient();
var locales = new[]
{
"en-US",
"de-DE",
"bg-BG",
"ru-RU",
"es-ES",
"fr-FR",
"uk-UA",
"zh-TW",
"zh-CN",
};
foreach (var locale in locales)
{
using var response = await client.GetAsync(
$"/platform/i18n/{locale}.json",
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var bundle = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(bundle);
Assert.Contains("common.actions.save", bundle!);
Assert.Contains("ui.actions.save", bundle);
}
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AvailableLocales_IncludesExpandedLocaleSet()
{
using var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-localization");
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-localization");
client.DefaultRequestHeaders.Add("X-Actor", "test-actor");
using var response = await client.GetAsync(
"/api/v1/platform/localization/locales",
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<JsonObject>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(payload);
var locales = payload!["locales"]?.AsArray().Select(node => node?.GetValue<string>()).ToHashSet(StringComparer.OrdinalIgnoreCase);
Assert.NotNull(locales);
Assert.Contains("en-US", locales!);
Assert.Contains("de-DE", locales);
Assert.Contains("bg-BG", locales);
Assert.Contains("ru-RU", locales);
Assert.Contains("es-ES", locales);
Assert.Contains("fr-FR", locales);
Assert.Contains("uk-UA", locales);
Assert.Contains("zh-TW", locales);
Assert.Contains("zh-CN", locales);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PlatformNamespaceBundles_AreAvailableForAllSupportedLocales()
{
using var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-localization");
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-localization");
client.DefaultRequestHeaders.Add("X-Actor", "test-actor");
var locales = new[]
{
"en-US",
"de-DE",
"bg-BG",
"ru-RU",
"es-ES",
"fr-FR",
"uk-UA",
"zh-TW",
"zh-CN",
};
foreach (var locale in locales)
{
using var response = await client.GetAsync(
$"/api/v1/platform/localization/bundles/{locale}/platform",
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<JsonObject>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(payload);
var strings = payload!["strings"]?.AsObject();
Assert.NotNull(strings);
Assert.Contains("platform.health.status_healthy", strings!);
Assert.Contains("platform.migration.failed", strings);
}
}
}

View File

@@ -0,0 +1,67 @@
using StellaOps.TestKit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class PlatformTranslationsMigrationScriptTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration057_DefinesPlatformTranslationsSchemaObjects()
{
var scriptPath = GetMigrationPath("057_PlatformTranslations.sql");
var sql = File.ReadAllText(scriptPath);
Assert.Contains("CREATE TABLE IF NOT EXISTS platform.translations", sql, StringComparison.Ordinal);
Assert.Contains("CONSTRAINT ux_translations_tenant_locale_key UNIQUE (tenant_id, locale, key)", sql, StringComparison.Ordinal);
Assert.Contains("CREATE INDEX IF NOT EXISTS ix_translations_tenant_locale", sql, StringComparison.Ordinal);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration057_IsPresentInReleaseMigrationSequence()
{
var migrationsDir = GetMigrationsDirectory();
var migrationNames = Directory.GetFiles(migrationsDir, "*.sql")
.Select(Path.GetFileName)
.Where(static name => name is not null)
.Select(static name => name!)
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
var index056 = Array.IndexOf(migrationNames, "056_RunCapsuleReplayLinkage.sql");
var index057 = Array.IndexOf(migrationNames, "057_PlatformTranslations.sql");
Assert.True(index056 >= 0, "Expected migration 056 to exist.");
Assert.True(index057 > index056, "Expected migration 057 to appear after migration 056.");
}
private static string GetMigrationPath(string fileName)
{
return Path.Combine(GetMigrationsDirectory(), fileName);
}
private static string GetMigrationsDirectory()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
var candidate = Path.Combine(
current.FullName,
"src",
"Platform",
"__Libraries",
"StellaOps.Platform.Database",
"Migrations",
"Release");
if (Directory.Exists(candidate))
{
return candidate;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Could not locate Platform release migrations directory.");
}
}

View File

@@ -1,4 +1,5 @@
using System.Linq;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json.Nodes;
using StellaOps.Platform.WebService.Contracts;
@@ -44,4 +45,87 @@ public sealed class PreferencesEndpointsTests : IClassFixture<PlatformWebApplica
Assert.NotNull(widgets);
Assert.Equal(new[] { "health", "quota" }, widgets!.Select(widget => widget!.GetValue<string>()).ToArray());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LanguagePreference_RoundTripAndSurvivesDashboardUpdate()
{
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-preferences");
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "actor-preferences");
var initial = await client.GetFromJsonAsync<PlatformLanguagePreference>(
"/api/v1/platform/preferences/language",
TestContext.Current.CancellationToken);
Assert.NotNull(initial);
Assert.Null(initial!.Locale);
var setLanguage = await client.PutAsJsonAsync(
"/api/v1/platform/preferences/language",
new PlatformLanguagePreferenceRequest("es-ES"),
TestContext.Current.CancellationToken);
setLanguage.EnsureSuccessStatusCode();
var updated = await setLanguage.Content.ReadFromJsonAsync<PlatformLanguagePreference>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(updated);
Assert.Equal("es-ES", updated!.Locale);
var dashboardUpdate = new PlatformDashboardPreferencesRequest(new JsonObject
{
["layout"] = "incident",
["widgets"] = new JsonArray("health"),
["filters"] = new JsonObject { ["scope"] = "tenant" }
});
var dashboardResponse = await client.PutAsJsonAsync(
"/api/v1/platform/preferences/dashboard",
dashboardUpdate,
TestContext.Current.CancellationToken);
dashboardResponse.EnsureSuccessStatusCode();
var reloaded = await client.GetFromJsonAsync<PlatformLanguagePreference>(
"/api/v1/platform/preferences/language",
TestContext.Current.CancellationToken);
Assert.NotNull(reloaded);
Assert.Equal("es-ES", reloaded!.Locale);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LanguagePreference_RejectsUnsupportedLocale()
{
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-preferences");
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "actor-preferences");
var response = await client.PutAsJsonAsync(
"/api/v1/platform/preferences/language",
new PlatformLanguagePreferenceRequest("xx-XX"),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LanguagePreference_NormalizesUkrainianAlias()
{
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-preferences");
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "actor-preferences");
var response = await client.PutAsJsonAsync(
"/api/v1/platform/preferences/language",
new PlatformLanguagePreferenceRequest("uk"),
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var updated = await response.Content.ReadFromJsonAsync<PlatformLanguagePreference>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(updated);
Assert.Equal("uk-UA", updated!.Locale);
}
}

View File

@@ -19,3 +19,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0762-A | DONE | Waived (test project; revalidated 2026-01-07). |
| TASK-030-019 | BLOCKED | Added analytics maintenance + cache normalization + query executor tests; analytics schema fixtures blocked by ingestion dependencies. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT_20260224_001-LOC-002-T | DONE | Sprint `docs/implplan/SPRINT_20260224_001_Platform_unified_translation_gap_closure.md`: added migration script + localization endpoint tests for translation persistence and override behavior. |
| SPRINT_20260224_004-LOC-302-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: added language preference endpoint coverage in `PreferencesEndpointsTests` (round-trip persistence + invalid locale rejection) and expanded locale catalog verification in `LocalizationEndpointsTests`. |
| SPRINT_20260224_004-LOC-305-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: extended `LocalizationEndpointsTests` to verify common-layer and `platform.*` namespace bundle availability for all supported locales. |
| SPRINT_20260224_004-LOC-307-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: extended localization and preference endpoint tests for Ukrainian rollout (`uk-UA` locale catalog/bundle assertions and alias normalization to canonical `uk-UA`). |