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
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';