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,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
|
+
});
|