feat(i18n): persist Bokmal preference globally
This commit is contained in:
@@ -96,6 +96,7 @@ public sealed class AuthController : ControllerBase
|
||||
{
|
||||
public string AppVersion { get; init; } = "unknown";
|
||||
public string? AppCommitSha { get; init; }
|
||||
public string UiLanguage { get; init; } = "en";
|
||||
}
|
||||
public sealed record PendingEmailChangeResult(string? PendingEmail, DateTimeOffset? RequestedAtUtc);
|
||||
private const int MaxAvatarBytes = 1_000_000;
|
||||
@@ -539,6 +540,21 @@ public sealed class AuthController : ControllerBase
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
public sealed record UpdateLanguagePreferenceRequest(string? Language);
|
||||
|
||||
[HttpPut("preferences/language")]
|
||||
[Authorize(AuthenticationSchemes = "local")]
|
||||
public async Task<IActionResult> UpdateLanguagePreference([FromBody] UpdateLanguagePreferenceRequest request)
|
||||
{
|
||||
var user = await _users.GetUserAsync(User);
|
||||
if (user is null) return Unauthorized();
|
||||
var language = NormalizeUiLanguage(request?.Language);
|
||||
if (language is null) return BadRequest("Supported languages are en and nb-NO.");
|
||||
user.UiLanguage = language;
|
||||
var result = await _users.UpdateAsync(user);
|
||||
return result.Succeeded ? NoContent() : BadRequest(string.Join("; ", result.Errors.Select(error => error.Description)));
|
||||
}
|
||||
|
||||
public sealed record RequestEmailChangeRequest(string Email, string CurrentPassword);
|
||||
public sealed record ConfirmEmailChangeRequest(string UserId, string Email, string Token);
|
||||
public sealed record CancelEmailChangeRequest(string CurrentPassword);
|
||||
@@ -1200,7 +1216,20 @@ public sealed class AuthController : ControllerBase
|
||||
MicrosoftLink: new MicrosoftLinkDto(
|
||||
Linked: !string.IsNullOrWhiteSpace(user.MicrosoftTenantId) && !string.IsNullOrWhiteSpace(user.MicrosoftObjectId),
|
||||
Email: user.MicrosoftEmail,
|
||||
LinkedAt: user.MicrosoftLinkedAt));
|
||||
LinkedAt: user.MicrosoftLinkedAt))
|
||||
{
|
||||
UiLanguage = NormalizeUiLanguage(user.UiLanguage) ?? "en",
|
||||
};
|
||||
}
|
||||
|
||||
private static string? NormalizeUiLanguage(string? language)
|
||||
{
|
||||
var value = language?.Trim();
|
||||
if (string.Equals(value, "en", StringComparison.OrdinalIgnoreCase)) return "en";
|
||||
if (string.Equals(value, "nb", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, "nb-NO", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, "no", StringComparison.OrdinalIgnoreCase)) return "nb-NO";
|
||||
return null;
|
||||
}
|
||||
|
||||
private MeResult WithBuildMetadata(MeResult result, bool include)
|
||||
|
||||
@@ -92,6 +92,10 @@ namespace JobTrackerApi.Data
|
||||
.Property(x => x.EmailFollowUpRemindersEnabled)
|
||||
.HasDefaultValue(true);
|
||||
|
||||
modelBuilder.Entity<ApplicationUser>()
|
||||
.Property(x => x.UiLanguage)
|
||||
.HasMaxLength(16);
|
||||
|
||||
modelBuilder.Entity<AccountDeletionRequest>().Property(x => x.OwnerUserId).HasMaxLength(255);
|
||||
modelBuilder.Entity<AccountDeletionRequest>().Property(x => x.OwnerKey).HasMaxLength(64);
|
||||
modelBuilder.Entity<AccountDeletionRequest>().Property(x => x.RequestedByUserId).HasMaxLength(255);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using JobTrackerApi.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace JobTrackerApi.Migrations;
|
||||
|
||||
[DbContext(typeof(JobTrackerContext))]
|
||||
[Migration("20260828090000_AddUiLanguagePreference")]
|
||||
public partial class AddUiLanguagePreference : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "UiLanguage",
|
||||
table: "AspNetUsers",
|
||||
type: ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase) ? "varchar(16)" : "TEXT",
|
||||
maxLength: 16,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "UiLanguage", table: "AspNetUsers");
|
||||
}
|
||||
}
|
||||
@@ -481,6 +481,10 @@ namespace JobTrackerApi.Migrations
|
||||
b.Property<string>("StripeSubscriptionStatus")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UiLanguage")
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("TotpEnabledAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ public sealed class ApplicationUser : IdentityUser
|
||||
public bool AiEnabled { get; set; } = true;
|
||||
public bool ExternalAiProcessingAllowed { get; set; }
|
||||
public bool EmailFollowUpRemindersEnabled { get; set; } = true;
|
||||
// BCP 47 application UI preference. Null/legacy values safely fall back to English.
|
||||
public string? UiLanguage { get; set; }
|
||||
public string DeletionStatus { get; set; } = AccountDeletionStatuses.Active;
|
||||
public DateTimeOffset? DeletionRequestedAtUtc { get; set; }
|
||||
}
|
||||
|
||||
@@ -533,6 +533,7 @@ public static class StartupInitializationExtensions
|
||||
EnsureColumn(conn, "AspNetUsers", "StripeSubscriptionId", "ALTER TABLE AspNetUsers ADD COLUMN StripeSubscriptionId TEXT NULL;");
|
||||
EnsureColumn(conn, "AspNetUsers", "StripeSubscriptionStatus", "ALTER TABLE AspNetUsers ADD COLUMN StripeSubscriptionStatus TEXT NULL;");
|
||||
EnsureColumn(conn, "AspNetUsers", "StripeLastEventCreatedUtc", "ALTER TABLE AspNetUsers ADD COLUMN StripeLastEventCreatedUtc TEXT NULL;");
|
||||
EnsureColumn(conn, "AspNetUsers", "UiLanguage", "ALTER TABLE AspNetUsers ADD COLUMN UiLanguage TEXT NULL;");
|
||||
|
||||
static void EnsureUserRuleSettingsTable(DbConnection c)
|
||||
{
|
||||
@@ -1435,6 +1436,7 @@ public static class StartupInitializationExtensions
|
||||
EnsureMySqlColumn(conn, "AspNetUsers", "StripeSubscriptionId", "ALTER TABLE `AspNetUsers` ADD COLUMN `StripeSubscriptionId` varchar(255) NULL;");
|
||||
EnsureMySqlColumn(conn, "AspNetUsers", "StripeSubscriptionStatus", "ALTER TABLE `AspNetUsers` ADD COLUMN `StripeSubscriptionStatus` varchar(64) NULL;");
|
||||
EnsureMySqlColumn(conn, "AspNetUsers", "StripeLastEventCreatedUtc", "ALTER TABLE `AspNetUsers` ADD COLUMN `StripeLastEventCreatedUtc` datetime(6) NULL;");
|
||||
EnsureMySqlColumn(conn, "AspNetUsers", "UiLanguage", "ALTER TABLE `AspNetUsers` ADD COLUMN `UiLanguage` varchar(16) NULL;");
|
||||
|
||||
// RuleSettings is MIGRATION-owned — the initial migration creates it. The reconciler
|
||||
// used to create it too, which made a clean install fail with "Table 'RuleSettings'
|
||||
|
||||
Reference in New Issue
Block a user