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,343 @@
1
+ /**
2
+ * Melaka Core - Utilities
3
+ *
4
+ * Shared utility functions for content processing, hashing, and field detection.
5
+ */
6
+
7
+ import { createHash } from 'crypto';
8
+ import type { SchemaType, SeparatedContent } from './types';
9
+
10
+ // ============================================================================
11
+ // Content Hashing
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Generate SHA256 hash of content for change detection.
16
+ *
17
+ * @param content - Object to hash
18
+ * @returns Hex-encoded SHA256 hash
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const hash = hashContent({ title: 'Hello', body: 'World' });
23
+ * // 'a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e'
24
+ * ```
25
+ */
26
+ export function hashContent(content: Record<string, unknown>): string {
27
+ const normalized = JSON.stringify(content, Object.keys(content).sort());
28
+ return createHash('sha256').update(normalized).digest('hex');
29
+ }
30
+
31
+ // ============================================================================
32
+ // Field Type Detection
33
+ // ============================================================================
34
+
35
+ /**
36
+ * Check if a value is a Firestore DocumentReference.
37
+ */
38
+ function isDocumentReference(value: unknown): boolean {
39
+ return (
40
+ value !== null &&
41
+ typeof value === 'object' &&
42
+ 'path' in value &&
43
+ typeof (value as { path: unknown }).path === 'string' &&
44
+ 'id' in value
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Check if a value is a Firestore Timestamp.
50
+ */
51
+ function isTimestamp(value: unknown): boolean {
52
+ return (
53
+ value !== null &&
54
+ typeof value === 'object' &&
55
+ '_seconds' in value &&
56
+ '_nanoseconds' in value
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Check if a value is a Firestore GeoPoint.
62
+ */
63
+ function isGeoPoint(value: unknown): boolean {
64
+ return (
65
+ value !== null &&
66
+ typeof value === 'object' &&
67
+ 'latitude' in value &&
68
+ 'longitude' in value
69
+ );
70
+ }
71
+
72
+ /**
73
+ * Detect the schema type of a value.
74
+ *
75
+ * @param value - Value to detect type of
76
+ * @returns Detected schema type
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * detectFieldType('hello') // 'string'
81
+ * detectFieldType(['a', 'b']) // 'string[]'
82
+ * detectFieldType(123) // 'number'
83
+ * detectFieldType({ foo: 'bar' }) // 'object'
84
+ * ```
85
+ */
86
+ export function detectFieldType(value: unknown): SchemaType {
87
+ // Null values
88
+ if (value === null) {
89
+ return 'object|null';
90
+ }
91
+
92
+ // Arrays
93
+ if (Array.isArray(value)) {
94
+ if (value.length === 0) {
95
+ // Empty arrays default to string[] (most common case)
96
+ return 'string[]';
97
+ }
98
+
99
+ const firstItem = value[0];
100
+
101
+ // Check for DocumentReference array
102
+ if (isDocumentReference(firstItem)) {
103
+ return 'DocumentReference[]';
104
+ }
105
+
106
+ // Check for string array
107
+ if (typeof firstItem === 'string') {
108
+ return 'string[]';
109
+ }
110
+
111
+ // Check for number array
112
+ if (typeof firstItem === 'number') {
113
+ return 'number[]';
114
+ }
115
+
116
+ // Object array
117
+ if (typeof firstItem === 'object' && firstItem !== null) {
118
+ return 'object[]';
119
+ }
120
+
121
+ // Default to string[] for other arrays
122
+ return 'string[]';
123
+ }
124
+
125
+ // DocumentReference
126
+ if (isDocumentReference(value)) {
127
+ return 'DocumentReference';
128
+ }
129
+
130
+ // Timestamp
131
+ if (isTimestamp(value)) {
132
+ return 'object'; // Timestamps are copied, not translated
133
+ }
134
+
135
+ // GeoPoint
136
+ if (isGeoPoint(value)) {
137
+ return 'object'; // GeoPoints are copied, not translated
138
+ }
139
+
140
+ // Primitives
141
+ if (typeof value === 'string') {
142
+ return 'string';
143
+ }
144
+
145
+ if (typeof value === 'number') {
146
+ return 'number';
147
+ }
148
+
149
+ if (typeof value === 'boolean') {
150
+ return 'boolean';
151
+ }
152
+
153
+ // Objects
154
+ if (typeof value === 'object') {
155
+ return 'object';
156
+ }
157
+
158
+ // Default fallback
159
+ return 'object';
160
+ }
161
+
162
+ /**
163
+ * Check if a schema type represents translatable content.
164
+ *
165
+ * Only strings and string arrays are sent to AI for translation.
166
+ * Everything else is copied as-is.
167
+ *
168
+ * @param schemaType - Schema type to check
169
+ * @returns Whether the type is translatable
170
+ */
171
+ export function isTranslatableType(schemaType: SchemaType): boolean {
172
+ return schemaType === 'string' || schemaType === 'string[]';
173
+ }
174
+
175
+ // ============================================================================
176
+ // Content Separation
177
+ // ============================================================================
178
+
179
+ /**
180
+ * Separate document content into translatable and non-translatable parts.
181
+ *
182
+ * @param doc - Document data
183
+ * @param fields - Optional list of fields to consider (if omitted, all fields are considered)
184
+ * @returns Separated content with detected types
185
+ *
186
+ * @example
187
+ * ```typescript
188
+ * const result = separateContent({
189
+ * title: 'Hello',
190
+ * price: 99,
191
+ * tags: ['sale', 'new'],
192
+ * author_ref: docRef,
193
+ * });
194
+ *
195
+ * // result.translatable = { title: 'Hello', tags: ['sale', 'new'] }
196
+ * // result.nonTranslatable = { price: 99, author_ref: docRef }
197
+ * // result.detectedTypes = { title: 'string', price: 'number', ... }
198
+ * ```
199
+ */
200
+ export function separateContent(
201
+ doc: Record<string, unknown>,
202
+ fields?: string[]
203
+ ): SeparatedContent {
204
+ const translatable: Record<string, unknown> = {};
205
+ const nonTranslatable: Record<string, unknown> = {};
206
+ const detectedTypes: Record<string, SchemaType> = {};
207
+
208
+ for (const [field, value] of Object.entries(doc)) {
209
+ // Skip internal Melaka metadata
210
+ if (field === '_melaka') {
211
+ continue;
212
+ }
213
+
214
+ // If fields list is provided, only process those fields
215
+ if (fields && !fields.includes(field)) {
216
+ nonTranslatable[field] = value;
217
+ continue;
218
+ }
219
+
220
+ const schemaType = detectFieldType(value);
221
+ detectedTypes[field] = schemaType;
222
+
223
+ if (isTranslatableType(schemaType)) {
224
+ translatable[field] = value;
225
+ } else {
226
+ nonTranslatable[field] = value;
227
+ }
228
+ }
229
+
230
+ return { translatable, nonTranslatable, detectedTypes };
231
+ }
232
+
233
+ // ============================================================================
234
+ // Glossary Utilities
235
+ // ============================================================================
236
+
237
+ /**
238
+ * Merge shared and collection-specific glossaries.
239
+ *
240
+ * Collection glossary takes precedence over shared glossary.
241
+ *
242
+ * @param shared - Shared glossary (applies to all collections)
243
+ * @param collection - Collection-specific glossary
244
+ * @returns Merged glossary
245
+ */
246
+ export function mergeGlossaries(
247
+ shared?: Record<string, string>,
248
+ collection?: Record<string, string>
249
+ ): Record<string, string> {
250
+ return {
251
+ ...(shared || {}),
252
+ ...(collection || {}),
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Format glossary for inclusion in AI prompt.
258
+ *
259
+ * @param glossary - Glossary to format
260
+ * @returns Formatted string for prompt
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * formatGlossary({ 'checkout': 'checkout', 'cart': 'troli' })
265
+ * // 'checkout → checkout\ncart → troli'
266
+ * ```
267
+ */
268
+ export function formatGlossary(glossary?: Record<string, string>): string {
269
+ if (!glossary || Object.keys(glossary).length === 0) {
270
+ return 'No specific glossary terms.';
271
+ }
272
+
273
+ return Object.entries(glossary)
274
+ .map(([term, translation]) => `${term} → ${translation}`)
275
+ .join('\n');
276
+ }
277
+
278
+ // ============================================================================
279
+ // Language Utilities
280
+ // ============================================================================
281
+
282
+ /**
283
+ * Get display name for a language code.
284
+ *
285
+ * @param code - BCP 47 language code
286
+ * @returns Human-readable language name
287
+ *
288
+ * @example
289
+ * ```typescript
290
+ * getLanguageName('ms-MY') // 'Malay (Malaysia)'
291
+ * getLanguageName('zh-CN') // 'Chinese (Simplified)'
292
+ * ```
293
+ */
294
+ export function getLanguageName(code: string): string {
295
+ const names: Record<string, string> = {
296
+ 'ms-MY': 'Malay (Malaysia)',
297
+ 'zh-CN': 'Chinese (Simplified)',
298
+ 'zh-TW': 'Chinese (Traditional)',
299
+ 'ta-IN': 'Tamil (India)',
300
+ 'hi-IN': 'Hindi (India)',
301
+ 'id-ID': 'Indonesian',
302
+ 'th-TH': 'Thai',
303
+ 'vi-VN': 'Vietnamese',
304
+ 'ja-JP': 'Japanese',
305
+ 'ko-KR': 'Korean',
306
+ 'es-ES': 'Spanish (Spain)',
307
+ 'es-MX': 'Spanish (Mexico)',
308
+ 'fr-FR': 'French (France)',
309
+ 'de-DE': 'German',
310
+ 'pt-BR': 'Portuguese (Brazil)',
311
+ 'pt-PT': 'Portuguese (Portugal)',
312
+ 'ar-SA': 'Arabic (Saudi Arabia)',
313
+ 'ru-RU': 'Russian',
314
+ 'it-IT': 'Italian',
315
+ 'nl-NL': 'Dutch',
316
+ 'pl-PL': 'Polish',
317
+ 'tr-TR': 'Turkish',
318
+ 'en-US': 'English (US)',
319
+ 'en-GB': 'English (UK)',
320
+ };
321
+
322
+ return names[code] || code;
323
+ }
324
+
325
+ // ============================================================================
326
+ // ID Generation
327
+ // ============================================================================
328
+
329
+ /**
330
+ * Generate a unique batch ID for translation operations.
331
+ *
332
+ * @returns Unique batch ID
333
+ *
334
+ * @example
335
+ * ```typescript
336
+ * generateBatchId() // 'batch_1708972800000_a1b2c3'
337
+ * ```
338
+ */
339
+ export function generateBatchId(): string {
340
+ const timestamp = Date.now();
341
+ const random = Math.random().toString(36).substring(2, 8);
342
+ return `batch_${timestamp}_${random}`;
343
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "declaration": true,
12
+ "declarationMap": true,
13
+ "sourceMap": true,
14
+ "outDir": "./dist",
15
+ "rootDir": "./src"
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }
@@ -0,0 +1,60 @@
1
+ # @melaka/firestore
2
+
3
+ Firestore adapter and triggers for Melaka - AI-powered Firestore localization.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @melaka/firestore
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### i18n Operations
14
+
15
+ ```typescript
16
+ import { readTranslation, writeTranslation, getI18nRef } from '@melaka/firestore';
17
+
18
+ // Read a translation
19
+ const translation = await readTranslation(docRef, 'ms-MY');
20
+
21
+ // Write a translation
22
+ await writeTranslation(docRef, 'ms-MY', translatedContent, nonTranslatable, metadata);
23
+
24
+ // Check if translation is current
25
+ const isCurrent = await isTranslationCurrent(docRef, 'ms-MY', sourceHash);
26
+ ```
27
+
28
+ ### Translation Processing
29
+
30
+ ```typescript
31
+ import { processTranslation } from '@melaka/firestore';
32
+
33
+ const result = await processTranslation(
34
+ docRef,
35
+ docData,
36
+ 'ms-MY',
37
+ config,
38
+ collectionConfig
39
+ );
40
+
41
+ if (result.success) {
42
+ console.log('Translation completed!');
43
+ }
44
+ ```
45
+
46
+ ### Code Generation
47
+
48
+ ```typescript
49
+ import { generateTriggers } from '@melaka/firestore';
50
+
51
+ const files = generateTriggers(config, {
52
+ outputDir: 'functions/src/melaka',
53
+ });
54
+
55
+ // files contains triggers.ts, task-handler.ts, index.ts
56
+ ```
57
+
58
+ ## Documentation
59
+
60
+ See the [main Melaka documentation](https://github.com/rizahassan/melaka).
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@melaka/firestore",
3
+ "version": "0.0.0",
4
+ "description": "Firestore adapter and triggers for Melaka",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts",
20
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "lint": "eslint src/",
24
+ "typecheck": "tsc --noEmit",
25
+ "clean": "rm -rf dist"
26
+ },
27
+ "dependencies": {
28
+ "@melaka/core": "workspace:*",
29
+ "@melaka/ai": "workspace:*"
30
+ },
31
+ "devDependencies": {
32
+ "firebase-admin": "^12.0.0",
33
+ "firebase-functions": "^6.0.0",
34
+ "tsup": "^8.0.0",
35
+ "typescript": "^5.3.0",
36
+ "vitest": "^1.2.0"
37
+ },
38
+ "peerDependencies": {
39
+ "firebase-admin": ">=11.0.0",
40
+ "firebase-functions": ">=4.0.0"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/rizahassan/melaka.git",
45
+ "directory": "packages/firestore"
46
+ },
47
+ "license": "MIT"
48
+ }