@better-i18n/schemas 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,33 +1,32 @@
1
1
  {
2
2
  "name": "@better-i18n/schemas",
3
- "version": "0.2.1",
4
- "description": "Public schemas and types for Better i18n SDK and MCP",
3
+ "version": "0.2.3",
4
+ "description": "Shared validation schemas for API and frontend",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
8
  "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "bun": "./src/index.ts",
12
- "default": "./dist/index.js"
13
- },
14
- "./ai-models": {
15
- "types": "./dist/ai-models.d.ts",
16
- "bun": "./src/ai-models.ts",
17
- "default": "./dist/ai-models.js"
18
- }
9
+ ".": "./src/index.ts",
10
+ "./projects": "./src/projects/index.ts",
11
+ "./organizations": "./src/organizations/index.ts",
12
+ "./languages": "./src/languages/index.ts",
13
+ "./github": "./src/github.ts",
14
+ "./translations": "./src/translations.ts"
19
15
  },
20
16
  "files": [
21
- "dist"
17
+ "src"
22
18
  ],
23
19
  "scripts": {
24
- "build": "tsc",
25
- "typecheck": "tsc --noEmit",
26
- "clean": "rm -rf dist",
27
- "prepublishOnly": "bun run build"
20
+ "type-check": "tsc --noEmit"
21
+ },
22
+ "dependencies": {
23
+ "zod": "^3.25.76"
28
24
  },
29
25
  "devDependencies": {
30
26
  "typescript": "~5.9.2"
31
27
  },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
32
31
  "license": "MIT"
33
32
  }
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Shared AI Model Configuration
3
+ * Single source of truth for all AI model definitions used across frontend and backend.
4
+ */
5
+
6
+ // Provider types
7
+ export type ModelProvider =
8
+ | "better-ai"
9
+ | "openai"
10
+ | "gemini"
11
+ | "claude"
12
+ | "openrouter"
13
+ | "azure-openai";
14
+
15
+ // Model categories
16
+ export type ModelCategory =
17
+ | "flagship"
18
+ | "reasoning"
19
+ | "performance"
20
+ | "specialized"
21
+ | "legacy";
22
+
23
+ // Icon types for models
24
+ export type ModelIcon = "OpenAI" | "Gemini" | "Claude" | "BetterAI" | "AzureOpenAI";
25
+
26
+ // Model color configuration
27
+ export interface ModelColors {
28
+ /** Background color for UI elements */
29
+ background: string;
30
+ /** Hover background color */
31
+ hover: string;
32
+ /** Icon/text color */
33
+ icon: string;
34
+ }
35
+
36
+ // Complete model configuration
37
+ export interface AIModelConfig {
38
+ /** UI display ID (e.g., "gpt-5.2", "gemini-3-flash") */
39
+ id: string;
40
+ /** Display name for UI (e.g., "ChatGPT 5.2", "Gemini 3 Flash") */
41
+ name: string;
42
+ /** Provider identifier */
43
+ provider: ModelProvider;
44
+ /** Icon to display for this model (from lucide-react or custom) */
45
+ icon: ModelIcon;
46
+ /** Colors for UI elements */
47
+ colors: ModelColors;
48
+ /** Actual API model ID to send to provider SDK (e.g., "gpt-5.2", "gemini-2.5-flash") */
49
+ apiModelId: string;
50
+ /** Max context window in tokens */
51
+ contextSize: number;
52
+ /** Max output tokens */
53
+ maxOutput: number;
54
+ /** Model description */
55
+ description: string;
56
+ /** Optional badge (e.g., "New", "Fast", "Default") */
57
+ badge?: string;
58
+ /** Is this the default model? */
59
+ isDefault?: boolean;
60
+ /** Release date info */
61
+ releaseDate?: string;
62
+ /** Training data cutoff */
63
+ trainingData?: string;
64
+ /** Speed score 0-100 */
65
+ speed?: number;
66
+ /** Model category */
67
+ category?: ModelCategory;
68
+ /** Whether this model requires user's own API key (not platform key) */
69
+ requiresUserKey?: boolean;
70
+ }
71
+
72
+ // Provider configuration (for UI)
73
+ export interface ProviderInfo {
74
+ id: ModelProvider;
75
+ name: string;
76
+ brandColor: string;
77
+ }
78
+
79
+ /**
80
+ * All available AI models - Single Source of Truth
81
+ *
82
+ * Important: `apiModelId` is the actual model ID sent to the provider's SDK.
83
+ * This may differ from `id` which is used for UI/selection purposes.
84
+ *
85
+ * We keep only the best/flagship models from each provider to avoid clutter.
86
+ */
87
+ export const AI_MODELS: AIModelConfig[] = [
88
+ // ============================================
89
+ // Better AI - Platform Default (Powered by Gemini 3.1 Pro Preview)
90
+ // ============================================
91
+ {
92
+ id: "better-ai",
93
+ name: "Better AI",
94
+ provider: "better-ai",
95
+ icon: "BetterAI",
96
+ colors: {
97
+ background: "bg-gray-50 dark:bg-neutral-700",
98
+ hover: "hover:bg-gray-100 dark:hover:bg-neutral-600",
99
+ icon: "text-gray-700 dark:text-white",
100
+ },
101
+ apiModelId: "gemini-2.5-flash",
102
+ contextSize: 1_048_576, // 1M tokens
103
+ maxOutput: 65_536,
104
+ description:
105
+ "Powerful AI model powered by Gemini 2.5 Preview. Great for translations.",
106
+ isDefault: true,
107
+ badge: "Default",
108
+ releaseDate: "Jan 2025",
109
+ trainingData: "Jan 2025",
110
+ speed: 95,
111
+ category: "flagship",
112
+ requiresUserKey: false,
113
+ },
114
+
115
+ // ============================================
116
+ // Azure OpenAI Models (via Azure credits)
117
+ // ============================================
118
+ {
119
+ id: "gpt-5.3",
120
+ name: "GPT 5.3",
121
+ provider: "azure-openai",
122
+ icon: "AzureOpenAI",
123
+ colors: {
124
+ background: "bg-gray-100 dark:bg-neutral-800",
125
+ hover: "hover:bg-gray-200 dark:hover:bg-neutral-700",
126
+ icon: "text-gray-700 dark:text-neutral-300",
127
+ },
128
+ apiModelId: "gpt-5.3-chat",
129
+ contextSize: 1_047_576,
130
+ maxOutput: 128_000,
131
+ description: "OpenAI's latest and most capable model via Azure.",
132
+ badge: "New",
133
+ releaseDate: "2025",
134
+ trainingData: "2025",
135
+ speed: 90,
136
+ category: "flagship",
137
+ requiresUserKey: false,
138
+ },
139
+ {
140
+ id: "gpt-5.2",
141
+ name: "GPT 5.2",
142
+ provider: "azure-openai",
143
+ icon: "AzureOpenAI",
144
+ colors: {
145
+ background: "bg-gray-100 dark:bg-neutral-800",
146
+ hover: "hover:bg-gray-200 dark:hover:bg-neutral-700",
147
+ icon: "text-gray-700 dark:text-neutral-300",
148
+ },
149
+ apiModelId: "gpt-5.2-chat",
150
+ contextSize: 1_047_576,
151
+ maxOutput: 128_000,
152
+ description: "OpenAI GPT 5.2 via Azure OpenAI.",
153
+ badge: "Fast",
154
+ releaseDate: "2025",
155
+ trainingData: "2025",
156
+ speed: 95,
157
+ category: "flagship",
158
+ requiresUserKey: false,
159
+ },
160
+
161
+ // ============================================
162
+ // Google Gemini Models
163
+ // ============================================
164
+ {
165
+ id: "gemini-3-pro",
166
+ name: "Gemini 3 Pro",
167
+ provider: "gemini",
168
+ icon: "Gemini",
169
+ colors: {
170
+ background: "bg-blue-50 dark:bg-blue-700",
171
+ hover: "hover:bg-blue-100 dark:hover:bg-blue-600",
172
+ icon: "text-blue-500 dark:text-blue-100",
173
+ },
174
+ apiModelId: "gemini-3-pro-preview",
175
+ contextSize: 1_048_576, // 1M tokens (verified from Google docs)
176
+ maxOutput: 65_536,
177
+ description: "Google's most capable model. Best for complex tasks.",
178
+ badge: "Pro",
179
+ releaseDate: "Jan 2025",
180
+ trainingData: "Jan 2025",
181
+ speed: 90,
182
+ category: "flagship",
183
+ requiresUserKey: false,
184
+ },
185
+ ];
186
+
187
+ // ============================================
188
+ // Helper Functions
189
+ // ============================================
190
+
191
+ /**
192
+ * Get model config by ID
193
+ */
194
+ export function getModelById(modelId: string): AIModelConfig | undefined {
195
+ return AI_MODELS.find((m) => m.id === modelId);
196
+ }
197
+
198
+ /**
199
+ * Get the actual API model ID for a given UI model ID
200
+ */
201
+ export function getApiModelId(modelId: string): string {
202
+ const model = getModelById(modelId);
203
+ return model?.apiModelId ?? modelId;
204
+ }
205
+
206
+ /**
207
+ * Get provider for a model ID
208
+ */
209
+ export function getModelProvider(modelId: string): ModelProvider {
210
+ const model = getModelById(modelId);
211
+ return model?.provider ?? "better-ai";
212
+ }
213
+
214
+ /**
215
+ * Get default model
216
+ */
217
+ export function getDefaultModel(): AIModelConfig {
218
+ return AI_MODELS.find((m) => m.isDefault) ?? AI_MODELS[0];
219
+ }
220
+
221
+ /**
222
+ * Get models by provider
223
+ */
224
+ export function getModelsByProvider(provider: ModelProvider): AIModelConfig[] {
225
+ return AI_MODELS.filter((m) => m.provider === provider);
226
+ }
227
+
228
+ /**
229
+ * Get models that don't require user API key (platform-provided)
230
+ */
231
+ export function getPlatformModels(): AIModelConfig[] {
232
+ return AI_MODELS.filter((m) => !m.requiresUserKey);
233
+ }
234
+
235
+ /**
236
+ * Check if a model requires user's own API key
237
+ */
238
+ export function modelRequiresUserKey(modelId: string): boolean {
239
+ const model = getModelById(modelId);
240
+ return model?.requiresUserKey ?? false;
241
+ }
242
+
243
+ /**
244
+ * Format context size for display (e.g., "128K", "2M")
245
+ */
246
+ export function formatContextSize(tokens: number): string {
247
+ if (tokens >= 1_000_000) {
248
+ return `${(tokens / 1_000_000).toFixed(0)}M`;
249
+ }
250
+ return `${(tokens / 1_000).toFixed(0)}K`;
251
+ }
package/src/content.ts ADDED
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Zod validation schemas for Content Model CRUD operations.
3
+ *
4
+ * Content models are user-defined content types (blog, changelog, legal, etc.)
5
+ * with custom field definitions. These schemas validate all API inputs.
6
+ */
7
+
8
+ import { z } from "zod";
9
+
10
+ // —————————————————————————————————————————————————————————————————————————————
11
+ // Shared enums
12
+ // —————————————————————————————————————————————————————————————————————————————
13
+
14
+ export const contentModelFieldTypeEnum = z.enum([
15
+ "text",
16
+ "textarea",
17
+ "richtext",
18
+ "number",
19
+ "boolean",
20
+ "date",
21
+ "datetime",
22
+ "enum",
23
+ "media",
24
+ "relation",
25
+ ]);
26
+
27
+ export const contentModelKindEnum = z.enum(["collection", "single"]);
28
+
29
+ // —————————————————————————————————————————————————————————————————————————————
30
+ // Field-level config schemas (must precede model schemas that reference them)
31
+ // —————————————————————————————————————————————————————————————————————————————
32
+
33
+ /** Single enum option (label displayed to users, value stored in DB) */
34
+ export const enumValueItemSchema = z.object({
35
+ label: z.string().min(1).max(200),
36
+ value: z.string().min(1).max(200),
37
+ });
38
+
39
+ /** Extensible field-level options (Unsplash, AI generation, enum values, etc.) */
40
+ export const fieldOptionsSchema = z.object({
41
+ unsplash: z.object({ enabled: z.boolean() }).optional(),
42
+ aiGeneration: z.object({
43
+ enabled: z.boolean(),
44
+ prompt: z.string().max(2000).optional(),
45
+ model: z.string().max(100).optional(),
46
+ }).optional(),
47
+ enumValues: z.array(enumValueItemSchema).max(200).optional(),
48
+ showInTable: z.boolean().optional(),
49
+ }).optional();
50
+
51
+ /** Type-specific field configuration (e.g., targetModel for relation fields) */
52
+ export const fieldConfigSchema = z.object({
53
+ targetModel: z.string().optional(),
54
+ }).optional();
55
+
56
+ // —————————————————————————————————————————————————————————————————————————————
57
+ // Model schemas
58
+ // —————————————————————————————————————————————————————————————————————————————
59
+
60
+ /** List all content models for a project */
61
+ export const listContentModelsSchema = z.object({
62
+ projectId: z.string().uuid(),
63
+ });
64
+
65
+ /** Get a single content model by slug */
66
+ export const getContentModelBySlugSchema = z.object({
67
+ projectId: z.string().uuid(),
68
+ slug: z.string().min(1),
69
+ });
70
+
71
+ /** Get a single content model by ID */
72
+ export const getContentModelSchema = z.object({
73
+ modelId: z.string().uuid(),
74
+ });
75
+
76
+ /** Create a content model with fields atomically */
77
+ export const createContentModelSchema = z.object({
78
+ projectId: z.string().uuid(),
79
+ organizationId: z.string().uuid(),
80
+ slug: z
81
+ .string()
82
+ .min(1)
83
+ .max(100)
84
+ .regex(/^[a-z0-9-]+$/, "Slug must contain only lowercase letters, numbers, and hyphens"),
85
+ displayName: z.string().min(1).max(200),
86
+ description: z.string().max(1000).optional(),
87
+ kind: contentModelKindEnum.default("collection"),
88
+ icon: z.string().max(50).optional(),
89
+ enableVersionHistory: z.boolean().default(true),
90
+ includeBody: z.boolean().default(true),
91
+ fields: z
92
+ .array(
93
+ z.object({
94
+ name: z
95
+ .string()
96
+ .min(1)
97
+ .max(100)
98
+ .regex(
99
+ /^[a-z_][a-z0-9_]*$/,
100
+ "Field name must be snake_case (lowercase letters, numbers, underscores)",
101
+ ),
102
+ displayName: z.string().min(1).max(200),
103
+ type: contentModelFieldTypeEnum.default("text"),
104
+ localized: z.boolean().default(false),
105
+ required: z.boolean().default(false),
106
+ placeholder: z.string().max(500).optional(),
107
+ helpText: z.string().max(500).optional(),
108
+ position: z.number().int().min(0),
109
+ fieldConfig: fieldConfigSchema,
110
+ options: fieldOptionsSchema,
111
+ }),
112
+ )
113
+ .default([]),
114
+ });
115
+
116
+ /** Table settings schema for base field visibility */
117
+ export const contentModelTableSettingsSchema = z.object({
118
+ baseFields: z.record(z.string(), z.boolean()).optional(),
119
+ }).optional();
120
+
121
+ /** Update a content model's metadata (not fields) */
122
+ export const updateContentModelSchema = z.object({
123
+ modelId: z.string().uuid(),
124
+ displayName: z.string().min(1).max(200).optional(),
125
+ description: z.string().max(1000).optional(),
126
+ kind: contentModelKindEnum.optional(),
127
+ icon: z.string().max(50).optional(),
128
+ enableVersionHistory: z.boolean().optional(),
129
+ includeBody: z.boolean().optional(),
130
+ tableSettings: contentModelTableSettingsSchema,
131
+ });
132
+
133
+ /** Delete a content model */
134
+ export const deleteContentModelSchema = z.object({
135
+ modelId: z.string().uuid(),
136
+ });
137
+
138
+ // —————————————————————————————————————————————————————————————————————————————
139
+ // Field schemas
140
+ // —————————————————————————————————————————————————————————————————————————————
141
+
142
+ /** Add a field to a content model */
143
+ export const addContentModelFieldSchema = z.object({
144
+ modelId: z.string().uuid(),
145
+ name: z
146
+ .string()
147
+ .min(1)
148
+ .max(100)
149
+ .regex(
150
+ /^[a-z_][a-z0-9_]*$/,
151
+ "Field name must be snake_case",
152
+ ),
153
+ displayName: z.string().min(1).max(200),
154
+ type: contentModelFieldTypeEnum.default("text"),
155
+ localized: z.boolean().default(false),
156
+ required: z.boolean().default(false),
157
+ placeholder: z.string().max(500).optional(),
158
+ helpText: z.string().max(500).optional(),
159
+ position: z.number().int().min(0).optional(),
160
+ options: fieldOptionsSchema,
161
+ fieldConfig: fieldConfigSchema,
162
+ });
163
+
164
+ /** Update an existing field */
165
+ export const updateContentModelFieldSchema = z.object({
166
+ fieldId: z.string().uuid(),
167
+ displayName: z.string().min(1).max(200).optional(),
168
+ type: contentModelFieldTypeEnum.optional(),
169
+ localized: z.boolean().optional(),
170
+ required: z.boolean().optional(),
171
+ placeholder: z.string().max(500).optional(),
172
+ helpText: z.string().max(500).optional(),
173
+ options: fieldOptionsSchema,
174
+ fieldConfig: fieldConfigSchema,
175
+ });
176
+
177
+ /** Remove a field from a content model */
178
+ export const removeContentModelFieldSchema = z.object({
179
+ fieldId: z.string().uuid(),
180
+ });
181
+
182
+ /** Reorder fields within a content model */
183
+ export const reorderContentModelFieldsSchema = z.object({
184
+ modelId: z.string().uuid(),
185
+ fieldIds: z.array(z.string().uuid()).min(1),
186
+ });
187
+
188
+ // —————————————————————————————————————————————————————————————————————————————
189
+ // Entry schemas
190
+ // —————————————————————————————————————————————————————————————————————————————
191
+
192
+ export const contentEntryStatusEnum = z.enum(["draft", "published", "archived"]);
193
+ export const contentEntrySortFieldEnum = z.enum(["publishedAt", "createdAt", "updatedAt", "title"]);
194
+
195
+ /** List content entries with filtering and pagination */
196
+ export const listContentEntriesSchema = z.object({
197
+ projectId: z.string().uuid(),
198
+ modelSlug: z.string().optional(),
199
+ status: contentEntryStatusEnum.optional(),
200
+ search: z.union([z.string(), z.array(z.string())]).optional(),
201
+ /** Which languages to search content IN (default: all). Separate from languageCodes which filters BY having translations. */
202
+ searchLanguageCodes: z.array(z.string().transform(v => v.toLowerCase())).optional(),
203
+ /** Opt-in body search (default: false, can be slow on large bodies) */
204
+ searchInBody: z.boolean().default(false).optional(),
205
+ languageCodes: z.array(z.string().transform(v => v.toLowerCase())).optional(),
206
+ /** Filter entries that do NOT have a translation for this language code */
207
+ missingLanguageCode: z.string().transform(v => v.toLowerCase()).optional(),
208
+ /** Filter by custom field values. Key = field name, value = exact match. */
209
+ fieldFilters: z.record(z.string(), z.string()).optional(),
210
+ /** Filter entries by specific IDs */
211
+ ids: z.array(z.string().uuid()).optional(),
212
+ sort: contentEntrySortFieldEnum.optional(),
213
+ order: z.enum(["asc", "desc"]).optional(),
214
+ page: z.number().int().min(1).default(1),
215
+ limit: z.number().int().min(1).max(100).default(25),
216
+ });
217
+
218
+ /** Get a single content entry by ID */
219
+ export const getContentEntrySchema = z.object({
220
+ entryId: z.string().uuid(),
221
+ });
222
+
223
+ /** Create a content entry */
224
+ export const createContentEntrySchema = z.object({
225
+ contentModelId: z.string().uuid(),
226
+ organizationId: z.string().uuid(),
227
+ slug: z
228
+ .string()
229
+ .min(1)
230
+ .max(200)
231
+ .regex(/^[a-z0-9-]+$/, "Slug must contain only lowercase letters, numbers, and hyphens"),
232
+ sourceLanguageCode: z.string().default("en").transform(v => v.toLowerCase()),
233
+ title: z.string().min(1).max(500),
234
+ body: z.unknown().default([]),
235
+ bodyMarkdown: z.string().optional(),
236
+ customFields: z.record(z.string(), z.string().nullable()).default({}),
237
+ status: contentEntryStatusEnum.default("draft"),
238
+ });
239
+
240
+ /** Update a content entry */
241
+ export const updateContentEntrySchema = z.object({
242
+ entryId: z.string().uuid(),
243
+ languageCode: z.string().optional().transform(v => v?.toLowerCase()),
244
+ title: z.string().min(1).max(500).optional(),
245
+ body: z.unknown().optional(),
246
+ bodyMarkdown: z.string().optional(),
247
+ customFields: z.record(z.string(), z.string().nullable()).optional(),
248
+ status: contentEntryStatusEnum.optional(),
249
+ slug: z
250
+ .string()
251
+ .min(1)
252
+ .max(200)
253
+ .regex(/^[a-z0-9-]+$/, "Slug must contain only lowercase letters, numbers, and hyphens")
254
+ .optional(),
255
+ publishedAt: z.string().datetime({ offset: true }).nullable().optional(),
256
+ translationStatus: z.enum(["draft", "published"]).optional(),
257
+ });
258
+
259
+ /** Delete a content entry */
260
+ export const deleteContentEntrySchema = z.object({
261
+ entryId: z.string().uuid(),
262
+ });
263
+
264
+ /** Publish a content entry */
265
+ export const publishContentEntrySchema = z.object({
266
+ entryId: z.string().uuid(),
267
+ });
268
+
269
+ /** Generate AI content for a specific field */
270
+ export const generateFieldContentSchema = z.object({
271
+ entryId: z.string().uuid(),
272
+ fieldName: z.string().min(1),
273
+ languageCode: z.string().min(1).transform(v => v.toLowerCase()),
274
+ modelId: z.string().max(100).optional(),
275
+ prompt: z.string().max(4000).optional(),
276
+ });
277
+
278
+ // —————————————————————————————————————————————————————————————————————————————
279
+ // Type exports
280
+ // —————————————————————————————————————————————————————————————————————————————
281
+
282
+ export type EnumValueItem = z.infer<typeof enumValueItemSchema>;
283
+ export type ListContentModelsInput = z.infer<typeof listContentModelsSchema>;
284
+ export type GetContentModelBySlugInput = z.infer<typeof getContentModelBySlugSchema>;
285
+ export type GetContentModelInput = z.infer<typeof getContentModelSchema>;
286
+ export type CreateContentModelInput = z.infer<typeof createContentModelSchema>;
287
+ export type UpdateContentModelInput = z.infer<typeof updateContentModelSchema>;
288
+ export type DeleteContentModelInput = z.infer<typeof deleteContentModelSchema>;
289
+ export type AddContentModelFieldInput = z.infer<typeof addContentModelFieldSchema>;
290
+ export type UpdateContentModelFieldInput = z.infer<typeof updateContentModelFieldSchema>;
291
+ export type RemoveContentModelFieldInput = z.infer<typeof removeContentModelFieldSchema>;
292
+ export type ReorderContentModelFieldsInput = z.infer<typeof reorderContentModelFieldsSchema>;
293
+
294
+ export type ListContentEntriesInput = z.infer<typeof listContentEntriesSchema>;
295
+ export type GetContentEntryInput = z.infer<typeof getContentEntrySchema>;
296
+ export type CreateContentEntryInput = z.infer<typeof createContentEntrySchema>;
297
+ export type UpdateContentEntryInput = z.infer<typeof updateContentEntrySchema>;
298
+ export type DeleteContentEntryInput = z.infer<typeof deleteContentEntrySchema>;
299
+ export type PublishContentEntryInput = z.infer<typeof publishContentEntrySchema>;
300
+ export type GenerateFieldContentInput = z.infer<typeof generateFieldContentSchema>;