feat: localize membership tier display
This commit is contained in:
@@ -22,6 +22,8 @@ internal class AppDbContext(
|
|||||||
{
|
{
|
||||||
public DbSet<Organization> Organizations => Set<Organization>();
|
public DbSet<Organization> Organizations => Set<Organization>();
|
||||||
public DbSet<OrganizationMembershipTier> OrganizationMembershipTiers => Set<OrganizationMembershipTier>();
|
public DbSet<OrganizationMembershipTier> OrganizationMembershipTiers => Set<OrganizationMembershipTier>();
|
||||||
|
public DbSet<OrganizationMembershipTierTranslation> OrganizationMembershipTierTranslations =>
|
||||||
|
Set<OrganizationMembershipTierTranslation>();
|
||||||
public DbSet<OrganizationMembership> OrganizationMemberships => Set<OrganizationMembership>();
|
public DbSet<OrganizationMembership> OrganizationMemberships => Set<OrganizationMembership>();
|
||||||
public DbSet<Workspace> Workspaces => Set<Workspace>();
|
public DbSet<Workspace> Workspaces => Set<Workspace>();
|
||||||
public DbSet<WorkspaceInvite> WorkspaceInvites => Set<WorkspaceInvite>();
|
public DbSet<WorkspaceInvite> WorkspaceInvites => Set<WorkspaceInvite>();
|
||||||
|
|||||||
2382
backend/src/Socialize.Api/Migrations/20260508003746_LocalizeOrganizationMembershipTiers.Designer.cs
generated
Normal file
2382
backend/src/Socialize.Api/Migrations/20260508003746_LocalizeOrganizationMembershipTiers.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class LocalizeOrganizationMembershipTiers : Migration
|
||||||
|
{
|
||||||
|
private static readonly string[] MembershipTierTranslationSeedColumns =
|
||||||
|
[
|
||||||
|
"Id",
|
||||||
|
"Culture",
|
||||||
|
"Description",
|
||||||
|
"MembershipTierId",
|
||||||
|
"Name"
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly string[] MembershipTierColumnsToRestore =
|
||||||
|
[
|
||||||
|
"Description",
|
||||||
|
"Name"
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Description",
|
||||||
|
table: "OrganizationMembershipTiers");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Name",
|
||||||
|
table: "OrganizationMembershipTiers");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "OrganizationMembershipTierTranslations",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
MembershipTierId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Culture = table.Column<string>(type: "character varying(16)", maxLength: 16, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_OrganizationMembershipTierTranslations", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_OrganizationMembershipTierTranslations_OrganizationMembersh~",
|
||||||
|
column: x => x.MembershipTierId,
|
||||||
|
principalTable: "OrganizationMembershipTiers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "OrganizationMembershipTierTranslations",
|
||||||
|
columns: MembershipTierTranslationSeedColumns,
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000001"), "en", "For trying Socialize on one real approval workflow.", new Guid("20000000-0000-0000-0000-000000000001"), "Free" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000002"), "fr", "Pour essayer Socialize sur un vrai workflow d'approbation.", new Guid("20000000-0000-0000-0000-000000000001"), "Free" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000003"), "en", "For solo operators managing recurring client reviews.", new Guid("20000000-0000-0000-0000-000000000002"), "Freelance" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000004"), "fr", "Pour les independants qui gerent des revisions client recurrentes.", new Guid("20000000-0000-0000-0000-000000000002"), "Freelance" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000005"), "en", "For agencies that need repeatable client approval operations.", new Guid("20000000-0000-0000-0000-000000000003"), "Agency" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000006"), "fr", "Pour les agences qui veulent des operations d'approbation client repetables.", new Guid("20000000-0000-0000-0000-000000000003"), "Agency" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000007"), "en", "For larger organizations with governance and access needs.", new Guid("20000000-0000-0000-0000-000000000004"), "Enterprise" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000008"), "fr", "Pour les grandes organisations avec des besoins de gouvernance et d'acces.", new Guid("20000000-0000-0000-0000-000000000004"), "Enterprise" }
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationMembershipTierTranslations_MembershipTierId_Cul~",
|
||||||
|
table: "OrganizationMembershipTierTranslations",
|
||||||
|
columns: ["MembershipTierId", "Culture"],
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "OrganizationMembershipTierTranslations");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Description",
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
type: "character varying(512)",
|
||||||
|
maxLength: 512,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Name",
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
type: "character varying(128)",
|
||||||
|
maxLength: 128,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("20000000-0000-0000-0000-000000000001"),
|
||||||
|
columns: MembershipTierColumnsToRestore,
|
||||||
|
values: new object[] { "For trying Socialize on one real approval workflow.", "Free" });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("20000000-0000-0000-0000-000000000002"),
|
||||||
|
columns: MembershipTierColumnsToRestore,
|
||||||
|
values: new object[] { "For solo operators managing recurring client reviews.", "Freelance" });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("20000000-0000-0000-0000-000000000003"),
|
||||||
|
columns: MembershipTierColumnsToRestore,
|
||||||
|
values: new object[] { "For agencies that need repeatable client approval operations.", "Agency" });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("20000000-0000-0000-0000-000000000004"),
|
||||||
|
columns: MembershipTierColumnsToRestore,
|
||||||
|
values: new object[] { "For larger organizations with governance and access needs.", "Enterprise" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1724,11 +1724,6 @@ namespace Socialize.Api.Migrations
|
|||||||
b.Property<int?>("ActiveContentLimit")
|
b.Property<int?>("ActiveContentLimit")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)");
|
|
||||||
|
|
||||||
b.Property<int?>("ExternalReviewerLimit")
|
b.Property<int?>("ExternalReviewerLimit")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
@@ -1746,11 +1741,6 @@ namespace Socialize.Api.Migrations
|
|||||||
b.Property<int?>("MonthlyPriceCents")
|
b.Property<int?>("MonthlyPriceCents")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<int>("SortOrder")
|
b.Property<int>("SortOrder")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
@@ -1771,13 +1761,11 @@ namespace Socialize.Api.Migrations
|
|||||||
{
|
{
|
||||||
Id = new Guid("20000000-0000-0000-0000-000000000001"),
|
Id = new Guid("20000000-0000-0000-0000-000000000001"),
|
||||||
ActiveContentLimit = 3,
|
ActiveContentLimit = 3,
|
||||||
Description = "For trying Socialize on one real approval workflow.",
|
|
||||||
ExternalReviewerLimit = 1,
|
ExternalReviewerLimit = 1,
|
||||||
IsCustom = false,
|
IsCustom = false,
|
||||||
Key = "free",
|
Key = "free",
|
||||||
MemberLimit = 2,
|
MemberLimit = 2,
|
||||||
MonthlyPriceCents = 0,
|
MonthlyPriceCents = 0,
|
||||||
Name = "Free",
|
|
||||||
SortOrder = 10,
|
SortOrder = 10,
|
||||||
WorkspaceLimit = 1
|
WorkspaceLimit = 1
|
||||||
},
|
},
|
||||||
@@ -1785,13 +1773,11 @@ namespace Socialize.Api.Migrations
|
|||||||
{
|
{
|
||||||
Id = new Guid("20000000-0000-0000-0000-000000000002"),
|
Id = new Guid("20000000-0000-0000-0000-000000000002"),
|
||||||
ActiveContentLimit = 25,
|
ActiveContentLimit = 25,
|
||||||
Description = "For solo operators managing recurring client reviews.",
|
|
||||||
ExternalReviewerLimit = 10,
|
ExternalReviewerLimit = 10,
|
||||||
IsCustom = false,
|
IsCustom = false,
|
||||||
Key = "freelance",
|
Key = "freelance",
|
||||||
MemberLimit = 5,
|
MemberLimit = 5,
|
||||||
MonthlyPriceCents = 1900,
|
MonthlyPriceCents = 1900,
|
||||||
Name = "Freelance",
|
|
||||||
SortOrder = 20,
|
SortOrder = 20,
|
||||||
WorkspaceLimit = 3
|
WorkspaceLimit = 3
|
||||||
},
|
},
|
||||||
@@ -1799,26 +1785,120 @@ namespace Socialize.Api.Migrations
|
|||||||
{
|
{
|
||||||
Id = new Guid("20000000-0000-0000-0000-000000000003"),
|
Id = new Guid("20000000-0000-0000-0000-000000000003"),
|
||||||
ActiveContentLimit = 250,
|
ActiveContentLimit = 250,
|
||||||
Description = "For agencies that need repeatable client approval operations.",
|
|
||||||
IsCustom = false,
|
IsCustom = false,
|
||||||
Key = "agency",
|
Key = "agency",
|
||||||
MemberLimit = 25,
|
MemberLimit = 25,
|
||||||
MonthlyPriceCents = 7900,
|
MonthlyPriceCents = 7900,
|
||||||
Name = "Agency",
|
|
||||||
SortOrder = 30,
|
SortOrder = 30,
|
||||||
WorkspaceLimit = 15
|
WorkspaceLimit = 15
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("20000000-0000-0000-0000-000000000004"),
|
Id = new Guid("20000000-0000-0000-0000-000000000004"),
|
||||||
Description = "For larger organizations with governance and access needs.",
|
|
||||||
IsCustom = true,
|
IsCustom = true,
|
||||||
Key = "enterprise",
|
Key = "enterprise",
|
||||||
Name = "Enterprise",
|
|
||||||
SortOrder = 40
|
SortOrder = 40
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTierTranslation", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Culture")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("character varying(16)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<Guid>("MembershipTierId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MembershipTierId", "Culture")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("OrganizationMembershipTierTranslations", (string)null);
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000001"),
|
||||||
|
Culture = "en",
|
||||||
|
Description = "For trying Socialize on one real approval workflow.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000001"),
|
||||||
|
Name = "Free"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000002"),
|
||||||
|
Culture = "fr",
|
||||||
|
Description = "Pour essayer Socialize sur un vrai workflow d'approbation.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000001"),
|
||||||
|
Name = "Free"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000003"),
|
||||||
|
Culture = "en",
|
||||||
|
Description = "For solo operators managing recurring client reviews.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000002"),
|
||||||
|
Name = "Freelance"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000004"),
|
||||||
|
Culture = "fr",
|
||||||
|
Description = "Pour les independants qui gerent des revisions client recurrentes.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000002"),
|
||||||
|
Name = "Freelance"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000005"),
|
||||||
|
Culture = "en",
|
||||||
|
Description = "For agencies that need repeatable client approval operations.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000003"),
|
||||||
|
Name = "Agency"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000006"),
|
||||||
|
Culture = "fr",
|
||||||
|
Description = "Pour les agences qui veulent des operations d'approbation client repetables.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000003"),
|
||||||
|
Name = "Agency"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000007"),
|
||||||
|
Culture = "en",
|
||||||
|
Description = "For larger organizations with governance and access needs.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000004"),
|
||||||
|
Name = "Enterprise"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000008"),
|
||||||
|
Culture = "fr",
|
||||||
|
Description = "Pour les grandes organisations avec des besoins de gouvernance et d'acces.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000004"),
|
||||||
|
Name = "Enterprise"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -2256,6 +2336,15 @@ namespace Socialize.Api.Migrations
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTierTranslation", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTier", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MembershipTierId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Socialize.Api.Modules.Organizations.Data.Organization", null)
|
b.HasOne("Socialize.Api.Modules.Organizations.Data.Organization", null)
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ internal class OrganizationMembershipTier
|
|||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public required string Key { get; set; }
|
public required string Key { get; set; }
|
||||||
public required string Name { get; set; }
|
|
||||||
public required string Description { get; set; }
|
|
||||||
public int? MonthlyPriceCents { get; set; }
|
public int? MonthlyPriceCents { get; set; }
|
||||||
public int? WorkspaceLimit { get; set; }
|
public int? WorkspaceLimit { get; set; }
|
||||||
public int? ActiveContentLimit { get; set; }
|
public int? ActiveContentLimit { get; set; }
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ internal static class OrganizationMembershipTierSeed
|
|||||||
{
|
{
|
||||||
Id = FreeId,
|
Id = FreeId,
|
||||||
Key = "free",
|
Key = "free",
|
||||||
Name = "Free",
|
|
||||||
Description = "For trying Socialize on one real approval workflow.",
|
|
||||||
MonthlyPriceCents = 0,
|
MonthlyPriceCents = 0,
|
||||||
WorkspaceLimit = 1,
|
WorkspaceLimit = 1,
|
||||||
ActiveContentLimit = 3,
|
ActiveContentLimit = 3,
|
||||||
@@ -26,8 +24,6 @@ internal static class OrganizationMembershipTierSeed
|
|||||||
{
|
{
|
||||||
Id = FreelanceId,
|
Id = FreelanceId,
|
||||||
Key = "freelance",
|
Key = "freelance",
|
||||||
Name = "Freelance",
|
|
||||||
Description = "For solo operators managing recurring client reviews.",
|
|
||||||
MonthlyPriceCents = 1900,
|
MonthlyPriceCents = 1900,
|
||||||
WorkspaceLimit = 3,
|
WorkspaceLimit = 3,
|
||||||
ActiveContentLimit = 25,
|
ActiveContentLimit = 25,
|
||||||
@@ -39,8 +35,6 @@ internal static class OrganizationMembershipTierSeed
|
|||||||
{
|
{
|
||||||
Id = AgencyId,
|
Id = AgencyId,
|
||||||
Key = "agency",
|
Key = "agency",
|
||||||
Name = "Agency",
|
|
||||||
Description = "For agencies that need repeatable client approval operations.",
|
|
||||||
MonthlyPriceCents = 7900,
|
MonthlyPriceCents = 7900,
|
||||||
WorkspaceLimit = 15,
|
WorkspaceLimit = 15,
|
||||||
ActiveContentLimit = 250,
|
ActiveContentLimit = 250,
|
||||||
@@ -52,8 +46,6 @@ internal static class OrganizationMembershipTierSeed
|
|||||||
{
|
{
|
||||||
Id = EnterpriseId,
|
Id = EnterpriseId,
|
||||||
Key = "enterprise",
|
Key = "enterprise",
|
||||||
Name = "Enterprise",
|
|
||||||
Description = "For larger organizations with governance and access needs.",
|
|
||||||
MonthlyPriceCents = null,
|
MonthlyPriceCents = null,
|
||||||
WorkspaceLimit = null,
|
WorkspaceLimit = null,
|
||||||
ActiveContentLimit = null,
|
ActiveContentLimit = null,
|
||||||
@@ -63,4 +55,72 @@ internal static class OrganizationMembershipTierSeed
|
|||||||
SortOrder = 40,
|
SortOrder = 40,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public static readonly OrganizationMembershipTierTranslation[] Translations =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("20000000-0000-0001-0000-000000000001"),
|
||||||
|
MembershipTierId = FreeId,
|
||||||
|
Culture = "en",
|
||||||
|
Name = "Free",
|
||||||
|
Description = "For trying Socialize on one real approval workflow.",
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("20000000-0000-0001-0000-000000000002"),
|
||||||
|
MembershipTierId = FreeId,
|
||||||
|
Culture = "fr",
|
||||||
|
Name = "Free",
|
||||||
|
Description = "Pour essayer Socialize sur un vrai workflow d'approbation.",
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("20000000-0000-0001-0000-000000000003"),
|
||||||
|
MembershipTierId = FreelanceId,
|
||||||
|
Culture = "en",
|
||||||
|
Name = "Freelance",
|
||||||
|
Description = "For solo operators managing recurring client reviews.",
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("20000000-0000-0001-0000-000000000004"),
|
||||||
|
MembershipTierId = FreelanceId,
|
||||||
|
Culture = "fr",
|
||||||
|
Name = "Freelance",
|
||||||
|
Description = "Pour les independants qui gerent des revisions client recurrentes.",
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("20000000-0000-0001-0000-000000000005"),
|
||||||
|
MembershipTierId = AgencyId,
|
||||||
|
Culture = "en",
|
||||||
|
Name = "Agency",
|
||||||
|
Description = "For agencies that need repeatable client approval operations.",
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("20000000-0000-0001-0000-000000000006"),
|
||||||
|
MembershipTierId = AgencyId,
|
||||||
|
Culture = "fr",
|
||||||
|
Name = "Agency",
|
||||||
|
Description = "Pour les agences qui veulent des operations d'approbation client repetables.",
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("20000000-0000-0001-0000-000000000007"),
|
||||||
|
MembershipTierId = EnterpriseId,
|
||||||
|
Culture = "en",
|
||||||
|
Name = "Enterprise",
|
||||||
|
Description = "For larger organizations with governance and access needs.",
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("20000000-0000-0001-0000-000000000008"),
|
||||||
|
MembershipTierId = EnterpriseId,
|
||||||
|
Culture = "fr",
|
||||||
|
Name = "Enterprise",
|
||||||
|
Description = "Pour les grandes organisations avec des besoins de gouvernance et d'acces.",
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Socialize.Api.Modules.Organizations.Data;
|
||||||
|
|
||||||
|
internal class OrganizationMembershipTierTranslation
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
public Guid MembershipTierId { get; set; }
|
||||||
|
public required string Culture { get; set; }
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public required string Description { get; set; }
|
||||||
|
}
|
||||||
@@ -30,13 +30,26 @@ internal static class OrganizationModelConfiguration
|
|||||||
tier.ToTable("OrganizationMembershipTiers");
|
tier.ToTable("OrganizationMembershipTiers");
|
||||||
tier.HasKey(x => x.Id);
|
tier.HasKey(x => x.Id);
|
||||||
tier.Property(x => x.Key).HasMaxLength(64).IsRequired();
|
tier.Property(x => x.Key).HasMaxLength(64).IsRequired();
|
||||||
tier.Property(x => x.Name).HasMaxLength(128).IsRequired();
|
|
||||||
tier.Property(x => x.Description).HasMaxLength(512).IsRequired();
|
|
||||||
tier.HasIndex(x => x.Key).IsUnique();
|
tier.HasIndex(x => x.Key).IsUnique();
|
||||||
tier.HasIndex(x => x.SortOrder);
|
tier.HasIndex(x => x.SortOrder);
|
||||||
tier.HasData(OrganizationMembershipTierSeed.Tiers);
|
tier.HasData(OrganizationMembershipTierSeed.Tiers);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<OrganizationMembershipTierTranslation>(translation =>
|
||||||
|
{
|
||||||
|
translation.ToTable("OrganizationMembershipTierTranslations");
|
||||||
|
translation.HasKey(x => x.Id);
|
||||||
|
translation.Property(x => x.Culture).HasMaxLength(16).IsRequired();
|
||||||
|
translation.Property(x => x.Name).HasMaxLength(128).IsRequired();
|
||||||
|
translation.Property(x => x.Description).HasMaxLength(512).IsRequired();
|
||||||
|
translation.HasIndex(x => new { x.MembershipTierId, x.Culture }).IsUnique();
|
||||||
|
translation.HasOne<OrganizationMembershipTier>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.MembershipTierId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
translation.HasData(OrganizationMembershipTierSeed.Translations);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<OrganizationMembership>(membership =>
|
modelBuilder.Entity<OrganizationMembership>(membership =>
|
||||||
{
|
{
|
||||||
membership.ToTable("OrganizationMemberships");
|
membership.ToTable("OrganizationMemberships");
|
||||||
|
|||||||
@@ -71,7 +71,11 @@ internal class CreateOrganizationHandler(
|
|||||||
OrganizationDto.FromOrganization(
|
OrganizationDto.FromOrganization(
|
||||||
organization,
|
organization,
|
||||||
OrganizationPermissionRules.GetPermissionsForRole(OrganizationRoles.Owner),
|
OrganizationPermissionRules.GetPermissionsForRole(OrganizationRoles.Owner),
|
||||||
OrganizationMembershipTierDto.FromTier(membershipTier)),
|
await OrganizationMembershipTierLocalization.GetLocalizedTierDtoAsync(
|
||||||
|
dbContext,
|
||||||
|
membershipTier,
|
||||||
|
HttpContext,
|
||||||
|
ct)),
|
||||||
StatusCodes.Status201Created,
|
StatusCodes.Status201Created,
|
||||||
ct);
|
ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,14 +47,17 @@ internal class GetOrganizationHandler(
|
|||||||
IReadOnlyCollection<OrganizationMemberDto> members = await GetMembersAsync(organizationId, ct);
|
IReadOnlyCollection<OrganizationMemberDto> members = await GetMembersAsync(organizationId, ct);
|
||||||
IReadOnlyCollection<WorkspaceDto> workspaces = await GetWorkspacesAsync(organizationId, ct);
|
IReadOnlyCollection<WorkspaceDto> workspaces = await GetWorkspacesAsync(organizationId, ct);
|
||||||
OrganizationMembershipTier membershipTier = await GetMembershipTierAsync(organization.MembershipTierId, ct);
|
OrganizationMembershipTier membershipTier = await GetMembershipTierAsync(organization.MembershipTierId, ct);
|
||||||
OrganizationUsageDto usage = await GetUsageAsync(organization, membershipTier, ct);
|
OrganizationMembershipTierDto membershipTierDto =
|
||||||
IReadOnlyCollection<OrganizationMembershipTierDto> availableMembershipTiers = await GetAvailableMembershipTiersAsync(ct);
|
await OrganizationMembershipTierLocalization.GetLocalizedTierDtoAsync(dbContext, membershipTier, HttpContext, ct);
|
||||||
|
OrganizationUsageDto usage = await GetUsageAsync(organization, membershipTier, membershipTierDto, ct);
|
||||||
|
IReadOnlyCollection<OrganizationMembershipTierDto> availableMembershipTiers =
|
||||||
|
await OrganizationMembershipTierLocalization.GetLocalizedTierDtosAsync(dbContext, HttpContext, ct);
|
||||||
|
|
||||||
await SendOkAsync(
|
await SendOkAsync(
|
||||||
OrganizationDto.FromOrganization(
|
OrganizationDto.FromOrganization(
|
||||||
organization,
|
organization,
|
||||||
currentUserPermissions,
|
currentUserPermissions,
|
||||||
OrganizationMembershipTierDto.FromTier(membershipTier),
|
membershipTierDto,
|
||||||
members,
|
members,
|
||||||
workspaces,
|
workspaces,
|
||||||
usage,
|
usage,
|
||||||
@@ -107,6 +110,7 @@ internal class GetOrganizationHandler(
|
|||||||
private async Task<OrganizationUsageDto> GetUsageAsync(
|
private async Task<OrganizationUsageDto> GetUsageAsync(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
OrganizationMembershipTier membershipTier,
|
OrganizationMembershipTier membershipTier,
|
||||||
|
OrganizationMembershipTierDto membershipTierDto,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
Guid[] workspaceIds = await dbContext.Workspaces
|
Guid[] workspaceIds = await dbContext.Workspaces
|
||||||
@@ -138,7 +142,7 @@ internal class GetOrganizationHandler(
|
|||||||
|
|
||||||
return new OrganizationUsageDto(
|
return new OrganizationUsageDto(
|
||||||
membershipTier.Key,
|
membershipTier.Key,
|
||||||
membershipTier.Name,
|
membershipTierDto.Name,
|
||||||
[
|
[
|
||||||
new OrganizationUsageItemDto("users", userCount, membershipTier.MemberLimit),
|
new OrganizationUsageItemDto("users", userCount, membershipTier.MemberLimit),
|
||||||
new OrganizationUsageItemDto("workspaces", workspaceIds.Length, membershipTier.WorkspaceLimit),
|
new OrganizationUsageItemDto("workspaces", workspaceIds.Length, membershipTier.WorkspaceLimit),
|
||||||
@@ -155,18 +159,6 @@ internal class GetOrganizationHandler(
|
|||||||
.SingleAsync(tier => tier.Id == OrganizationMembershipTierSeed.FreeId, ct);
|
.SingleAsync(tier => tier.Id == OrganizationMembershipTierSeed.FreeId, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IReadOnlyCollection<OrganizationMembershipTierDto>> GetAvailableMembershipTiersAsync(CancellationToken ct)
|
|
||||||
{
|
|
||||||
List<OrganizationMembershipTier> tiers = await dbContext.OrganizationMembershipTiers
|
|
||||||
.OrderBy(tier => tier.SortOrder)
|
|
||||||
.ThenBy(tier => tier.Name)
|
|
||||||
.ToListAsync(ct);
|
|
||||||
|
|
||||||
return tiers
|
|
||||||
.Select(OrganizationMembershipTierDto.FromTier)
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<int> GetExternalReviewerCountAsync(
|
private async Task<int> GetExternalReviewerCountAsync(
|
||||||
IReadOnlyCollection<Guid> workspaceIds,
|
IReadOnlyCollection<Guid> workspaceIds,
|
||||||
IReadOnlyCollection<Guid> organizationMemberUserIds,
|
IReadOnlyCollection<Guid> organizationMemberUserIds,
|
||||||
|
|||||||
@@ -26,17 +26,11 @@ internal class GetOrganizationsHandler(
|
|||||||
.OrderBy(organization => organization.Name)
|
.OrderBy(organization => organization.Name)
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
|
|
||||||
Guid[] membershipTierIds = organizations
|
Dictionary<Guid, OrganizationMembershipTierDto> membershipTiersById =
|
||||||
.Select(organization => organization.MembershipTierId)
|
(await OrganizationMembershipTierLocalization.GetLocalizedTierDtosAsync(dbContext, HttpContext, ct))
|
||||||
.Distinct()
|
|
||||||
.ToArray();
|
|
||||||
List<OrganizationMembershipTier> membershipTierModels = await dbContext.OrganizationMembershipTiers
|
|
||||||
.Where(tier => membershipTierIds.Contains(tier.Id))
|
|
||||||
.ToListAsync(ct);
|
|
||||||
Dictionary<Guid, OrganizationMembershipTierDto> membershipTiersById = membershipTierModels
|
|
||||||
.ToDictionary(
|
.ToDictionary(
|
||||||
tier => tier.Id,
|
tier => tier.Id,
|
||||||
OrganizationMembershipTierDto.FromTier);
|
tier => tier);
|
||||||
|
|
||||||
List<OrganizationDto> response = [];
|
List<OrganizationDto> response = [];
|
||||||
foreach (Organization organization in organizations)
|
foreach (Organization organization in organizations)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using FastEndpoints;
|
using FastEndpoints;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Api.Modules.Organizations.Data;
|
using Socialize.Api.Modules.Organizations.Services;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Organizations.Handlers;
|
namespace Socialize.Api.Modules.Organizations.Handlers;
|
||||||
|
|
||||||
@@ -16,15 +15,8 @@ internal class ListOrganizationMembershipTiersHandler(AppDbContext dbContext)
|
|||||||
|
|
||||||
public override async Task HandleAsync(CancellationToken ct)
|
public override async Task HandleAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
List<OrganizationMembershipTier> tierModels = await dbContext.OrganizationMembershipTiers
|
await SendOkAsync(
|
||||||
.OrderBy(tier => tier.SortOrder)
|
await OrganizationMembershipTierLocalization.GetLocalizedTierDtosAsync(dbContext, HttpContext, ct),
|
||||||
.ThenBy(tier => tier.Name)
|
ct);
|
||||||
.ToListAsync(ct);
|
|
||||||
|
|
||||||
OrganizationMembershipTierDto[] tiers = tierModels
|
|
||||||
.Select(OrganizationMembershipTierDto.FromTier)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
await SendOkAsync(tiers, ct);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,13 +62,15 @@ internal record OrganizationMembershipTierDto(
|
|||||||
bool IsCustom,
|
bool IsCustom,
|
||||||
int SortOrder)
|
int SortOrder)
|
||||||
{
|
{
|
||||||
public static OrganizationMembershipTierDto FromTier(OrganizationMembershipTier tier)
|
public static OrganizationMembershipTierDto FromTier(
|
||||||
|
OrganizationMembershipTier tier,
|
||||||
|
OrganizationMembershipTierTranslation translation)
|
||||||
{
|
{
|
||||||
return new OrganizationMembershipTierDto(
|
return new OrganizationMembershipTierDto(
|
||||||
tier.Id,
|
tier.Id,
|
||||||
tier.Key,
|
tier.Key,
|
||||||
tier.Name,
|
translation.Name,
|
||||||
tier.Description,
|
translation.Description,
|
||||||
tier.MonthlyPriceCents,
|
tier.MonthlyPriceCents,
|
||||||
tier.WorkspaceLimit,
|
tier.WorkspaceLimit,
|
||||||
tier.ActiveContentLimit,
|
tier.ActiveContentLimit,
|
||||||
|
|||||||
@@ -74,7 +74,11 @@ internal class UpdateOrganizationMembershipTierHandler(
|
|||||||
OrganizationDto.FromOrganization(
|
OrganizationDto.FromOrganization(
|
||||||
organization,
|
organization,
|
||||||
currentUserPermissions,
|
currentUserPermissions,
|
||||||
OrganizationMembershipTierDto.FromTier(membershipTier)),
|
await OrganizationMembershipTierLocalization.GetLocalizedTierDtoAsync(
|
||||||
|
dbContext,
|
||||||
|
membershipTier,
|
||||||
|
HttpContext,
|
||||||
|
ct)),
|
||||||
ct);
|
ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Globalization;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Modules.Organizations.Data;
|
||||||
|
using Socialize.Api.Modules.Organizations.Handlers;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Organizations.Services;
|
||||||
|
|
||||||
|
internal static class OrganizationMembershipTierLocalization
|
||||||
|
{
|
||||||
|
private const string DefaultCulture = "en";
|
||||||
|
|
||||||
|
private static readonly HashSet<string> SupportedCultures = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"en",
|
||||||
|
"fr",
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string GetRequestedCulture(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(httpContext);
|
||||||
|
|
||||||
|
string header = httpContext.Request.Headers.AcceptLanguage.ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(header))
|
||||||
|
{
|
||||||
|
return DefaultCulture;
|
||||||
|
}
|
||||||
|
|
||||||
|
return header
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Select(ParseLanguagePreference)
|
||||||
|
.Where(preference => preference.Culture is not null)
|
||||||
|
.OrderByDescending(preference => preference.Quality)
|
||||||
|
.Select(preference => preference.Culture!)
|
||||||
|
.FirstOrDefault(IsSupportedCulture)
|
||||||
|
?? DefaultCulture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<IReadOnlyCollection<OrganizationMembershipTierDto>> GetLocalizedTierDtosAsync(
|
||||||
|
AppDbContext dbContext,
|
||||||
|
HttpContext httpContext,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(dbContext);
|
||||||
|
ArgumentNullException.ThrowIfNull(httpContext);
|
||||||
|
|
||||||
|
string culture = GetRequestedCulture(httpContext);
|
||||||
|
|
||||||
|
List<OrganizationMembershipTier> tiers = await dbContext.OrganizationMembershipTiers
|
||||||
|
.OrderBy(tier => tier.SortOrder)
|
||||||
|
.ThenBy(tier => tier.Key)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
Guid[] tierIds = tiers
|
||||||
|
.Select(tier => tier.Id)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
List<OrganizationMembershipTierTranslation> translations = await dbContext.OrganizationMembershipTierTranslations
|
||||||
|
.Where(translation =>
|
||||||
|
tierIds.Contains(translation.MembershipTierId) &&
|
||||||
|
(translation.Culture == culture || translation.Culture == DefaultCulture))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
return tiers
|
||||||
|
.Select(tier => OrganizationMembershipTierDto.FromTier(
|
||||||
|
tier,
|
||||||
|
SelectTranslation(translations, tier.Id, culture)))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<OrganizationMembershipTierDto> GetLocalizedTierDtoAsync(
|
||||||
|
AppDbContext dbContext,
|
||||||
|
OrganizationMembershipTier tier,
|
||||||
|
HttpContext httpContext,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(dbContext);
|
||||||
|
ArgumentNullException.ThrowIfNull(tier);
|
||||||
|
ArgumentNullException.ThrowIfNull(httpContext);
|
||||||
|
|
||||||
|
string culture = GetRequestedCulture(httpContext);
|
||||||
|
|
||||||
|
List<OrganizationMembershipTierTranslation> translations = await dbContext.OrganizationMembershipTierTranslations
|
||||||
|
.Where(translation =>
|
||||||
|
translation.MembershipTierId == tier.Id &&
|
||||||
|
(translation.Culture == culture || translation.Culture == DefaultCulture))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
return OrganizationMembershipTierDto.FromTier(tier, SelectTranslation(translations, tier.Id, culture));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OrganizationMembershipTierTranslation SelectTranslation(
|
||||||
|
IReadOnlyCollection<OrganizationMembershipTierTranslation> translations,
|
||||||
|
Guid membershipTierId,
|
||||||
|
string culture)
|
||||||
|
{
|
||||||
|
return translations.FirstOrDefault(translation =>
|
||||||
|
translation.MembershipTierId == membershipTierId &&
|
||||||
|
string.Equals(translation.Culture, culture, StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? translations.First(translation =>
|
||||||
|
translation.MembershipTierId == membershipTierId &&
|
||||||
|
string.Equals(translation.Culture, DefaultCulture, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LanguagePreference ParseLanguagePreference(string value)
|
||||||
|
{
|
||||||
|
string[] parts = value.Split(';', StringSplitOptions.TrimEntries);
|
||||||
|
string? culture = NormalizeCulture(parts[0]);
|
||||||
|
decimal quality = 1m;
|
||||||
|
|
||||||
|
foreach (string part in parts.Skip(1))
|
||||||
|
{
|
||||||
|
if (part.StartsWith("q=", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
decimal.TryParse(part[2..], NumberStyles.Number, CultureInfo.InvariantCulture, out decimal parsedQuality))
|
||||||
|
{
|
||||||
|
quality = parsedQuality;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LanguagePreference(culture, quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeCulture(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value) || value == "*")
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string neutralCulture = value.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)[0];
|
||||||
|
|
||||||
|
return SupportedCultures.FirstOrDefault(culture =>
|
||||||
|
string.Equals(culture, neutralCulture, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSupportedCulture(string culture)
|
||||||
|
{
|
||||||
|
return SupportedCultures.Contains(culture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record LanguagePreference(string? Culture, decimal Quality);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Task: Localized organization membership tier display
|
||||||
|
|
||||||
|
## Feature Spec
|
||||||
|
|
||||||
|
`docs/FEATURES/organizations.md`
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Move membership tier display names and descriptions into localized database rows so API consumers receive tier copy in the requested language with English fallback.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Add membership tier translation persistence for `en` and `fr`.
|
||||||
|
- Remove tier `Name` and `Description` from the base tier model.
|
||||||
|
- Select tier translation from `Accept-Language`, fallback to English.
|
||||||
|
- Send the active frontend locale as `Accept-Language` on API requests.
|
||||||
|
- Regenerate EF migration, OpenAPI, and frontend schema.
|
||||||
|
- Audit similar database-backed display strings that are currently unlocalized.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet ef migrations add LocalizeOrganizationMembershipTiers --project backend/src/Socialize.Api/Socialize.Api.csproj --startup-project backend/src/Socialize.Api/Socialize.Api.csproj
|
||||||
|
dotnet test backend/Socialize.slnx
|
||||||
|
cd frontend && npm run build
|
||||||
|
./scripts/update-openapi.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Done When
|
||||||
|
|
||||||
|
- [x] Tier display names and descriptions are stored per culture.
|
||||||
|
- [x] Tier APIs return localized `name` and `description`.
|
||||||
|
- [x] English is used when the requested locale is missing.
|
||||||
|
- [x] Frontend API requests include the current locale.
|
||||||
|
- [x] Similar unlocalized persisted display strings are identified.
|
||||||
|
|
||||||
|
## Localization Audit
|
||||||
|
|
||||||
|
- Calendar catalog seed rows still store curated `Title` and `Description` as single-language strings. This looks similar to membership tiers because the copy is product-owned catalog metadata, not user-authored content.
|
||||||
|
- Backend-generated notification/activity messages are persisted as English sentences. They are event history, but they are still user-visible generated copy.
|
||||||
|
- Most other `Name`, `Title`, and `Description` fields found in modules are user-authored domain content, imported calendar event data, identifiers, roles, statuses, or test data and should not be translated by the platform.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
||||||
import config from '@/config.js';
|
import config from '@/config.js';
|
||||||
|
import { i18n } from '@/plugins/i18n.js';
|
||||||
|
|
||||||
export function useClient() {
|
export function useClient() {
|
||||||
const client = axios.create({
|
const client = axios.create({
|
||||||
@@ -36,6 +37,8 @@ export function useClient() {
|
|||||||
config.headers.Authorization = `Bearer ${authStore.accessToken}`;
|
config.headers.Authorization = `Bearer ${authStore.accessToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.headers['Accept-Language'] = i18n.global.locale.value ?? 'en';
|
||||||
|
|
||||||
if (config.data instanceof FormData) {
|
if (config.data instanceof FormData) {
|
||||||
console.log(`Data is FormData, removing explicit Content-Type header for: ${config.method?.toUpperCase()} ${config.url}`);
|
console.log(`Data is FormData, removing explicit Content-Type header for: ${config.method?.toUpperCase()} ${config.url}`);
|
||||||
delete config.headers['Content-Type'];
|
delete config.headers['Content-Type'];
|
||||||
|
|||||||
Reference in New Issue
Block a user