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.
- package/CONTRIBUTING.md +347 -0
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/docs/AI_PROVIDERS.md +343 -0
- package/docs/ARCHITECTURE.md +512 -0
- package/docs/CLI.md +438 -0
- package/docs/CONFIGURATION.md +453 -0
- package/docs/INTEGRATION.md +477 -0
- package/docs/ROADMAP.md +248 -0
- package/package.json +46 -0
- package/packages/ai/README.md +43 -0
- package/packages/ai/package.json +42 -0
- package/packages/ai/src/facade.ts +120 -0
- package/packages/ai/src/index.ts +34 -0
- package/packages/ai/src/prompt.ts +117 -0
- package/packages/ai/src/providers/gemini.ts +185 -0
- package/packages/ai/src/providers/index.ts +9 -0
- package/packages/ai/src/types.ts +134 -0
- package/packages/ai/tsconfig.json +19 -0
- package/packages/cli/README.md +70 -0
- package/packages/cli/package.json +44 -0
- package/packages/cli/src/cli.ts +30 -0
- package/packages/cli/src/commands/deploy.ts +115 -0
- package/packages/cli/src/commands/index.ts +9 -0
- package/packages/cli/src/commands/init.ts +107 -0
- package/packages/cli/src/commands/status.ts +73 -0
- package/packages/cli/src/commands/translate.ts +92 -0
- package/packages/cli/src/commands/validate.ts +69 -0
- package/packages/cli/tsconfig.json +19 -0
- package/packages/core/README.md +46 -0
- package/packages/core/package.json +50 -0
- package/packages/core/src/config.ts +241 -0
- package/packages/core/src/index.ts +111 -0
- package/packages/core/src/schema-generator.ts +263 -0
- package/packages/core/src/schemas.ts +126 -0
- package/packages/core/src/types.ts +481 -0
- package/packages/core/src/utils.ts +343 -0
- package/packages/core/tsconfig.json +19 -0
- package/packages/firestore/README.md +60 -0
- package/packages/firestore/package.json +48 -0
- package/packages/firestore/src/generator.ts +270 -0
- package/packages/firestore/src/i18n.ts +262 -0
- package/packages/firestore/src/index.ts +54 -0
- package/packages/firestore/src/processor.ts +245 -0
- package/packages/firestore/src/queue.ts +202 -0
- package/packages/firestore/src/task-handler.ts +164 -0
- package/packages/firestore/tsconfig.json +19 -0
- package/pnpm-workspace.yaml +2 -0
- 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';
|