From bcea6ab0996480dc1f7810441b8d8d51396278a9 Mon Sep 17 00:00:00 2001 From: Sean Albert Date: Fri, 17 Jul 2026 09:43:25 -0700 Subject: [PATCH 1/2] feat(api): add key-level translation strings and jurisdiction content models --- .../migration.sql | 55 ++++++++++++++++++ api/prisma/schema.prisma | 57 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 api/prisma/migrations/66_key_level_translation_content/migration.sql diff --git a/api/prisma/migrations/66_key_level_translation_content/migration.sql b/api/prisma/migrations/66_key_level_translation_content/migration.sql new file mode 100644 index 0000000000..4c3af275e8 --- /dev/null +++ b/api/prisma/migrations/66_key_level_translation_content/migration.sql @@ -0,0 +1,55 @@ +-- CreateEnum +CREATE TYPE "site_enum" AS ENUM ('public', 'partners'); + +-- CreateEnum +CREATE TYPE "translation_origin" AS ENUM ('machine', 'human'); + +-- CreateTable +CREATE TABLE "jurisdiction_content" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "jurisdiction_id" UUID NOT NULL, + "language" "languages_enum" NOT NULL, + "footer" JSONB, + "faq" JSONB, + "resources" JSONB, + "disclaimers" JSONB, + "contact" JSONB, + + CONSTRAINT "jurisdiction_content_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "translation_strings" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "jurisdiction_id" UUID, + "language" "languages_enum" NOT NULL, + "site" "site_enum", + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + "source_hash" TEXT, + "origin" "translation_origin", + + CONSTRAINT "translation_strings_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "jurisdiction_content_jurisdiction_id_language_key" ON "jurisdiction_content"("jurisdiction_id", "language"); + +-- CreateIndex +CREATE INDEX "translation_strings_jurisdiction_id_language_site_idx" ON "translation_strings"("jurisdiction_id", "language", "site"); + +-- CreateIndex +-- NULLS NOT DISTINCT (hand-added; Prisma cannot express it in the schema) so base rows (null +-- jurisdiction_id and site) and global Partners rows (null jurisdiction_id) cannot be duplicated. +-- Requires Postgres 15+. +CREATE UNIQUE INDEX "translation_strings_jurisdiction_id_language_site_key_key" ON "translation_strings"("jurisdiction_id", "language", "site", "key") NULLS NOT DISTINCT; + +-- AddForeignKey +ALTER TABLE "jurisdiction_content" ADD CONSTRAINT "jurisdiction_content_jurisdiction_id_fkey" FOREIGN KEY ("jurisdiction_id") REFERENCES "jurisdictions"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "translation_strings" ADD CONSTRAINT "translation_strings_jurisdiction_id_fkey" FOREIGN KEY ("jurisdiction_id") REFERENCES "jurisdictions"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index b24800c215..30dd234793 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -779,6 +779,27 @@ model HouseholdMemberSnapshot { } // Jurisdiction Section +model JurisdictionContent { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + + jurisdictionId String @map("jurisdiction_id") @db.Uuid + language LanguagesEnum + + // Typed JSON documents; rich-text bodies are sanitized HTML. + footer Json? + faq Json? + resources Json? + disclaimers Json? + contact Json? + + jurisdictions Jurisdictions @relation(fields: [jurisdictionId], references: [id], onDelete: Cascade, onUpdate: NoAction) + + @@unique([jurisdictionId, language]) + @@map("jurisdiction_content") +} + model Jurisdictions { id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) @@ -818,12 +839,14 @@ model Jurisdictions { agency Agency[] amiCharts AmiChart[] featureFlags FeatureFlags[] + jurisdictionContent JurisdictionContent[] listings Listings[] listingSnapshots ListingSnapshot[] multiselectQuestions MultiselectQuestions[] properties Properties[] reservedCommunityTypes ReservedCommunityTypes[] translations Translations[] + translationStrings TranslationStrings[] user_accounts UserAccounts[] userAccountSnapshot UserAccountSnapshot[] @@ -1726,6 +1749,26 @@ model ScriptRuns { } // Translation Section +model TranslationStrings { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + + jurisdictionId String? @map("jurisdiction_id") @db.Uuid + language LanguagesEnum + site SiteEnum? + key String + value String + sourceHash String? @map("source_hash") + origin TranslationOrigin? + + jurisdictions Jurisdictions? @relation(fields: [jurisdictionId], references: [id], onDelete: NoAction, onUpdate: NoAction) + + @@unique([jurisdictionId, language, site, key]) + @@index([jurisdictionId, language, site]) + @@map("translation_strings") +} + model Translations { id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) @@ -2375,6 +2418,20 @@ enum LanguagesEnum { @@map("languages_enum") } +enum SiteEnum { + public + partners + + @@map("site_enum") +} + +enum TranslationOrigin { + machine + human + + @@map("translation_origin") +} + enum ListingEventsTypeEnum { openHouse publicLottery From 5d97638625d0d98a4deee2617f87c485d3e98bb4 Mon Sep 17 00:00:00 2001 From: Sean Albert Date: Fri, 17 Jul 2026 13:51:54 -0700 Subject: [PATCH 2/2] refactor(api): drop redundant translation_strings index, note unique-index nullability --- .../migrations/66_key_level_translation_content/migration.sql | 3 --- api/prisma/schema.prisma | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/api/prisma/migrations/66_key_level_translation_content/migration.sql b/api/prisma/migrations/66_key_level_translation_content/migration.sql index 4c3af275e8..a2b3898422 100644 --- a/api/prisma/migrations/66_key_level_translation_content/migration.sql +++ b/api/prisma/migrations/66_key_level_translation_content/migration.sql @@ -39,9 +39,6 @@ CREATE TABLE "translation_strings" ( -- CreateIndex CREATE UNIQUE INDEX "jurisdiction_content_jurisdiction_id_language_key" ON "jurisdiction_content"("jurisdiction_id", "language"); --- CreateIndex -CREATE INDEX "translation_strings_jurisdiction_id_language_site_idx" ON "translation_strings"("jurisdiction_id", "language", "site"); - -- CreateIndex -- NULLS NOT DISTINCT (hand-added; Prisma cannot express it in the schema) so base rows (null -- jurisdiction_id and site) and global Partners rows (null jurisdiction_id) cannot be duplicated. diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 30dd234793..b24effc367 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -1764,8 +1764,8 @@ model TranslationStrings { jurisdictions Jurisdictions? @relation(fields: [jurisdictionId], references: [id], onDelete: NoAction, onUpdate: NoAction) + // The unique index is NULLS NOT DISTINCT; set in the migration, as Prisma cannot express it here. @@unique([jurisdictionId, language, site, key]) - @@index([jurisdictionId, language, site]) @@map("translation_strings") }