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,134 @@
1
+ /**
2
+ * Melaka AI - Provider Interface
3
+ *
4
+ * Defines the common interface all AI providers must implement.
5
+ */
6
+
7
+ import type { z } from 'zod';
8
+
9
+ /**
10
+ * Configuration for an AI translation request.
11
+ */
12
+ export interface TranslationOptions {
13
+ /**
14
+ * Target language code (BCP 47 format).
15
+ * @example 'ms-MY', 'zh-CN'
16
+ */
17
+ targetLanguage: string;
18
+
19
+ /**
20
+ * Custom prompt/context for the translation.
21
+ */
22
+ prompt?: string;
23
+
24
+ /**
25
+ * Glossary of terms to preserve or translate consistently.
26
+ */
27
+ glossary?: Record<string, string>;
28
+
29
+ /**
30
+ * Temperature for generation (0-1).
31
+ */
32
+ temperature?: number;
33
+ }
34
+
35
+ /**
36
+ * Result of an AI translation.
37
+ */
38
+ export interface TranslationResponse<T = Record<string, unknown>> {
39
+ /**
40
+ * Whether the translation succeeded.
41
+ */
42
+ success: boolean;
43
+
44
+ /**
45
+ * Translated content (matches input schema structure).
46
+ */
47
+ output?: T;
48
+
49
+ /**
50
+ * Error message if failed.
51
+ */
52
+ error?: string;
53
+
54
+ /**
55
+ * Token usage statistics.
56
+ */
57
+ usage?: {
58
+ inputTokens: number;
59
+ outputTokens: number;
60
+ totalTokens: number;
61
+ };
62
+
63
+ /**
64
+ * Duration in milliseconds.
65
+ */
66
+ durationMs?: number;
67
+ }
68
+
69
+ /**
70
+ * Common interface for all AI providers.
71
+ *
72
+ * Each provider (Gemini, OpenAI, Claude) implements this interface.
73
+ */
74
+ export interface AIProvider {
75
+ /**
76
+ * Provider name for identification.
77
+ */
78
+ readonly name: string;
79
+
80
+ /**
81
+ * Translate content using the AI provider.
82
+ *
83
+ * @param content - Content to translate (translatable fields only)
84
+ * @param schema - Zod schema defining the expected output structure
85
+ * @param options - Translation options (language, prompt, glossary)
86
+ * @returns Translation result
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * const result = await provider.translate(
91
+ * { title: 'Hello World', body: 'Welcome to our app' },
92
+ * z.object({ title: z.string(), body: z.string() }),
93
+ * { targetLanguage: 'ms-MY' }
94
+ * );
95
+ * ```
96
+ */
97
+ translate<T>(
98
+ content: Record<string, unknown>,
99
+ schema: z.ZodSchema<T>,
100
+ options: TranslationOptions
101
+ ): Promise<TranslationResponse<T>>;
102
+
103
+ /**
104
+ * Check if the provider is properly configured.
105
+ *
106
+ * @returns Whether the provider has valid credentials
107
+ */
108
+ isConfigured(): boolean;
109
+
110
+ /**
111
+ * Get the model name being used.
112
+ */
113
+ getModel(): string;
114
+ }
115
+
116
+ /**
117
+ * Configuration for creating an AI provider instance.
118
+ */
119
+ export interface ProviderConfig {
120
+ /**
121
+ * API key for the provider.
122
+ */
123
+ apiKey?: string;
124
+
125
+ /**
126
+ * Model name to use.
127
+ */
128
+ model: string;
129
+
130
+ /**
131
+ * Default temperature for generation.
132
+ */
133
+ temperature?: number;
134
+ }
@@ -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,70 @@
1
+ # @melaka/cli
2
+
3
+ Command-line interface for Melaka - AI-powered Firestore localization.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @melaka/cli
9
+ # or
10
+ pnpm add -g @melaka/cli
11
+ ```
12
+
13
+ ## Commands
14
+
15
+ ### `melaka init`
16
+
17
+ Initialize Melaka in your Firebase project.
18
+
19
+ ```bash
20
+ cd your-firebase-project
21
+ melaka init
22
+ ```
23
+
24
+ Creates `melaka.config.ts` with a starter configuration.
25
+
26
+ ### `melaka deploy`
27
+
28
+ Generate and deploy Melaka Cloud Functions.
29
+
30
+ ```bash
31
+ melaka deploy
32
+ ```
33
+
34
+ Options:
35
+ - `-o, --output <dir>` — Output directory (default: `functions/src/melaka`)
36
+ - `--no-firebase` — Generate only, skip deployment
37
+ - `--dry-run` — Show what would be generated
38
+
39
+ ### `melaka translate`
40
+
41
+ Manually trigger translation for collections.
42
+
43
+ ```bash
44
+ melaka translate
45
+ melaka translate --collection products
46
+ melaka translate --collection products --language ms-MY
47
+ melaka translate --force
48
+ ```
49
+
50
+ ### `melaka status`
51
+
52
+ Check translation status and configuration.
53
+
54
+ ```bash
55
+ melaka status
56
+ melaka status --collection products
57
+ melaka status --json
58
+ ```
59
+
60
+ ### `melaka validate`
61
+
62
+ Validate your `melaka.config.ts`.
63
+
64
+ ```bash
65
+ melaka validate
66
+ ```
67
+
68
+ ## Documentation
69
+
70
+ See the [main Melaka documentation](https://github.com/rizahassan/melaka).
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@melaka/cli",
3
+ "version": "0.0.0",
4
+ "description": "CLI for Melaka - AI-powered Firestore localization",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "bin": {
8
+ "melaka": "./dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup src/cli.ts --format cjs --dts",
15
+ "dev": "tsup src/cli.ts --format cjs --dts --watch",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "lint": "eslint src/",
19
+ "typecheck": "tsc --noEmit",
20
+ "clean": "rm -rf dist"
21
+ },
22
+ "dependencies": {
23
+ "@melaka/core": "workspace:*",
24
+ "@melaka/firestore": "workspace:*",
25
+ "commander": "^12.0.0",
26
+ "chalk": "^5.3.0",
27
+ "ora": "^8.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.3.0",
32
+ "vitest": "^1.2.0",
33
+ "@types/node": "^20.0.0"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/rizahassan/melaka.git",
38
+ "directory": "packages/cli"
39
+ },
40
+ "license": "MIT",
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ }
44
+ }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Melaka CLI
4
+ *
5
+ * Command-line interface for Melaka - AI-powered Firestore localization.
6
+ */
7
+
8
+ import { Command } from 'commander';
9
+ import { initCommand } from './commands/init';
10
+ import { deployCommand } from './commands/deploy';
11
+ import { translateCommand } from './commands/translate';
12
+ import { statusCommand } from './commands/status';
13
+ import { validateCommand } from './commands/validate';
14
+
15
+ const program = new Command();
16
+
17
+ program
18
+ .name('melaka')
19
+ .description('AI-powered localization for Firebase Firestore')
20
+ .version('0.0.0');
21
+
22
+ // Register commands
23
+ program.addCommand(initCommand);
24
+ program.addCommand(deployCommand);
25
+ program.addCommand(translateCommand);
26
+ program.addCommand(statusCommand);
27
+ program.addCommand(validateCommand);
28
+
29
+ // Parse arguments
30
+ program.parse();
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Melaka CLI - Deploy Command
3
+ *
4
+ * Generate and deploy Melaka Cloud Functions.
5
+ */
6
+
7
+ import { Command } from 'commander';
8
+ import { writeFile, mkdir, access } from 'fs/promises';
9
+ import { join, dirname } from 'path';
10
+ import { loadConfig } from '@melaka/core';
11
+ import { generateTriggers, type GeneratedFile } from '@melaka/firestore';
12
+
13
+ export const deployCommand = new Command('deploy')
14
+ .description('Generate and deploy Melaka triggers')
15
+ .option('-o, --output <dir>', 'Output directory for generated files', 'functions/src/melaka')
16
+ .option('--no-firebase', 'Skip Firebase deployment (generate only)')
17
+ .option('--dry-run', 'Show what would be generated without writing')
18
+ .action(async (options) => {
19
+ const chalk = (await import('chalk')).default;
20
+ const ora = (await import('ora')).default;
21
+
22
+ // Load config
23
+ const configSpinner = ora('Loading melaka.config.ts...').start();
24
+ let config;
25
+
26
+ try {
27
+ config = await loadConfig();
28
+ configSpinner.succeed(`Loaded config: ${config.collections.length} collections, ${config.languages.length} languages`);
29
+ } catch (error) {
30
+ configSpinner.fail('Failed to load config');
31
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
32
+ process.exit(1);
33
+ }
34
+
35
+ // Generate files
36
+ const genSpinner = ora('Generating Cloud Functions...').start();
37
+
38
+ try {
39
+ const files = generateTriggers(config, {
40
+ outputDir: options.output,
41
+ });
42
+
43
+ if (options.dryRun) {
44
+ genSpinner.succeed('Generated (dry run)');
45
+ console.log('');
46
+ console.log(chalk.gray('Files that would be created:'));
47
+ for (const file of files) {
48
+ console.log(chalk.cyan(` ${options.output}/${file.path}`));
49
+ }
50
+ console.log('');
51
+ return;
52
+ }
53
+
54
+ // Write files
55
+ await writeGeneratedFiles(files, options.output);
56
+ genSpinner.succeed(`Generated ${files.length} files in ${options.output}/`);
57
+ } catch (error) {
58
+ genSpinner.fail('Failed to generate files');
59
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
60
+ process.exit(1);
61
+ }
62
+
63
+ // Deploy to Firebase (unless --no-firebase)
64
+ if (options.firebase !== false) {
65
+ console.log('');
66
+ console.log(chalk.yellow('📦 To deploy to Firebase, run:'));
67
+ console.log(chalk.cyan(' firebase deploy --only functions'));
68
+ console.log('');
69
+ console.log(chalk.gray('Or add the exports to your functions/src/index.ts:'));
70
+ console.log(chalk.gray(` export * from './melaka';`));
71
+ }
72
+
73
+ // Show summary
74
+ console.log('');
75
+ console.log(chalk.green('✨ Melaka triggers generated!'));
76
+ console.log('');
77
+ console.log('Generated triggers:');
78
+ for (const collection of config.collections) {
79
+ const name = `melakaTranslate${pascalCase(collection.path)}`;
80
+ console.log(chalk.gray(` - ${name} → ${collection.path}`));
81
+ }
82
+ console.log(chalk.gray(' - melakaTranslateTask (task handler)'));
83
+ console.log('');
84
+ });
85
+
86
+ /**
87
+ * Write generated files to disk.
88
+ */
89
+ async function writeGeneratedFiles(
90
+ files: GeneratedFile[],
91
+ outputDir: string
92
+ ): Promise<void> {
93
+ // Ensure output directory exists
94
+ await mkdir(outputDir, { recursive: true });
95
+
96
+ for (const file of files) {
97
+ const filePath = join(outputDir, file.path);
98
+ const fileDir = dirname(filePath);
99
+
100
+ // Ensure parent directory exists
101
+ await mkdir(fileDir, { recursive: true });
102
+
103
+ await writeFile(filePath, file.content);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Convert string to PascalCase.
109
+ */
110
+ function pascalCase(str: string): string {
111
+ return str
112
+ .split(/[-_\/]/)
113
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
114
+ .join('');
115
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Melaka CLI - Command Exports
3
+ */
4
+
5
+ export { initCommand } from './init';
6
+ export { deployCommand } from './deploy';
7
+ export { translateCommand } from './translate';
8
+ export { statusCommand } from './status';
9
+ export { validateCommand } from './validate';
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Melaka CLI - Init Command
3
+ *
4
+ * Initialize a new melaka.config.ts in the current project.
5
+ */
6
+
7
+ import { Command } from 'commander';
8
+ import { writeFile, access } from 'fs/promises';
9
+ import { join } from 'path';
10
+
11
+ export const initCommand = new Command('init')
12
+ .description('Initialize Melaka in your Firebase project')
13
+ .option('-f, --force', 'Overwrite existing config file')
14
+ .action(async (options) => {
15
+ const chalk = (await import('chalk')).default;
16
+ const ora = (await import('ora')).default;
17
+
18
+ const configPath = join(process.cwd(), 'melaka.config.ts');
19
+
20
+ // Check if config already exists
21
+ try {
22
+ await access(configPath);
23
+ if (!options.force) {
24
+ console.log(chalk.yellow('⚠️ melaka.config.ts already exists.'));
25
+ console.log(chalk.gray(' Use --force to overwrite.'));
26
+ return;
27
+ }
28
+ } catch {
29
+ // File doesn't exist, continue
30
+ }
31
+
32
+ const spinner = ora('Creating melaka.config.ts...').start();
33
+
34
+ try {
35
+ await writeFile(configPath, CONFIG_TEMPLATE);
36
+ spinner.succeed('Created melaka.config.ts');
37
+
38
+ console.log('');
39
+ console.log(chalk.green('✨ Melaka initialized!'));
40
+ console.log('');
41
+ console.log('Next steps:');
42
+ console.log(chalk.gray(' 1. Edit melaka.config.ts with your collections'));
43
+ console.log(chalk.gray(' 2. Set up your AI API key:'));
44
+ console.log(chalk.cyan(' firebase functions:secrets:set GEMINI_API_KEY'));
45
+ console.log(chalk.gray(' 3. Deploy triggers:'));
46
+ console.log(chalk.cyan(' melaka deploy'));
47
+ console.log('');
48
+ } catch (error) {
49
+ spinner.fail('Failed to create config file');
50
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
51
+ process.exit(1);
52
+ }
53
+ });
54
+
55
+ const CONFIG_TEMPLATE = `import { defineConfig } from 'melaka';
56
+
57
+ /**
58
+ * Melaka Configuration
59
+ *
60
+ * Configure your Firestore collections for automatic AI translation.
61
+ *
62
+ * @see https://github.com/rizahassan/melaka/blob/main/docs/CONFIGURATION.md
63
+ */
64
+ export default defineConfig({
65
+ // Target languages (BCP 47 format)
66
+ languages: ['ms-MY', 'zh-CN'],
67
+
68
+ // AI provider configuration
69
+ ai: {
70
+ provider: 'gemini',
71
+ model: 'gemini-2.5-flash',
72
+ apiKeySecret: 'GEMINI_API_KEY', // Firebase secret name
73
+ temperature: 0.3,
74
+ },
75
+
76
+ // Firebase region (should match your Firestore region)
77
+ region: 'us-central1',
78
+
79
+ // Default settings for all collections
80
+ defaults: {
81
+ batchSize: 20,
82
+ maxConcurrency: 10,
83
+ forceUpdate: false,
84
+ },
85
+
86
+ // Shared glossary (optional)
87
+ // Terms here are used consistently across all collections
88
+ glossary: {
89
+ // 'brand_name': 'Brand Name', // Don't translate
90
+ },
91
+
92
+ // Collections to translate
93
+ collections: [
94
+ {
95
+ path: 'articles',
96
+ fields: ['title', 'content', 'summary'],
97
+ prompt: 'Blog article content. Maintain markdown formatting.',
98
+ },
99
+ // Add more collections as needed:
100
+ // {
101
+ // path: 'products',
102
+ // fields: ['name', 'description'],
103
+ // prompt: 'E-commerce product descriptions.',
104
+ // },
105
+ ],
106
+ });
107
+ `;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Melaka CLI - Status Command
3
+ *
4
+ * Check translation status for collections.
5
+ */
6
+
7
+ import { Command } from 'commander';
8
+ import { loadConfig, getLanguageName } from '@melaka/core';
9
+
10
+ export const statusCommand = new Command('status')
11
+ .description('Check translation status')
12
+ .option('-c, --collection <path>', 'Specific collection to check')
13
+ .option('--json', 'Output as JSON')
14
+ .action(async (options) => {
15
+ const chalk = (await import('chalk')).default;
16
+ const ora = (await import('ora')).default;
17
+
18
+ // Load config
19
+ const configSpinner = ora('Loading melaka.config.ts...').start();
20
+ let config;
21
+
22
+ try {
23
+ config = await loadConfig();
24
+ configSpinner.succeed('Loaded config');
25
+ } catch (error) {
26
+ configSpinner.fail('Failed to load config');
27
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
28
+ process.exit(1);
29
+ }
30
+
31
+ // Note: Actual status checking requires Firebase Admin SDK
32
+ // This is a placeholder showing the config
33
+ console.log('');
34
+ console.log(chalk.cyan('Melaka Configuration:'));
35
+ console.log('');
36
+
37
+ console.log(chalk.white('AI Provider:'));
38
+ console.log(chalk.gray(` Provider: ${config.ai.provider}`));
39
+ console.log(chalk.gray(` Model: ${config.ai.model}`));
40
+ console.log(chalk.gray(` Region: ${config.region || 'us-central1'}`));
41
+ console.log('');
42
+
43
+ console.log(chalk.white('Languages:'));
44
+ for (const lang of config.languages) {
45
+ console.log(chalk.gray(` - ${lang} (${getLanguageName(lang)})`));
46
+ }
47
+ console.log('');
48
+
49
+ console.log(chalk.white('Collections:'));
50
+ for (const collection of config.collections) {
51
+ const fields = collection.fields?.join(', ') || 'auto-detect';
52
+ console.log(chalk.gray(` ${collection.path}`));
53
+ console.log(chalk.gray(` Fields: ${fields}`));
54
+ if (collection.prompt) {
55
+ const shortPrompt = collection.prompt.length > 50
56
+ ? collection.prompt.substring(0, 50) + '...'
57
+ : collection.prompt;
58
+ console.log(chalk.gray(` Prompt: ${shortPrompt}`));
59
+ }
60
+ }
61
+ console.log('');
62
+
63
+ if (config.glossary && Object.keys(config.glossary).length > 0) {
64
+ console.log(chalk.white('Shared Glossary:'));
65
+ for (const [term, translation] of Object.entries(config.glossary)) {
66
+ console.log(chalk.gray(` ${term} → ${translation}`));
67
+ }
68
+ console.log('');
69
+ }
70
+
71
+ console.log(chalk.yellow('⚠️ Live status checking requires Firebase connection.'));
72
+ console.log(chalk.gray(' Status will show actual translation progress in Phase 2.'));
73
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Melaka CLI - Translate Command
3
+ *
4
+ * Manually trigger translation for collections.
5
+ */
6
+
7
+ import { Command } from 'commander';
8
+ import { loadConfig, findCollectionConfig } from '@melaka/core';
9
+
10
+ export const translateCommand = new Command('translate')
11
+ .description('Manually translate a collection')
12
+ .option('-c, --collection <path>', 'Collection path to translate')
13
+ .option('-l, --language <code>', 'Specific language to translate')
14
+ .option('--force', 'Force re-translation of all documents')
15
+ .option('--dry-run', 'Show what would be translated without doing it')
16
+ .action(async (options) => {
17
+ const chalk = (await import('chalk')).default;
18
+ const ora = (await import('ora')).default;
19
+
20
+ // Load config
21
+ const configSpinner = ora('Loading melaka.config.ts...').start();
22
+ let config;
23
+
24
+ try {
25
+ config = await loadConfig();
26
+ configSpinner.succeed('Loaded config');
27
+ } catch (error) {
28
+ configSpinner.fail('Failed to load config');
29
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
30
+ process.exit(1);
31
+ }
32
+
33
+ // Determine which collections to translate
34
+ let collectionsToTranslate = config.collections;
35
+
36
+ if (options.collection) {
37
+ const collectionConfig = findCollectionConfig(config, options.collection);
38
+ if (!collectionConfig) {
39
+ console.error(chalk.red(`Collection not found in config: ${options.collection}`));
40
+ console.log(chalk.gray('Available collections:'));
41
+ for (const c of config.collections) {
42
+ console.log(chalk.gray(` - ${c.path}`));
43
+ }
44
+ process.exit(1);
45
+ }
46
+ collectionsToTranslate = [collectionConfig];
47
+ }
48
+
49
+ // Determine which languages to translate
50
+ let languagesToTranslate = config.languages;
51
+
52
+ if (options.language) {
53
+ if (!config.languages.includes(options.language)) {
54
+ console.error(chalk.red(`Language not found in config: ${options.language}`));
55
+ console.log(chalk.gray('Available languages:'));
56
+ for (const l of config.languages) {
57
+ console.log(chalk.gray(` - ${l}`));
58
+ }
59
+ process.exit(1);
60
+ }
61
+ languagesToTranslate = [options.language];
62
+ }
63
+
64
+ // Show what will be translated
65
+ console.log('');
66
+ console.log(chalk.cyan('Translation Plan:'));
67
+ console.log(chalk.gray(` Collections: ${collectionsToTranslate.map((c) => c.path).join(', ')}`));
68
+ console.log(chalk.gray(` Languages: ${languagesToTranslate.join(', ')}`));
69
+ console.log(chalk.gray(` Force: ${options.force ? 'yes' : 'no'}`));
70
+ console.log('');
71
+
72
+ if (options.dryRun) {
73
+ console.log(chalk.yellow('Dry run - no translations will be performed.'));
74
+ console.log('');
75
+ console.log(chalk.gray('To perform translations, this command needs to be run'));
76
+ console.log(chalk.gray('in a Firebase Functions environment or with emulators.'));
77
+ console.log('');
78
+ console.log(chalk.gray('For now, use triggers or the Firebase console to trigger translations.'));
79
+ return;
80
+ }
81
+
82
+ // Note: Actual translation requires Firebase Admin SDK
83
+ // This is a placeholder for the MVP
84
+ console.log(chalk.yellow('⚠️ Manual translation requires Firebase Functions environment.'));
85
+ console.log('');
86
+ console.log('To translate documents:');
87
+ console.log(chalk.gray(' 1. Deploy triggers: melaka deploy'));
88
+ console.log(chalk.gray(' 2. Update a document to trigger translation'));
89
+ console.log(chalk.gray(' 3. Or use Firebase Console to call the Cloud Task directly'));
90
+ console.log('');
91
+ console.log(chalk.gray('Batch translation via CLI will be available in Phase 2.'));
92
+ });