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
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "melaka",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "AI-powered localization for Firebase Firestore",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/rizahassan/melaka.git"
|
|
8
|
+
},
|
|
9
|
+
"author": "Riza Hassan <vehan.apps@gmail.com>",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"firebase",
|
|
13
|
+
"firestore",
|
|
14
|
+
"i18n",
|
|
15
|
+
"localization",
|
|
16
|
+
"translation",
|
|
17
|
+
"ai",
|
|
18
|
+
"gemini",
|
|
19
|
+
"openai",
|
|
20
|
+
"claude"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.0.0"
|
|
24
|
+
},
|
|
25
|
+
"packageManager": "pnpm@8.15.0",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "turbo run build",
|
|
28
|
+
"dev": "turbo run dev",
|
|
29
|
+
"test": "turbo run test",
|
|
30
|
+
"test:watch": "turbo run test:watch",
|
|
31
|
+
"lint": "turbo run lint",
|
|
32
|
+
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
|
33
|
+
"typecheck": "turbo run typecheck",
|
|
34
|
+
"clean": "turbo run clean && rm -rf node_modules",
|
|
35
|
+
"changeset": "changeset",
|
|
36
|
+
"version-packages": "changeset version",
|
|
37
|
+
"release": "turbo run build && changeset publish"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@changesets/cli": "^2.27.0",
|
|
41
|
+
"prettier": "^3.2.0",
|
|
42
|
+
"turbo": "^2.0.0",
|
|
43
|
+
"typescript": "^5.3.0",
|
|
44
|
+
"vitest": "^1.2.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @melaka/ai
|
|
2
|
+
|
|
3
|
+
AI provider adapters for Melaka - AI-powered Firestore localization.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @melaka/ai
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createTranslationFacade } from '@melaka/ai';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
|
|
17
|
+
const facade = createTranslationFacade({
|
|
18
|
+
provider: 'gemini',
|
|
19
|
+
model: 'gemini-2.5-flash',
|
|
20
|
+
apiKey: process.env.GEMINI_API_KEY,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const result = await facade.translate(
|
|
24
|
+
{ title: 'Hello World', body: 'Welcome to our app' },
|
|
25
|
+
z.object({ title: z.string(), body: z.string() }),
|
|
26
|
+
{ targetLanguage: 'ms-MY' }
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (result.success) {
|
|
30
|
+
console.log(result.output);
|
|
31
|
+
// { title: 'Hello Dunia', body: 'Selamat datang ke aplikasi kami' }
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Supported Providers
|
|
36
|
+
|
|
37
|
+
- **Gemini** (Google) - `gemini-2.5-flash`, `gemini-2.5-pro`
|
|
38
|
+
- **OpenAI** - `gpt-4o`, `gpt-4o-mini` (Phase 2)
|
|
39
|
+
- **Claude** (Anthropic) - `claude-sonnet-4-20250514` (Phase 2)
|
|
40
|
+
|
|
41
|
+
## Documentation
|
|
42
|
+
|
|
43
|
+
See the [main Melaka documentation](https://github.com/rizahassan/melaka).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@melaka/ai",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "AI provider adapters 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
|
+
"zod": "^3.22.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"tsup": "^8.0.0",
|
|
33
|
+
"typescript": "^5.3.0",
|
|
34
|
+
"vitest": "^1.2.0"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/rizahassan/melaka.git",
|
|
39
|
+
"directory": "packages/ai"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT"
|
|
42
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Melaka AI - Translation Facade
|
|
3
|
+
*
|
|
4
|
+
* High-level API for translating content using any configured AI provider.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { z } from 'zod';
|
|
8
|
+
import type { AIConfig } from '@melaka/core';
|
|
9
|
+
import type { AIProvider, ProviderConfig, TranslationOptions, TranslationResponse } from './types';
|
|
10
|
+
import { GeminiProvider } from './providers/gemini';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Translation facade for unified AI provider access.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const facade = createTranslationFacade({
|
|
18
|
+
* provider: 'gemini',
|
|
19
|
+
* model: 'gemini-2.5-flash',
|
|
20
|
+
* apiKey: process.env.GEMINI_API_KEY,
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* const result = await facade.translate(
|
|
24
|
+
* { title: 'Hello', body: 'Welcome' },
|
|
25
|
+
* z.object({ title: z.string(), body: z.string() }),
|
|
26
|
+
* { targetLanguage: 'ms-MY' }
|
|
27
|
+
* );
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export class TranslationFacade {
|
|
31
|
+
private provider: AIProvider;
|
|
32
|
+
|
|
33
|
+
constructor(aiConfig: AIConfig) {
|
|
34
|
+
this.provider = createProvider(aiConfig);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Translate content using the configured AI provider.
|
|
39
|
+
*
|
|
40
|
+
* @param content - Content to translate (translatable fields only)
|
|
41
|
+
* @param schema - Zod schema defining expected output structure
|
|
42
|
+
* @param options - Translation options
|
|
43
|
+
* @returns Translation result
|
|
44
|
+
*/
|
|
45
|
+
async translate<T>(
|
|
46
|
+
content: Record<string, unknown>,
|
|
47
|
+
schema: z.ZodSchema<T>,
|
|
48
|
+
options: TranslationOptions
|
|
49
|
+
): Promise<TranslationResponse<T>> {
|
|
50
|
+
return this.provider.translate(content, schema, options);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the underlying AI provider.
|
|
55
|
+
*/
|
|
56
|
+
getProvider(): AIProvider {
|
|
57
|
+
return this.provider;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get the provider name.
|
|
62
|
+
*/
|
|
63
|
+
getProviderName(): string {
|
|
64
|
+
return this.provider.name;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the model being used.
|
|
69
|
+
*/
|
|
70
|
+
getModel(): string {
|
|
71
|
+
return this.provider.getModel();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if the facade is properly configured.
|
|
76
|
+
*/
|
|
77
|
+
isConfigured(): boolean {
|
|
78
|
+
return this.provider.isConfigured();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create an AI provider based on configuration.
|
|
84
|
+
*
|
|
85
|
+
* @param aiConfig - AI configuration from melaka.config.ts
|
|
86
|
+
* @returns Configured AI provider
|
|
87
|
+
*/
|
|
88
|
+
function createProvider(aiConfig: AIConfig): AIProvider {
|
|
89
|
+
const providerConfig: ProviderConfig = {
|
|
90
|
+
apiKey: aiConfig.apiKey,
|
|
91
|
+
model: aiConfig.model,
|
|
92
|
+
temperature: aiConfig.temperature,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
switch (aiConfig.provider) {
|
|
96
|
+
case 'gemini':
|
|
97
|
+
return new GeminiProvider(providerConfig);
|
|
98
|
+
|
|
99
|
+
case 'openai':
|
|
100
|
+
// TODO: Implement OpenAI provider in Phase 2
|
|
101
|
+
throw new Error('OpenAI provider not yet implemented. Use Gemini for MVP.');
|
|
102
|
+
|
|
103
|
+
case 'claude':
|
|
104
|
+
// TODO: Implement Claude provider in Phase 2
|
|
105
|
+
throw new Error('Claude provider not yet implemented. Use Gemini for MVP.');
|
|
106
|
+
|
|
107
|
+
default:
|
|
108
|
+
throw new Error(`Unknown AI provider: ${aiConfig.provider}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create a translation facade instance.
|
|
114
|
+
*
|
|
115
|
+
* @param aiConfig - AI configuration
|
|
116
|
+
* @returns Configured translation facade
|
|
117
|
+
*/
|
|
118
|
+
export function createTranslationFacade(aiConfig: AIConfig): TranslationFacade {
|
|
119
|
+
return new TranslationFacade(aiConfig);
|
|
120
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @melaka/ai
|
|
3
|
+
*
|
|
4
|
+
* AI provider adapters for Melaka - AI-powered Firestore localization.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Types
|
|
10
|
+
export type {
|
|
11
|
+
AIProvider,
|
|
12
|
+
ProviderConfig,
|
|
13
|
+
TranslationOptions,
|
|
14
|
+
TranslationResponse,
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
// Providers
|
|
18
|
+
export {
|
|
19
|
+
GeminiProvider,
|
|
20
|
+
createGeminiProvider,
|
|
21
|
+
} from './providers';
|
|
22
|
+
|
|
23
|
+
// Facade
|
|
24
|
+
export {
|
|
25
|
+
TranslationFacade,
|
|
26
|
+
createTranslationFacade,
|
|
27
|
+
} from './facade';
|
|
28
|
+
|
|
29
|
+
// Prompt utilities
|
|
30
|
+
export {
|
|
31
|
+
buildTranslationPrompt,
|
|
32
|
+
buildSystemPrompt,
|
|
33
|
+
extractJsonFromResponse,
|
|
34
|
+
} from './prompt';
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Melaka AI - Prompt Builder
|
|
3
|
+
*
|
|
4
|
+
* Constructs prompts for AI translation with context, glossaries, and instructions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { formatGlossary, getLanguageName } from '@melaka/core';
|
|
8
|
+
import type { TranslationOptions } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build a translation prompt for the AI provider.
|
|
12
|
+
*
|
|
13
|
+
* @param content - Content to translate
|
|
14
|
+
* @param options - Translation options
|
|
15
|
+
* @returns Formatted prompt string
|
|
16
|
+
*/
|
|
17
|
+
export function buildTranslationPrompt(
|
|
18
|
+
content: Record<string, unknown>,
|
|
19
|
+
options: TranslationOptions
|
|
20
|
+
): string {
|
|
21
|
+
const { targetLanguage, prompt, glossary } = options;
|
|
22
|
+
const languageName = getLanguageName(targetLanguage);
|
|
23
|
+
|
|
24
|
+
const sections: string[] = [];
|
|
25
|
+
|
|
26
|
+
// Main instruction
|
|
27
|
+
sections.push(`Translate the following content from English to ${languageName} (${targetLanguage}).`);
|
|
28
|
+
|
|
29
|
+
// Preservation rules
|
|
30
|
+
sections.push(`
|
|
31
|
+
Preserve exactly:
|
|
32
|
+
- JSON structure and field names
|
|
33
|
+
- Markdown formatting (headers, bold, italic, links, code blocks)
|
|
34
|
+
- Proper nouns (names, brands, places) unless glossary specifies otherwise
|
|
35
|
+
- Numbers, dates, and measurements
|
|
36
|
+
- URLs, email addresses, and code snippets
|
|
37
|
+
- Emoji and special characters`);
|
|
38
|
+
|
|
39
|
+
// Custom context
|
|
40
|
+
if (prompt) {
|
|
41
|
+
sections.push(`
|
|
42
|
+
Context:
|
|
43
|
+
${prompt}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Glossary
|
|
47
|
+
sections.push(`
|
|
48
|
+
Glossary (use these translations consistently):
|
|
49
|
+
${formatGlossary(glossary)}`);
|
|
50
|
+
|
|
51
|
+
// Content to translate
|
|
52
|
+
sections.push(`
|
|
53
|
+
Content to translate:
|
|
54
|
+
${JSON.stringify(content, null, 2)}`);
|
|
55
|
+
|
|
56
|
+
// Output instruction
|
|
57
|
+
sections.push(`
|
|
58
|
+
Respond with ONLY the translated JSON object. Do not include any explanation or markdown code blocks.`);
|
|
59
|
+
|
|
60
|
+
return sections.join('\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build a system prompt for translation tasks.
|
|
65
|
+
*
|
|
66
|
+
* @returns System prompt string
|
|
67
|
+
*/
|
|
68
|
+
export function buildSystemPrompt(): string {
|
|
69
|
+
return `You are a professional translator specializing in software localization.
|
|
70
|
+
|
|
71
|
+
Your task is to translate content while:
|
|
72
|
+
1. Maintaining the exact JSON structure
|
|
73
|
+
2. Preserving technical terms, brand names, and proper nouns
|
|
74
|
+
3. Adapting idioms and cultural references appropriately
|
|
75
|
+
4. Ensuring natural, fluent translations in the target language
|
|
76
|
+
5. Following any glossary terms provided exactly
|
|
77
|
+
|
|
78
|
+
Always respond with valid JSON that matches the input structure.`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extract JSON from AI response that may contain markdown or extra text.
|
|
83
|
+
*
|
|
84
|
+
* @param response - Raw AI response
|
|
85
|
+
* @returns Parsed JSON object
|
|
86
|
+
* @throws Error if no valid JSON found
|
|
87
|
+
*/
|
|
88
|
+
export function extractJsonFromResponse(response: string): Record<string, unknown> {
|
|
89
|
+
// Try direct parse first
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(response);
|
|
92
|
+
} catch {
|
|
93
|
+
// Continue to extraction
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Try to extract from markdown code block
|
|
97
|
+
const codeBlockMatch = response.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
98
|
+
if (codeBlockMatch) {
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(codeBlockMatch[1].trim());
|
|
101
|
+
} catch {
|
|
102
|
+
// Continue to other methods
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Try to find JSON object in the response
|
|
107
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
108
|
+
if (jsonMatch) {
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(jsonMatch[0]);
|
|
111
|
+
} catch {
|
|
112
|
+
// Fall through to error
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
throw new Error('Failed to extract valid JSON from AI response');
|
|
117
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Melaka AI - Gemini Provider
|
|
3
|
+
*
|
|
4
|
+
* AI provider adapter for Google's Gemini models.
|
|
5
|
+
* Uses direct Gemini API for MVP (Genkit integration in Phase 2).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { z } from 'zod';
|
|
9
|
+
import type { AIProvider, ProviderConfig, TranslationOptions, TranslationResponse } from '../types';
|
|
10
|
+
import { buildTranslationPrompt, buildSystemPrompt, extractJsonFromResponse } from '../prompt';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gemini API response structure.
|
|
14
|
+
*/
|
|
15
|
+
interface GeminiResponse {
|
|
16
|
+
candidates?: Array<{
|
|
17
|
+
content?: {
|
|
18
|
+
parts?: Array<{
|
|
19
|
+
text?: string;
|
|
20
|
+
}>;
|
|
21
|
+
};
|
|
22
|
+
}>;
|
|
23
|
+
usageMetadata?: {
|
|
24
|
+
promptTokenCount?: number;
|
|
25
|
+
candidatesTokenCount?: number;
|
|
26
|
+
totalTokenCount?: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Gemini AI provider.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const provider = new GeminiProvider({
|
|
36
|
+
* apiKey: process.env.GEMINI_API_KEY,
|
|
37
|
+
* model: 'gemini-2.5-flash',
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* const result = await provider.translate(
|
|
41
|
+
* { title: 'Hello' },
|
|
42
|
+
* z.object({ title: z.string() }),
|
|
43
|
+
* { targetLanguage: 'ms-MY' }
|
|
44
|
+
* );
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export class GeminiProvider implements AIProvider {
|
|
48
|
+
readonly name = 'gemini';
|
|
49
|
+
|
|
50
|
+
private config: ProviderConfig;
|
|
51
|
+
|
|
52
|
+
constructor(config: ProviderConfig) {
|
|
53
|
+
this.config = {
|
|
54
|
+
temperature: 0.3,
|
|
55
|
+
...config,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async translate<T>(
|
|
60
|
+
content: Record<string, unknown>,
|
|
61
|
+
schema: z.ZodSchema<T>,
|
|
62
|
+
options: TranslationOptions
|
|
63
|
+
): Promise<TranslationResponse<T>> {
|
|
64
|
+
const startTime = Date.now();
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
if (!this.isConfigured()) {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
error: 'Gemini API key not configured',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Build the prompt
|
|
75
|
+
const userPrompt = buildTranslationPrompt(content, {
|
|
76
|
+
...options,
|
|
77
|
+
temperature: options.temperature ?? this.config.temperature,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const systemPrompt = buildSystemPrompt();
|
|
81
|
+
|
|
82
|
+
// Call Gemini API directly
|
|
83
|
+
const response = await fetch(
|
|
84
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${this.config.model}:generateContent?key=${this.config.apiKey}`,
|
|
85
|
+
{
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
contents: [
|
|
92
|
+
{
|
|
93
|
+
parts: [{ text: userPrompt }],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
systemInstruction: {
|
|
97
|
+
parts: [{ text: systemPrompt }],
|
|
98
|
+
},
|
|
99
|
+
generationConfig: {
|
|
100
|
+
temperature: options.temperature ?? this.config.temperature,
|
|
101
|
+
responseMimeType: 'application/json',
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const durationMs = Date.now() - startTime;
|
|
108
|
+
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
const errorData = await response.json().catch(() => ({}));
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
error: `Gemini API error: ${response.status} ${JSON.stringify(errorData)}`,
|
|
114
|
+
durationMs,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const data = await response.json() as GeminiResponse;
|
|
119
|
+
|
|
120
|
+
// Extract text from response
|
|
121
|
+
const textResponse = data.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
122
|
+
|
|
123
|
+
if (!textResponse) {
|
|
124
|
+
return {
|
|
125
|
+
success: false,
|
|
126
|
+
error: 'No response from Gemini',
|
|
127
|
+
durationMs,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Parse and validate
|
|
132
|
+
const parsed = extractJsonFromResponse(textResponse);
|
|
133
|
+
const validated = schema.safeParse(parsed);
|
|
134
|
+
|
|
135
|
+
if (!validated.success) {
|
|
136
|
+
return {
|
|
137
|
+
success: false,
|
|
138
|
+
error: `Schema validation failed: ${validated.error.message}`,
|
|
139
|
+
durationMs,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Extract usage metadata
|
|
144
|
+
const usageMetadata = data.usageMetadata;
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
output: validated.data,
|
|
149
|
+
usage: usageMetadata
|
|
150
|
+
? {
|
|
151
|
+
inputTokens: usageMetadata.promptTokenCount || 0,
|
|
152
|
+
outputTokens: usageMetadata.candidatesTokenCount || 0,
|
|
153
|
+
totalTokens: usageMetadata.totalTokenCount || 0,
|
|
154
|
+
}
|
|
155
|
+
: undefined,
|
|
156
|
+
durationMs,
|
|
157
|
+
};
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const durationMs = Date.now() - startTime;
|
|
160
|
+
return {
|
|
161
|
+
success: false,
|
|
162
|
+
error: error instanceof Error ? error.message : String(error),
|
|
163
|
+
durationMs,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
isConfigured(): boolean {
|
|
169
|
+
return !!this.config.apiKey;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getModel(): string {
|
|
173
|
+
return this.config.model;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create a Gemini provider instance.
|
|
179
|
+
*
|
|
180
|
+
* @param config - Provider configuration
|
|
181
|
+
* @returns Configured Gemini provider
|
|
182
|
+
*/
|
|
183
|
+
export function createGeminiProvider(config: ProviderConfig): GeminiProvider {
|
|
184
|
+
return new GeminiProvider(config);
|
|
185
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Melaka AI - Provider Exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { GeminiProvider, createGeminiProvider } from './gemini';
|
|
6
|
+
|
|
7
|
+
// Future providers:
|
|
8
|
+
// export { OpenAIProvider, createOpenAIProvider } from './openai';
|
|
9
|
+
// export { ClaudeProvider, createClaudeProvider } from './claude';
|