feat: add i18n persistence model
This commit is contained in:
+2
-2
@@ -2,8 +2,8 @@ import "server-only";
|
|||||||
|
|
||||||
import Database from "better-sqlite3";
|
import Database from "better-sqlite3";
|
||||||
import { drizzle, type BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
|
import { drizzle, type BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
|
||||||
import { ensureDataDirectories, getServerConfig } from "@/server/config";
|
import { ensureDataDirectories, getServerConfig } from "../config";
|
||||||
import * as schema from "@/server/db/schema";
|
import * as schema from "./schema";
|
||||||
|
|
||||||
export type SqliteConnection = {
|
export type SqliteConnection = {
|
||||||
sqlite: Database.Database;
|
sqlite: Database.Database;
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
CREATE TABLE `content_translations` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`entity_type` text NOT NULL,
|
||||||
|
`entity_id` text NOT NULL,
|
||||||
|
`field` text NOT NULL,
|
||||||
|
`locale` text NOT NULL,
|
||||||
|
`value` text NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||||
|
CONSTRAINT "content_translations_entity_type_check" CHECK("content_translations"."entity_type" in ('branding', 'calendar_event', 'client', 'planning_section', 'project', 'task')),
|
||||||
|
CONSTRAINT "content_translations_field_check" CHECK(length("content_translations"."field") between 1 and 64),
|
||||||
|
CONSTRAINT "content_translations_locale_check" CHECK(length("content_translations"."locale") between 2 and 12)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `content_translations_entity_field_locale_unique` ON `content_translations` (`entity_type`,`entity_id`,`field`,`locale`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `content_translations_locale_idx` ON `content_translations` (`locale`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `content_translations_entity_locale_idx` ON `content_translations` (`entity_type`,`entity_id`,`locale`);--> statement-breakpoint
|
||||||
|
INSERT INTO `content_translations` (`entity_type`, `entity_id`, `field`, `locale`, `value`)
|
||||||
|
SELECT 'project', `id`, 'name', 'tr', `name` FROM `projects` WHERE coalesce(trim(`name`), '') != ''
|
||||||
|
ON CONFLICT(`entity_type`, `entity_id`, `field`, `locale`) DO UPDATE SET
|
||||||
|
`value` = excluded.`value`,
|
||||||
|
`updated_at` = cast(unixepoch('subsecond') * 1000 as integer);--> statement-breakpoint
|
||||||
|
INSERT INTO `content_translations` (`entity_type`, `entity_id`, `field`, `locale`, `value`)
|
||||||
|
SELECT 'project', `id`, 'description', 'tr', `description` FROM `projects` WHERE coalesce(trim(`description`), '') != ''
|
||||||
|
ON CONFLICT(`entity_type`, `entity_id`, `field`, `locale`) DO UPDATE SET
|
||||||
|
`value` = excluded.`value`,
|
||||||
|
`updated_at` = cast(unixepoch('subsecond') * 1000 as integer);--> statement-breakpoint
|
||||||
|
INSERT INTO `content_translations` (`entity_type`, `entity_id`, `field`, `locale`, `value`)
|
||||||
|
SELECT 'project', `id`, 'coverImageAlt', 'tr', `cover_image_alt` FROM `projects` WHERE coalesce(trim(`cover_image_alt`), '') != ''
|
||||||
|
ON CONFLICT(`entity_type`, `entity_id`, `field`, `locale`) DO UPDATE SET
|
||||||
|
`value` = excluded.`value`,
|
||||||
|
`updated_at` = cast(unixepoch('subsecond') * 1000 as integer);--> statement-breakpoint
|
||||||
|
INSERT INTO `content_translations` (`entity_type`, `entity_id`, `field`, `locale`, `value`)
|
||||||
|
SELECT 'task', `id`, 'title', 'tr', `title` FROM `tasks` WHERE coalesce(trim(`title`), '') != ''
|
||||||
|
ON CONFLICT(`entity_type`, `entity_id`, `field`, `locale`) DO UPDATE SET
|
||||||
|
`value` = excluded.`value`,
|
||||||
|
`updated_at` = cast(unixepoch('subsecond') * 1000 as integer);--> statement-breakpoint
|
||||||
|
INSERT INTO `content_translations` (`entity_type`, `entity_id`, `field`, `locale`, `value`)
|
||||||
|
SELECT 'task', `id`, 'description', 'tr', `description` FROM `tasks` WHERE coalesce(trim(`description`), '') != ''
|
||||||
|
ON CONFLICT(`entity_type`, `entity_id`, `field`, `locale`) DO UPDATE SET
|
||||||
|
`value` = excluded.`value`,
|
||||||
|
`updated_at` = cast(unixepoch('subsecond') * 1000 as integer);--> statement-breakpoint
|
||||||
|
INSERT INTO `content_translations` (`entity_type`, `entity_id`, `field`, `locale`, `value`)
|
||||||
|
SELECT 'planning_section', `id`, 'title', 'tr', `title` FROM `project_planning_sections` WHERE coalesce(trim(`title`), '') != ''
|
||||||
|
ON CONFLICT(`entity_type`, `entity_id`, `field`, `locale`) DO UPDATE SET
|
||||||
|
`value` = excluded.`value`,
|
||||||
|
`updated_at` = cast(unixepoch('subsecond') * 1000 as integer);--> statement-breakpoint
|
||||||
|
INSERT INTO `content_translations` (`entity_type`, `entity_id`, `field`, `locale`, `value`)
|
||||||
|
SELECT 'planning_section', `id`, 'content', 'tr', `content` FROM `project_planning_sections` WHERE coalesce(trim(`content`), '') != ''
|
||||||
|
ON CONFLICT(`entity_type`, `entity_id`, `field`, `locale`) DO UPDATE SET
|
||||||
|
`value` = excluded.`value`,
|
||||||
|
`updated_at` = cast(unixepoch('subsecond') * 1000 as integer);--> statement-breakpoint
|
||||||
|
INSERT INTO `content_translations` (`entity_type`, `entity_id`, `field`, `locale`, `value`)
|
||||||
|
SELECT 'branding', `id`, 'portalWelcome', 'tr', `portal_welcome_text` FROM `instance_branding` WHERE coalesce(trim(`portal_welcome_text`), '') != ''
|
||||||
|
ON CONFLICT(`entity_type`, `entity_id`, `field`, `locale`) DO UPDATE SET
|
||||||
|
`value` = excluded.`value`,
|
||||||
|
`updated_at` = cast(unixepoch('subsecond') * 1000 as integer);--> statement-breakpoint
|
||||||
|
INSERT INTO `content_translations` (`entity_type`, `entity_id`, `field`, `locale`, `value`)
|
||||||
|
SELECT 'branding', `id`, 'portalFooter', 'tr', `portal_footer_text` FROM `instance_branding` WHERE coalesce(trim(`portal_footer_text`), '') != ''
|
||||||
|
ON CONFLICT(`entity_type`, `entity_id`, `field`, `locale`) DO UPDATE SET
|
||||||
|
`value` = excluded.`value`,
|
||||||
|
`updated_at` = cast(unixepoch('subsecond') * 1000 as integer);--> statement-breakpoint
|
||||||
|
CREATE TABLE `instance_i18n_settings` (
|
||||||
|
`key` text PRIMARY KEY DEFAULT 'default' NOT NULL,
|
||||||
|
`default_locale` text DEFAULT 'tr' NOT NULL,
|
||||||
|
`catalog_version` integer DEFAULT 1 NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `instance_locales` (
|
||||||
|
`code` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`native_name` text NOT NULL,
|
||||||
|
`status` text DEFAULT 'draft' NOT NULL,
|
||||||
|
`fallback_locale` text,
|
||||||
|
`text_direction` text DEFAULT 'ltr' NOT NULL,
|
||||||
|
`built_in` integer DEFAULT false NOT NULL,
|
||||||
|
`sort_order` integer DEFAULT 0 NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||||
|
CONSTRAINT "instance_locales_code_check" CHECK("instance_locales"."code" glob '[a-z][a-z]' or "instance_locales"."code" glob '[a-z][a-z]-[A-Z][A-Z]' or "instance_locales"."code" glob '[a-z][a-z]-[A-Z][A-Z][0-9]'),
|
||||||
|
CONSTRAINT "instance_locales_status_check" CHECK("instance_locales"."status" in ('draft', 'active', 'archived', 'test')),
|
||||||
|
CONSTRAINT "instance_locales_direction_check" CHECK("instance_locales"."text_direction" in ('ltr', 'rtl')),
|
||||||
|
CONSTRAINT "instance_locales_not_self_fallback_check" CHECK("instance_locales"."fallback_locale" is null or "instance_locales"."fallback_locale" != "instance_locales"."code")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX `instance_locales_status_idx` ON `instance_locales` (`status`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `instance_locales_fallback_idx` ON `instance_locales` (`fallback_locale`);--> statement-breakpoint
|
||||||
|
INSERT INTO `instance_locales` (`code`, `name`, `native_name`, `status`, `fallback_locale`, `text_direction`, `built_in`, `sort_order`)
|
||||||
|
VALUES
|
||||||
|
('tr', 'Turkish', 'Türkçe', 'active', null, 'ltr', 1, 10),
|
||||||
|
('en', 'English', 'English', 'active', 'tr', 'ltr', 1, 20)
|
||||||
|
ON CONFLICT(`code`) DO UPDATE SET
|
||||||
|
`name` = excluded.`name`,
|
||||||
|
`native_name` = excluded.`native_name`,
|
||||||
|
`status` = excluded.`status`,
|
||||||
|
`fallback_locale` = excluded.`fallback_locale`,
|
||||||
|
`text_direction` = excluded.`text_direction`,
|
||||||
|
`built_in` = excluded.`built_in`,
|
||||||
|
`sort_order` = excluded.`sort_order`,
|
||||||
|
`updated_at` = cast(unixepoch('subsecond') * 1000 as integer);--> statement-breakpoint
|
||||||
|
CREATE TABLE `instance_ui_translations` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`locale` text NOT NULL,
|
||||||
|
`namespace` text NOT NULL,
|
||||||
|
`translation_key` text NOT NULL,
|
||||||
|
`value` text NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||||
|
CONSTRAINT "instance_ui_translations_locale_check" CHECK(length("instance_ui_translations"."locale") between 2 and 12),
|
||||||
|
CONSTRAINT "instance_ui_translations_namespace_check" CHECK(length("instance_ui_translations"."namespace") between 1 and 64),
|
||||||
|
CONSTRAINT "instance_ui_translations_key_check" CHECK(length("instance_ui_translations"."translation_key") between 1 and 160)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `instance_ui_translations_locale_key_unique` ON `instance_ui_translations` (`locale`,`namespace`,`translation_key`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `instance_ui_translations_locale_namespace_idx` ON `instance_ui_translations` (`locale`,`namespace`);--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_user_preferences` (
|
||||||
|
`owner_user_id` text PRIMARY KEY NOT NULL,
|
||||||
|
`timezone` text DEFAULT 'Europe/Istanbul' NOT NULL,
|
||||||
|
`default_currency` text DEFAULT 'TRY' NOT NULL,
|
||||||
|
`language` text DEFAULT 'tr' NOT NULL,
|
||||||
|
`date_format` text DEFAULT 'dd.MM.yyyy' NOT NULL,
|
||||||
|
`color_mode` text DEFAULT 'system' NOT NULL,
|
||||||
|
`sidebar_collapsed` integer DEFAULT false NOT NULL,
|
||||||
|
`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
`updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
FOREIGN KEY (`owner_user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
CONSTRAINT "user_preferences_currency_check" CHECK(length("__new_user_preferences"."default_currency") = 3),
|
||||||
|
CONSTRAINT "user_preferences_language_check" CHECK(length("__new_user_preferences"."language") between 2 and 12),
|
||||||
|
CONSTRAINT "user_preferences_color_mode_check" CHECK("__new_user_preferences"."color_mode" in ('light', 'dark', 'system'))
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_user_preferences`("owner_user_id", "timezone", "default_currency", "language", "date_format", "color_mode", "sidebar_collapsed", "created_at", "updated_at") SELECT "owner_user_id", "timezone", "default_currency", "language", "date_format", "color_mode", "sidebar_collapsed", "created_at", "updated_at" FROM `user_preferences`;--> statement-breakpoint
|
||||||
|
DROP TABLE `user_preferences`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_user_preferences` RENAME TO `user_preferences`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||||
|
UPDATE `user_preferences`
|
||||||
|
SET `language` = 'tr'
|
||||||
|
WHERE length(`language`) < 2 OR length(`language`) > 12;--> statement-breakpoint
|
||||||
|
INSERT INTO `instance_i18n_settings` (`key`, `default_locale`, `catalog_version`)
|
||||||
|
VALUES ('default', 'tr', 1)
|
||||||
|
ON CONFLICT(`key`) DO UPDATE SET
|
||||||
|
`default_locale` = CASE
|
||||||
|
WHEN `instance_i18n_settings`.`default_locale` IS NULL OR `instance_i18n_settings`.`default_locale` = '' THEN excluded.`default_locale`
|
||||||
|
ELSE `instance_i18n_settings`.`default_locale`
|
||||||
|
END,
|
||||||
|
`catalog_version` = max(`instance_i18n_settings`.`catalog_version`, excluded.`catalog_version`),
|
||||||
|
`updated_at` = cast(unixepoch('subsecond') * 1000 as integer);--> statement-breakpoint
|
||||||
|
ALTER TABLE `portal_invitations` ADD `locale` text DEFAULT 'tr' NOT NULL;--> statement-breakpoint
|
||||||
|
CREATE INDEX `portal_invitations_locale_idx` ON `portal_invitations` (`locale`);--> statement-breakpoint
|
||||||
|
ALTER TABLE `clients` ADD `portal_locale` text;--> statement-breakpoint
|
||||||
|
CREATE INDEX `clients_portal_locale_idx` ON `clients` (`portal_locale`);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,13 @@
|
|||||||
"when": 1784268984532,
|
"when": 1784268984532,
|
||||||
"tag": "0007_flaky_kinsey_walden",
|
"tag": "0007_flaky_kinsey_walden",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1784413854972,
|
||||||
|
"tag": "0008_wild_mercury",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -125,6 +125,7 @@ export const portalInvitations = sqliteTable(
|
|||||||
tokenHash: text("token_hash").notNull(),
|
tokenHash: text("token_hash").notNull(),
|
||||||
clientId: text("client_id").notNull(),
|
clientId: text("client_id").notNull(),
|
||||||
email: text("email").notNull(),
|
email: text("email").notNull(),
|
||||||
|
locale: text("locale").default("tr").notNull(),
|
||||||
status: text("status").$type<PortalInvitationStatus>()
|
status: text("status").$type<PortalInvitationStatus>()
|
||||||
.default("pending")
|
.default("pending")
|
||||||
.notNull(),
|
.notNull(),
|
||||||
@@ -137,6 +138,7 @@ export const portalInvitations = sqliteTable(
|
|||||||
uniqueIndex("portal_invitations_token_hash_unique").on(table.tokenHash),
|
uniqueIndex("portal_invitations_token_hash_unique").on(table.tokenHash),
|
||||||
index("portal_invitations_client_id_idx").on(table.clientId),
|
index("portal_invitations_client_id_idx").on(table.clientId),
|
||||||
index("portal_invitations_email_idx").on(table.email),
|
index("portal_invitations_email_idx").on(table.email),
|
||||||
|
index("portal_invitations_locale_idx").on(table.locale),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const clients = sqliteTable(
|
|||||||
email: text("email"),
|
email: text("email"),
|
||||||
phone: text("phone"),
|
phone: text("phone"),
|
||||||
website: text("website"),
|
website: text("website"),
|
||||||
|
portalLocale: text("portal_locale"),
|
||||||
status: text("status").$type<ClientStatus>().default("active").notNull(),
|
status: text("status").$type<ClientStatus>().default("active").notNull(),
|
||||||
pipelineStage: text("pipeline_stage").$type<ClientPipelineStage>().default("lead").notNull(),
|
pipelineStage: text("pipeline_stage").$type<ClientPipelineStage>().default("lead").notNull(),
|
||||||
nextFollowUpDate: text("next_follow_up_date"),
|
nextFollowUpDate: text("next_follow_up_date"),
|
||||||
@@ -60,6 +61,7 @@ export const clients = sqliteTable(
|
|||||||
index("clients_owner_user_id_idx").on(table.ownerUserId),
|
index("clients_owner_user_id_idx").on(table.ownerUserId),
|
||||||
index("clients_owner_status_idx").on(table.ownerUserId, table.status),
|
index("clients_owner_status_idx").on(table.ownerUserId, table.status),
|
||||||
index("clients_owner_pipeline_idx").on(table.ownerUserId, table.pipelineStage),
|
index("clients_owner_pipeline_idx").on(table.ownerUserId, table.pipelineStage),
|
||||||
|
index("clients_portal_locale_idx").on(table.portalLocale),
|
||||||
index("clients_next_follow_up_date_idx").on(table.nextFollowUpDate),
|
index("clients_next_follow_up_date_idx").on(table.nextFollowUpDate),
|
||||||
check("clients_status_check", sql`${table.status} in ('active', 'paused', 'archived')`),
|
check("clients_status_check", sql`${table.status} in ('active', 'paused', 'archived')`),
|
||||||
check(
|
check(
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
check,
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
sqliteTable,
|
||||||
|
text,
|
||||||
|
uniqueIndex,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
export type LocaleStatus = "draft" | "active" | "archived" | "test";
|
||||||
|
export type TextDirection = "ltr" | "rtl";
|
||||||
|
export type TranslationEntityType =
|
||||||
|
| "branding"
|
||||||
|
| "calendar_event"
|
||||||
|
| "client"
|
||||||
|
| "planning_section"
|
||||||
|
| "project"
|
||||||
|
| "task";
|
||||||
|
|
||||||
|
const nowMs = sql`(cast(unixepoch('subsecond') * 1000 as integer))`;
|
||||||
|
|
||||||
|
export const instanceLocales = sqliteTable(
|
||||||
|
"instance_locales",
|
||||||
|
{
|
||||||
|
code: text("code").primaryKey(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
nativeName: text("native_name").notNull(),
|
||||||
|
status: text("status").$type<LocaleStatus>().default("draft").notNull(),
|
||||||
|
fallbackLocale: text("fallback_locale"),
|
||||||
|
textDirection: text("text_direction").$type<TextDirection>().default("ltr").notNull(),
|
||||||
|
builtIn: integer("built_in", { mode: "boolean" }).default(false).notNull(),
|
||||||
|
sortOrder: integer("sort_order").default(0).notNull(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).default(nowMs).notNull(),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||||
|
.default(nowMs)
|
||||||
|
.$onUpdate(() => new Date())
|
||||||
|
.notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index("instance_locales_status_idx").on(table.status),
|
||||||
|
index("instance_locales_fallback_idx").on(table.fallbackLocale),
|
||||||
|
check(
|
||||||
|
"instance_locales_code_check",
|
||||||
|
sql`${table.code} glob '[a-z][a-z]' or ${table.code} glob '[a-z][a-z]-[A-Z][A-Z]' or ${table.code} glob '[a-z][a-z]-[A-Z][A-Z][0-9]'`,
|
||||||
|
),
|
||||||
|
check("instance_locales_status_check", sql`${table.status} in ('draft', 'active', 'archived', 'test')`),
|
||||||
|
check("instance_locales_direction_check", sql`${table.textDirection} in ('ltr', 'rtl')`),
|
||||||
|
check("instance_locales_not_self_fallback_check", sql`${table.fallbackLocale} is null or ${table.fallbackLocale} != ${table.code}`),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const instanceI18nSettings = sqliteTable("instance_i18n_settings", {
|
||||||
|
key: text("key").primaryKey().default("default").notNull(),
|
||||||
|
defaultLocale: text("default_locale").default("tr").notNull(),
|
||||||
|
catalogVersion: integer("catalog_version").default(1).notNull(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).default(nowMs).notNull(),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||||
|
.default(nowMs)
|
||||||
|
.$onUpdate(() => new Date())
|
||||||
|
.notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const instanceUiTranslations = sqliteTable(
|
||||||
|
"instance_ui_translations",
|
||||||
|
{
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
locale: text("locale").notNull(),
|
||||||
|
namespace: text("namespace").notNull(),
|
||||||
|
translationKey: text("translation_key").notNull(),
|
||||||
|
value: text("value").notNull(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).default(nowMs).notNull(),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||||
|
.default(nowMs)
|
||||||
|
.$onUpdate(() => new Date())
|
||||||
|
.notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
uniqueIndex("instance_ui_translations_locale_key_unique").on(
|
||||||
|
table.locale,
|
||||||
|
table.namespace,
|
||||||
|
table.translationKey,
|
||||||
|
),
|
||||||
|
index("instance_ui_translations_locale_namespace_idx").on(table.locale, table.namespace),
|
||||||
|
check("instance_ui_translations_locale_check", sql`length(${table.locale}) between 2 and 12`),
|
||||||
|
check("instance_ui_translations_namespace_check", sql`length(${table.namespace}) between 1 and 64`),
|
||||||
|
check("instance_ui_translations_key_check", sql`length(${table.translationKey}) between 1 and 160`),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const contentTranslations = sqliteTable(
|
||||||
|
"content_translations",
|
||||||
|
{
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
entityType: text("entity_type").$type<TranslationEntityType>().notNull(),
|
||||||
|
entityId: text("entity_id").notNull(),
|
||||||
|
field: text("field").notNull(),
|
||||||
|
locale: text("locale").notNull(),
|
||||||
|
value: text("value").notNull(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).default(nowMs).notNull(),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||||
|
.default(nowMs)
|
||||||
|
.$onUpdate(() => new Date())
|
||||||
|
.notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
uniqueIndex("content_translations_entity_field_locale_unique").on(
|
||||||
|
table.entityType,
|
||||||
|
table.entityId,
|
||||||
|
table.field,
|
||||||
|
table.locale,
|
||||||
|
),
|
||||||
|
index("content_translations_locale_idx").on(table.locale),
|
||||||
|
index("content_translations_entity_locale_idx").on(table.entityType, table.entityId, table.locale),
|
||||||
|
check(
|
||||||
|
"content_translations_entity_type_check",
|
||||||
|
sql`${table.entityType} in ('branding', 'calendar_event', 'client', 'planning_section', 'project', 'task')`,
|
||||||
|
),
|
||||||
|
check("content_translations_field_check", sql`length(${table.field}) between 1 and 64`),
|
||||||
|
check("content_translations_locale_check", sql`length(${table.locale}) between 2 and 12`),
|
||||||
|
],
|
||||||
|
);
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from "./auth";
|
export * from "./auth";
|
||||||
export * from "./domain";
|
export * from "./domain";
|
||||||
|
export * from "./i18n";
|
||||||
export * from "./runtime";
|
export * from "./runtime";
|
||||||
export * from "./settings";
|
export * from "./settings";
|
||||||
export * from "./storage";
|
export * from "./storage";
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const userPreferences = sqliteTable(
|
|||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
check("user_preferences_currency_check", sql`length(${table.defaultCurrency}) = 3`),
|
check("user_preferences_currency_check", sql`length(${table.defaultCurrency}) = 3`),
|
||||||
check("user_preferences_language_check", sql`${table.language} in ('tr', 'en')`),
|
check("user_preferences_language_check", sql`length(${table.language}) between 2 and 12`),
|
||||||
check("user_preferences_color_mode_check", sql`${table.colorMode} in ('light', 'dark', 'system')`),
|
check("user_preferences_color_mode_check", sql`${table.colorMode} in ('light', 'dark', 'system')`),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export type DomainErrorCode =
|
export type DomainErrorCode =
|
||||||
| "VALIDATION_ERROR"
|
| "VALIDATION_ERROR"
|
||||||
|
| "UNSUPPORTED_LOCALE"
|
||||||
| "UNAUTHENTICATED"
|
| "UNAUTHENTICATED"
|
||||||
| "FORBIDDEN"
|
| "FORBIDDEN"
|
||||||
| "NOT_FOUND"
|
| "NOT_FOUND"
|
||||||
@@ -11,6 +12,7 @@ export type DomainErrorCode =
|
|||||||
|
|
||||||
const statusByCode: Record<DomainErrorCode, number> = {
|
const statusByCode: Record<DomainErrorCode, number> = {
|
||||||
VALIDATION_ERROR: 400,
|
VALIDATION_ERROR: 400,
|
||||||
|
UNSUPPORTED_LOCALE: 400,
|
||||||
UNAUTHENTICATED: 401,
|
UNAUTHENTICATED: 401,
|
||||||
FORBIDDEN: 403,
|
FORBIDDEN: 403,
|
||||||
NOT_FOUND: 404,
|
NOT_FOUND: 404,
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
instanceUiTranslations,
|
||||||
|
clients,
|
||||||
|
instanceI18nSettings,
|
||||||
|
instanceLocales,
|
||||||
|
portalInvitations,
|
||||||
|
userPreferences,
|
||||||
|
} from "../db/schema";
|
||||||
|
import type { DomainDatabase } from "../domain/database";
|
||||||
|
|
||||||
|
const I18N_SETTINGS_KEY = "default";
|
||||||
|
|
||||||
|
export function createI18nRepository(db: DomainDatabase) {
|
||||||
|
return {
|
||||||
|
listLocales: () => db.select().from(instanceLocales).orderBy(instanceLocales.sortOrder, instanceLocales.code).all(),
|
||||||
|
getLocale: (code: string) =>
|
||||||
|
db.select().from(instanceLocales).where(eq(instanceLocales.code, code)).get(),
|
||||||
|
createLocale: (value: typeof instanceLocales.$inferInsert) =>
|
||||||
|
db.insert(instanceLocales).values(value).returning().get(),
|
||||||
|
updateLocale: (code: string, value: Partial<typeof instanceLocales.$inferInsert>) =>
|
||||||
|
db.update(instanceLocales).set(value).where(eq(instanceLocales.code, code)).returning().get(),
|
||||||
|
getSettings: () =>
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(instanceI18nSettings)
|
||||||
|
.where(eq(instanceI18nSettings.key, I18N_SETTINGS_KEY))
|
||||||
|
.get(),
|
||||||
|
createSettingsIfMissing: () =>
|
||||||
|
db
|
||||||
|
.insert(instanceI18nSettings)
|
||||||
|
.values({ key: I18N_SETTINGS_KEY, defaultLocale: "tr", catalogVersion: 1 })
|
||||||
|
.onConflictDoNothing({ target: instanceI18nSettings.key })
|
||||||
|
.run(),
|
||||||
|
updateDefaultLocale: (code: string) =>
|
||||||
|
db
|
||||||
|
.update(instanceI18nSettings)
|
||||||
|
.set({
|
||||||
|
defaultLocale: code,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(instanceI18nSettings.key, I18N_SETTINGS_KEY))
|
||||||
|
.returning()
|
||||||
|
.get(),
|
||||||
|
listUiTranslations: (locale: string, namespaces: readonly string[]) => {
|
||||||
|
if (namespaces.length === 0) return [];
|
||||||
|
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
locale: instanceUiTranslations.locale,
|
||||||
|
namespace: instanceUiTranslations.namespace,
|
||||||
|
translationKey: instanceUiTranslations.translationKey,
|
||||||
|
value: instanceUiTranslations.value,
|
||||||
|
})
|
||||||
|
.from(instanceUiTranslations)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(instanceUiTranslations.locale, locale),
|
||||||
|
inArray(instanceUiTranslations.namespace, [...namespaces]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
},
|
||||||
|
listAllUiTranslations: () =>
|
||||||
|
db
|
||||||
|
.select({
|
||||||
|
locale: instanceUiTranslations.locale,
|
||||||
|
namespace: instanceUiTranslations.namespace,
|
||||||
|
translationKey: instanceUiTranslations.translationKey,
|
||||||
|
value: instanceUiTranslations.value,
|
||||||
|
})
|
||||||
|
.from(instanceUiTranslations)
|
||||||
|
.orderBy(
|
||||||
|
instanceUiTranslations.locale,
|
||||||
|
instanceUiTranslations.namespace,
|
||||||
|
instanceUiTranslations.translationKey,
|
||||||
|
)
|
||||||
|
.all(),
|
||||||
|
upsertUiTranslation: (value: typeof instanceUiTranslations.$inferInsert) =>
|
||||||
|
db
|
||||||
|
.insert(instanceUiTranslations)
|
||||||
|
.values(value)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: [
|
||||||
|
instanceUiTranslations.locale,
|
||||||
|
instanceUiTranslations.namespace,
|
||||||
|
instanceUiTranslations.translationKey,
|
||||||
|
],
|
||||||
|
set: {
|
||||||
|
value: value.value,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.run(),
|
||||||
|
deleteUiTranslation: (locale: string, namespace: string, translationKey: string) =>
|
||||||
|
db
|
||||||
|
.delete(instanceUiTranslations)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(instanceUiTranslations.locale, locale),
|
||||||
|
eq(instanceUiTranslations.namespace, namespace),
|
||||||
|
eq(instanceUiTranslations.translationKey, translationKey),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.run(),
|
||||||
|
bumpCatalogVersion: () =>
|
||||||
|
db
|
||||||
|
.update(instanceI18nSettings)
|
||||||
|
.set({
|
||||||
|
catalogVersion: sql`${instanceI18nSettings.catalogVersion} + 1`,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(instanceI18nSettings.key, I18N_SETTINGS_KEY))
|
||||||
|
.run(),
|
||||||
|
countLocaleReferences: (code: string) => ({
|
||||||
|
defaultSettings: Number(
|
||||||
|
db
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(instanceI18nSettings)
|
||||||
|
.where(eq(instanceI18nSettings.defaultLocale, code))
|
||||||
|
.get()?.count ?? 0,
|
||||||
|
),
|
||||||
|
fallbacks: Number(
|
||||||
|
db
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(instanceLocales)
|
||||||
|
.where(eq(instanceLocales.fallbackLocale, code))
|
||||||
|
.get()?.count ?? 0,
|
||||||
|
),
|
||||||
|
userPreferences: Number(
|
||||||
|
db
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(userPreferences)
|
||||||
|
.where(eq(userPreferences.language, code))
|
||||||
|
.get()?.count ?? 0,
|
||||||
|
),
|
||||||
|
clients: Number(
|
||||||
|
db
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.portalLocale, code))
|
||||||
|
.get()?.count ?? 0,
|
||||||
|
),
|
||||||
|
portalInvitations: Number(
|
||||||
|
db
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(portalInvitations)
|
||||||
|
.where(eq(portalInvitations.locale, code))
|
||||||
|
.get()?.count ?? 0,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user