melaka 0.0.0

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.
Files changed (49) hide show
  1. package/CONTRIBUTING.md +347 -0
  2. package/LICENSE +21 -0
  3. package/README.md +57 -0
  4. package/docs/AI_PROVIDERS.md +343 -0
  5. package/docs/ARCHITECTURE.md +512 -0
  6. package/docs/CLI.md +438 -0
  7. package/docs/CONFIGURATION.md +453 -0
  8. package/docs/INTEGRATION.md +477 -0
  9. package/docs/ROADMAP.md +248 -0
  10. package/package.json +46 -0
  11. package/packages/ai/README.md +43 -0
  12. package/packages/ai/package.json +42 -0
  13. package/packages/ai/src/facade.ts +120 -0
  14. package/packages/ai/src/index.ts +34 -0
  15. package/packages/ai/src/prompt.ts +117 -0
  16. package/packages/ai/src/providers/gemini.ts +185 -0
  17. package/packages/ai/src/providers/index.ts +9 -0
  18. package/packages/ai/src/types.ts +134 -0
  19. package/packages/ai/tsconfig.json +19 -0
  20. package/packages/cli/README.md +70 -0
  21. package/packages/cli/package.json +44 -0
  22. package/packages/cli/src/cli.ts +30 -0
  23. package/packages/cli/src/commands/deploy.ts +115 -0
  24. package/packages/cli/src/commands/index.ts +9 -0
  25. package/packages/cli/src/commands/init.ts +107 -0
  26. package/packages/cli/src/commands/status.ts +73 -0
  27. package/packages/cli/src/commands/translate.ts +92 -0
  28. package/packages/cli/src/commands/validate.ts +69 -0
  29. package/packages/cli/tsconfig.json +19 -0
  30. package/packages/core/README.md +46 -0
  31. package/packages/core/package.json +50 -0
  32. package/packages/core/src/config.ts +241 -0
  33. package/packages/core/src/index.ts +111 -0
  34. package/packages/core/src/schema-generator.ts +263 -0
  35. package/packages/core/src/schemas.ts +126 -0
  36. package/packages/core/src/types.ts +481 -0
  37. package/packages/core/src/utils.ts +343 -0
  38. package/packages/core/tsconfig.json +19 -0
  39. package/packages/firestore/README.md +60 -0
  40. package/packages/firestore/package.json +48 -0
  41. package/packages/firestore/src/generator.ts +270 -0
  42. package/packages/firestore/src/i18n.ts +262 -0
  43. package/packages/firestore/src/index.ts +54 -0
  44. package/packages/firestore/src/processor.ts +245 -0
  45. package/packages/firestore/src/queue.ts +202 -0
  46. package/packages/firestore/src/task-handler.ts +164 -0
  47. package/packages/firestore/tsconfig.json +19 -0
  48. package/pnpm-workspace.yaml +2 -0
  49. package/turbo.json +31 -0
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Melaka Firestore - Trigger Generator
3
+ *
4
+ * Generates Cloud Function code for Firestore triggers.
5
+ */
6
+
7
+ import type { MelakaConfig, CollectionConfig } from '@melaka/core';
8
+
9
+ /**
10
+ * Options for generating trigger code.
11
+ */
12
+ export interface GeneratorOptions {
13
+ /**
14
+ * Output directory for generated files.
15
+ */
16
+ outputDir: string;
17
+
18
+ /**
19
+ * Import path for Melaka packages.
20
+ * @default '@melaka/firestore'
21
+ */
22
+ melakaImport?: string;
23
+ }
24
+
25
+ /**
26
+ * Generated file output.
27
+ */
28
+ export interface GeneratedFile {
29
+ /**
30
+ * File path relative to output directory.
31
+ */
32
+ path: string;
33
+
34
+ /**
35
+ * File content.
36
+ */
37
+ content: string;
38
+ }
39
+
40
+ /**
41
+ * Generate trigger code for all configured collections.
42
+ *
43
+ * @param config - Melaka configuration
44
+ * @param options - Generator options
45
+ * @returns Array of generated files
46
+ */
47
+ export function generateTriggers(
48
+ config: MelakaConfig,
49
+ options: GeneratorOptions
50
+ ): GeneratedFile[] {
51
+ const files: GeneratedFile[] = [];
52
+
53
+ // Generate triggers file
54
+ files.push({
55
+ path: 'triggers.ts',
56
+ content: generateTriggersFile(config),
57
+ });
58
+
59
+ // Generate task handler file
60
+ files.push({
61
+ path: 'task-handler.ts',
62
+ content: generateTaskHandlerFile(config),
63
+ });
64
+
65
+ // Generate index file
66
+ files.push({
67
+ path: 'index.ts',
68
+ content: generateIndexFile(config),
69
+ });
70
+
71
+ return files;
72
+ }
73
+
74
+ /**
75
+ * Generate the triggers.ts file content.
76
+ */
77
+ function generateTriggersFile(config: MelakaConfig): string {
78
+ const imports = `
79
+ import { onDocumentWritten } from 'firebase-functions/v2/firestore';
80
+ import { getFunctions } from 'firebase-admin/functions';
81
+ import type { CollectionConfig } from '@melaka/core';
82
+ `.trim();
83
+
84
+ const triggerFunctions = config.collections
85
+ .map((collection) => generateTriggerFunction(collection, config))
86
+ .join('\n\n');
87
+
88
+ const enqueueHelper = `
89
+ /**
90
+ * Enqueue translation tasks for a document.
91
+ */
92
+ async function enqueueTranslation(
93
+ collectionPath: string,
94
+ documentId: string,
95
+ languages: string[],
96
+ config: CollectionConfig,
97
+ batchId: string
98
+ ): Promise<void> {
99
+ const queue = getFunctions().taskQueue('melakaTranslateTask');
100
+
101
+ for (let i = 0; i < languages.length; i++) {
102
+ const language = languages[i];
103
+ await queue.enqueue(
104
+ {
105
+ collectionPath,
106
+ documentId,
107
+ targetLanguage: language,
108
+ config,
109
+ batchId,
110
+ },
111
+ {
112
+ scheduleDelaySeconds: i * 2, // Stagger by 2 seconds
113
+ }
114
+ );
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Generate a unique batch ID.
120
+ */
121
+ function generateBatchId(): string {
122
+ const timestamp = Date.now();
123
+ const random = Math.random().toString(36).substring(2, 8);
124
+ return \`batch_\${timestamp}_\${random}\`;
125
+ }
126
+ `.trim();
127
+
128
+ return `${imports}\n\n${triggerFunctions}\n\n${enqueueHelper}`;
129
+ }
130
+
131
+ /**
132
+ * Generate a single trigger function.
133
+ */
134
+ function generateTriggerFunction(
135
+ collection: CollectionConfig,
136
+ config: MelakaConfig
137
+ ): string {
138
+ const functionName = `melakaTranslate${pascalCase(collection.path)}`;
139
+ const documentPath = collection.isCollectionGroup
140
+ ? `{parentPath}/${collection.path}/{docId}`
141
+ : `${collection.path}/{docId}`;
142
+
143
+ const collectionConfigStr = JSON.stringify(collection, null, 2)
144
+ .split('\n')
145
+ .map((line, i) => (i === 0 ? line : ' ' + line))
146
+ .join('\n');
147
+
148
+ return `
149
+ /**
150
+ * Auto-generated trigger for ${collection.path} collection.
151
+ */
152
+ export const ${functionName} = onDocumentWritten(
153
+ {
154
+ document: '${documentPath}',
155
+ region: '${config.region || 'us-central1'}',
156
+ },
157
+ async (event) => {
158
+ // Skip deletes
159
+ if (!event.data?.after.exists) {
160
+ return;
161
+ }
162
+
163
+ const collectionPath = ${collection.isCollectionGroup ? 'event.data.after.ref.parent.path' : `'${collection.path}'`};
164
+ const documentId = event.params.docId;
165
+ const batchId = generateBatchId();
166
+
167
+ const collectionConfig: CollectionConfig = ${collectionConfigStr};
168
+
169
+ const languages = ${JSON.stringify(config.languages)};
170
+
171
+ await enqueueTranslation(
172
+ collectionPath,
173
+ documentId,
174
+ languages,
175
+ collectionConfig,
176
+ batchId
177
+ );
178
+ }
179
+ );
180
+ `.trim();
181
+ }
182
+
183
+ /**
184
+ * Generate the task-handler.ts file content.
185
+ */
186
+ function generateTaskHandlerFile(config: MelakaConfig): string {
187
+ const apiKeySecret = config.ai.apiKeySecret || 'GEMINI_API_KEY';
188
+
189
+ return `
190
+ import { onTaskDispatched } from 'firebase-functions/v2/tasks';
191
+ import { defineSecret } from 'firebase-functions/params';
192
+ import { getFirestore } from 'firebase-admin/firestore';
193
+ import { handleTranslationTask } from '@melaka/firestore';
194
+ import type { MelakaConfig } from '@melaka/core';
195
+
196
+ const apiKeySecret = defineSecret('${apiKeySecret}');
197
+
198
+ /**
199
+ * Melaka configuration.
200
+ *
201
+ * This is a copy of your melaka.config.ts for runtime use.
202
+ * If you update melaka.config.ts, run \`melaka deploy\` to regenerate.
203
+ */
204
+ const config: MelakaConfig = ${JSON.stringify(config, null, 2)};
205
+
206
+ /**
207
+ * Task handler for processing translation tasks.
208
+ */
209
+ export const melakaTranslateTask = onTaskDispatched(
210
+ {
211
+ secrets: [apiKeySecret],
212
+ retryConfig: {
213
+ maxAttempts: 3,
214
+ minBackoffSeconds: 60,
215
+ maxBackoffSeconds: 300,
216
+ },
217
+ rateLimits: {
218
+ maxConcurrentDispatches: ${config.defaults?.maxConcurrency || 10},
219
+ },
220
+ region: '${config.region || 'us-central1'}',
221
+ },
222
+ async (request) => {
223
+ const result = await handleTranslationTask(request.data, {
224
+ firestore: getFirestore(),
225
+ config,
226
+ apiKey: apiKeySecret.value(),
227
+ });
228
+
229
+ if (!result.success && !result.skipped) {
230
+ // Throw to trigger retry
231
+ throw new Error(result.error || 'Translation failed');
232
+ }
233
+ }
234
+ );
235
+ `.trim();
236
+ }
237
+
238
+ /**
239
+ * Generate the index.ts file content.
240
+ */
241
+ function generateIndexFile(config: MelakaConfig): string {
242
+ const triggerExports = config.collections
243
+ .map((c) => `melakaTranslate${pascalCase(c.path)}`)
244
+ .join(',\n ');
245
+
246
+ return `
247
+ /**
248
+ * Melaka - Auto-generated Cloud Functions
249
+ *
250
+ * This file exports all Melaka triggers and task handlers.
251
+ * Import these in your main functions/src/index.ts file.
252
+ */
253
+
254
+ export {
255
+ ${triggerExports},
256
+ } from './triggers';
257
+
258
+ export { melakaTranslateTask } from './task-handler';
259
+ `.trim();
260
+ }
261
+
262
+ /**
263
+ * Convert a string to PascalCase.
264
+ */
265
+ function pascalCase(str: string): string {
266
+ return str
267
+ .split(/[-_\/]/)
268
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
269
+ .join('');
270
+ }
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Melaka Firestore - i18n Operations
3
+ *
4
+ * Functions for reading/writing to i18n subcollections.
5
+ */
6
+
7
+ import type { Firestore, DocumentReference, DocumentData, Timestamp } from 'firebase-admin/firestore';
8
+ import type { MelakaMetadata, TranslationStatus } from '@melaka/core';
9
+
10
+ /**
11
+ * Path to i18n subcollection for a document.
12
+ *
13
+ * @param docRef - Parent document reference
14
+ * @param locale - Target locale code
15
+ * @returns Path string to i18n document
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const path = getI18nPath(docRef, 'ms-MY');
20
+ * // 'products/abc123/i18n/ms-MY'
21
+ * ```
22
+ */
23
+ export function getI18nPath(docRef: DocumentReference, locale: string): string {
24
+ return `${docRef.path}/i18n/${locale}`;
25
+ }
26
+
27
+ /**
28
+ * Get the i18n document reference for a locale.
29
+ *
30
+ * @param docRef - Parent document reference
31
+ * @param locale - Target locale code
32
+ * @returns DocumentReference to i18n document
33
+ */
34
+ export function getI18nRef(
35
+ docRef: DocumentReference,
36
+ locale: string
37
+ ): DocumentReference {
38
+ return docRef.collection('i18n').doc(locale);
39
+ }
40
+
41
+ /**
42
+ * Read an existing translation from i18n subcollection.
43
+ *
44
+ * @param docRef - Parent document reference
45
+ * @param locale - Target locale code
46
+ * @returns Translation data or null if not exists
47
+ */
48
+ export async function readTranslation(
49
+ docRef: DocumentReference,
50
+ locale: string
51
+ ): Promise<DocumentData | null> {
52
+ const i18nRef = getI18nRef(docRef, locale);
53
+ const snapshot = await i18nRef.get();
54
+
55
+ if (!snapshot.exists) {
56
+ return null;
57
+ }
58
+
59
+ return snapshot.data() || null;
60
+ }
61
+
62
+ /**
63
+ * Read the Melaka metadata from an existing translation.
64
+ *
65
+ * @param docRef - Parent document reference
66
+ * @param locale - Target locale code
67
+ * @returns Melaka metadata or null if not exists
68
+ */
69
+ export async function readMelakaMetadata(
70
+ docRef: DocumentReference,
71
+ locale: string
72
+ ): Promise<MelakaMetadata | null> {
73
+ const translation = await readTranslation(docRef, locale);
74
+
75
+ if (!translation || !translation._melaka) {
76
+ return null;
77
+ }
78
+
79
+ return translation._melaka as MelakaMetadata;
80
+ }
81
+
82
+ /**
83
+ * Write a translation to i18n subcollection.
84
+ *
85
+ * @param docRef - Parent document reference
86
+ * @param locale - Target locale code
87
+ * @param translatedContent - Translated fields
88
+ * @param nonTranslatableContent - Copied fields
89
+ * @param metadata - Melaka metadata
90
+ */
91
+ export async function writeTranslation(
92
+ docRef: DocumentReference,
93
+ locale: string,
94
+ translatedContent: Record<string, unknown>,
95
+ nonTranslatableContent: Record<string, unknown>,
96
+ metadata: MelakaMetadata
97
+ ): Promise<void> {
98
+ const i18nRef = getI18nRef(docRef, locale);
99
+
100
+ const finalDoc = {
101
+ ...translatedContent,
102
+ ...nonTranslatableContent,
103
+ _melaka: metadata,
104
+ };
105
+
106
+ await i18nRef.set(finalDoc);
107
+ }
108
+
109
+ /**
110
+ * Update the Melaka metadata for an existing translation.
111
+ *
112
+ * @param docRef - Parent document reference
113
+ * @param locale - Target locale code
114
+ * @param metadata - Partial metadata to update
115
+ */
116
+ export async function updateMelakaMetadata(
117
+ docRef: DocumentReference,
118
+ locale: string,
119
+ metadata: Partial<MelakaMetadata>
120
+ ): Promise<void> {
121
+ const i18nRef = getI18nRef(docRef, locale);
122
+
123
+ const updates: Record<string, unknown> = {};
124
+ for (const [key, value] of Object.entries(metadata)) {
125
+ updates[`_melaka.${key}`] = value;
126
+ }
127
+
128
+ await i18nRef.update(updates);
129
+ }
130
+
131
+ /**
132
+ * Mark a translation as failed.
133
+ *
134
+ * @param docRef - Parent document reference
135
+ * @param locale - Target locale code
136
+ * @param error - Error message
137
+ * @param timestamp - Timestamp for the failure
138
+ */
139
+ export async function markTranslationFailed(
140
+ docRef: DocumentReference,
141
+ locale: string,
142
+ error: string,
143
+ timestamp: Timestamp
144
+ ): Promise<void> {
145
+ const i18nRef = getI18nRef(docRef, locale);
146
+ const existing = await i18nRef.get();
147
+
148
+ if (existing.exists) {
149
+ // Update existing document
150
+ await i18nRef.update({
151
+ '_melaka.status': 'failed' as TranslationStatus,
152
+ '_melaka.error': error,
153
+ '_melaka.translated_at': timestamp,
154
+ });
155
+ } else {
156
+ // Create minimal failed document
157
+ await i18nRef.set({
158
+ _melaka: {
159
+ status: 'failed' as TranslationStatus,
160
+ error,
161
+ translated_at: timestamp,
162
+ source_hash: '',
163
+ model: '',
164
+ reviewed: false,
165
+ },
166
+ });
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Delete a translation from i18n subcollection.
172
+ *
173
+ * @param docRef - Parent document reference
174
+ * @param locale - Target locale code
175
+ */
176
+ export async function deleteTranslation(
177
+ docRef: DocumentReference,
178
+ locale: string
179
+ ): Promise<void> {
180
+ const i18nRef = getI18nRef(docRef, locale);
181
+ await i18nRef.delete();
182
+ }
183
+
184
+ /**
185
+ * Delete all translations for a document.
186
+ *
187
+ * @param docRef - Parent document reference
188
+ */
189
+ export async function deleteAllTranslations(
190
+ docRef: DocumentReference
191
+ ): Promise<void> {
192
+ const i18nCollection = docRef.collection('i18n');
193
+ const snapshots = await i18nCollection.get();
194
+
195
+ const batch = docRef.firestore.batch();
196
+ for (const doc of snapshots.docs) {
197
+ batch.delete(doc.ref);
198
+ }
199
+
200
+ await batch.commit();
201
+ }
202
+
203
+ /**
204
+ * List all locales that have translations for a document.
205
+ *
206
+ * @param docRef - Parent document reference
207
+ * @returns Array of locale codes
208
+ */
209
+ export async function listTranslationLocales(
210
+ docRef: DocumentReference
211
+ ): Promise<string[]> {
212
+ const i18nCollection = docRef.collection('i18n');
213
+ const snapshots = await i18nCollection.select().get();
214
+
215
+ return snapshots.docs.map((doc) => doc.id);
216
+ }
217
+
218
+ /**
219
+ * Check if a translation exists and is up-to-date.
220
+ *
221
+ * @param docRef - Parent document reference
222
+ * @param locale - Target locale code
223
+ * @param sourceHash - Current source content hash
224
+ * @returns Whether translation exists and matches source hash
225
+ */
226
+ export async function isTranslationCurrent(
227
+ docRef: DocumentReference,
228
+ locale: string,
229
+ sourceHash: string
230
+ ): Promise<boolean> {
231
+ const metadata = await readMelakaMetadata(docRef, locale);
232
+
233
+ if (!metadata) {
234
+ return false;
235
+ }
236
+
237
+ return (
238
+ metadata.status === 'completed' &&
239
+ metadata.source_hash === sourceHash
240
+ );
241
+ }
242
+
243
+ /**
244
+ * Get translation status summary for a document.
245
+ *
246
+ * @param docRef - Parent document reference
247
+ * @param locales - Locales to check
248
+ * @returns Status for each locale
249
+ */
250
+ export async function getTranslationStatus(
251
+ docRef: DocumentReference,
252
+ locales: string[]
253
+ ): Promise<Record<string, TranslationStatus | 'missing'>> {
254
+ const status: Record<string, TranslationStatus | 'missing'> = {};
255
+
256
+ for (const locale of locales) {
257
+ const metadata = await readMelakaMetadata(docRef, locale);
258
+ status[locale] = metadata?.status || 'missing';
259
+ }
260
+
261
+ return status;
262
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @melaka/firestore
3
+ *
4
+ * Firestore adapter and triggers for Melaka - AI-powered Firestore localization.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ // i18n operations
10
+ export {
11
+ getI18nPath,
12
+ getI18nRef,
13
+ readTranslation,
14
+ readMelakaMetadata,
15
+ writeTranslation,
16
+ updateMelakaMetadata,
17
+ markTranslationFailed,
18
+ deleteTranslation,
19
+ deleteAllTranslations,
20
+ listTranslationLocales,
21
+ isTranslationCurrent,
22
+ getTranslationStatus,
23
+ } from './i18n';
24
+
25
+ // Translation processor
26
+ export {
27
+ processTranslation,
28
+ processAllLanguages,
29
+ type ProcessOptions,
30
+ type ProcessResult,
31
+ } from './processor';
32
+
33
+ // Task handling
34
+ export {
35
+ handleTranslationTask,
36
+ createTaskPayload,
37
+ type TaskHandlerContext,
38
+ } from './task-handler';
39
+
40
+ // Task queue
41
+ export {
42
+ enqueueDocumentTranslation,
43
+ enqueueCollectionTranslation,
44
+ type TaskQueue,
45
+ type EnqueueOptions,
46
+ type EnqueueResult,
47
+ } from './queue';
48
+
49
+ // Code generator
50
+ export {
51
+ generateTriggers,
52
+ type GeneratorOptions,
53
+ type GeneratedFile,
54
+ } from './generator';