diff --git a/client/src/pages/DeployPage.tsx b/client/src/pages/DeployPage.tsx index e447d19..973fd8d 100644 --- a/client/src/pages/DeployPage.tsx +++ b/client/src/pages/DeployPage.tsx @@ -34,7 +34,7 @@ export default function DeployPage() { const response = await fetch('/api/deploy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name }), + body: JSON.stringify({ name, lang }), }); const data = await response.json(); diff --git a/src/index.ts b/src/index.ts index 7a13297..c1af764 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,9 +10,10 @@ import type { DeploymentState as OrchestratorDeploymentState } from './orchestra const PORT = parseInt(process.env.PORT || '3000', 10); const HOST = process.env.HOST || '0.0.0.0'; -// Extended deployment state for HTTP server (adds logs) +// Extended deployment state for HTTP server (adds logs and language) interface HttpDeploymentState extends OrchestratorDeploymentState { logs: string[]; + lang: string; } const deployments = new Map(); @@ -90,6 +91,7 @@ async function deployStack(deploymentId: string): Promise { registryId: process.env.STACK_REGISTRY_ID, sharedProjectId: process.env.SHARED_PROJECT_ID, sharedEnvironmentId: process.env.SHARED_ENVIRONMENT_ID, + lang: deployment.lang, }); // Final update with logs @@ -144,7 +146,7 @@ app.get('/health', (c) => { app.post('/api/deploy', async (c) => { try { const body = await c.req.json(); - const { name } = body; + const { name, lang = 'en' } = body; // Validate name const validation = validateStackName(name); @@ -197,6 +199,7 @@ app.post('/api/deploy', async (c) => { started: new Date().toISOString(), }, logs: [], + lang, }; deployments.set(deploymentId, deployment); diff --git a/src/lib/i18n-backend.ts b/src/lib/i18n-backend.ts new file mode 100644 index 0000000..1953409 --- /dev/null +++ b/src/lib/i18n-backend.ts @@ -0,0 +1,65 @@ +export const backendTranslations = { + en: { + 'initializing': 'Initializing deployment', + 'creatingProject': 'Creating project', + 'gettingEnvironment': 'Getting environment ID', + 'environmentAvailable': 'Environment ID already available', + 'environmentRetrieved': 'Environment ID retrieved', + 'creatingApplication': 'Creating application', + 'configuringApplication': 'Configuring application', + 'creatingDomain': 'Creating domain', + 'deployingApplication': 'Deploying application', + 'waitingForSSL': 'Waiting for SSL certificate provisioning...', + 'waitingForStart': 'Waiting for application to start', + 'deploymentSuccess': 'Application deployed successfully', + 'verifyingHealth': 'Verifying application health', + }, + nl: { + 'initializing': 'Implementatie initialiseren', + 'creatingProject': 'Project aanmaken', + 'gettingEnvironment': 'Omgeving ID ophalen', + 'environmentAvailable': 'Omgeving ID al beschikbaar', + 'environmentRetrieved': 'Omgeving ID opgehaald', + 'creatingApplication': 'Applicatie aanmaken', + 'configuringApplication': 'Applicatie configureren', + 'creatingDomain': 'Domein aanmaken', + 'deployingApplication': 'Applicatie implementeren', + 'waitingForSSL': 'Wachten op SSL-certificaat...', + 'waitingForStart': 'Wachten tot applicatie start', + 'deploymentSuccess': 'Applicatie succesvol geïmplementeerd', + 'verifyingHealth': 'Applicatie gezondheid verifiëren', + }, + ar: { + 'initializing': 'جاري التهيئة', + 'creatingProject': 'إنشاء المشروع', + 'gettingEnvironment': 'الحصول على معرف البيئة', + 'environmentAvailable': 'معرف البيئة متاح بالفعل', + 'environmentRetrieved': 'تم استرداد معرف البيئة', + 'creatingApplication': 'إنشاء التطبيق', + 'configuringApplication': 'تكوين التطبيق', + 'creatingDomain': 'إنشاء النطاق', + 'deployingApplication': 'نشر التطبيق', + 'waitingForSSL': 'انتظار شهادة SSL...', + 'waitingForStart': 'انتظار بدء التطبيق', + 'deploymentSuccess': 'تم نشر التطبيق بنجاح', + 'verifyingHealth': 'التحقق من صحة التطبيق', + }, +} as const; + +export type BackendLanguage = keyof typeof backendTranslations; +export type BackendTranslationKey = keyof typeof backendTranslations.en; + +export function createTranslator(lang: BackendLanguage = 'en') { + return (key: BackendTranslationKey, params?: Record): string => { + const translations = backendTranslations[lang] || backendTranslations.en; + let text: string = translations[key]; + + if (params) { + Object.entries(params).forEach(([paramKey, value]) => { + text = text.replace(`{${paramKey}}`, String(value)); + }); + } + + return text; + }; +} diff --git a/src/orchestrator/production-deployer.ts b/src/orchestrator/production-deployer.ts index 50df463..5efd165 100644 --- a/src/orchestrator/production-deployer.ts +++ b/src/orchestrator/production-deployer.ts @@ -11,6 +11,7 @@ */ import { DokployProductionClient } from '../api/dokploy-production.js'; +import { createTranslator, type BackendLanguage } from '../lib/i18n-backend.js'; export interface DeploymentConfig { stackName: string; @@ -22,6 +23,7 @@ export interface DeploymentConfig { registryId?: string; sharedProjectId?: string; sharedEnvironmentId?: string; + lang?: string; } export interface DeploymentState { @@ -71,10 +73,12 @@ export type ProgressCallback = (state: DeploymentState) => void; export class ProductionDeployer { private client: DokployProductionClient; private progressCallback?: ProgressCallback; + private t: ReturnType; constructor(client: DokployProductionClient, progressCallback?: ProgressCallback) { this.client = client; this.progressCallback = progressCallback; + this.t = createTranslator('en'); } private notifyProgress(state: DeploymentState): void { @@ -87,13 +91,15 @@ export class ProductionDeployer { * Deploy a complete AI stack with full production safeguards */ async deploy(config: DeploymentConfig): Promise { + this.t = createTranslator((config.lang || 'en') as BackendLanguage); + const state: DeploymentState = { id: `dep_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, stackName: config.stackName, phase: 'initializing', status: 'in_progress', progress: 0, - message: 'Initializing deployment', + message: this.t('initializing'), resources: {}, timestamps: { started: new Date().toISOString(), @@ -228,12 +234,12 @@ export class ProductionDeployer { private async getEnvironment(state: DeploymentState): Promise { state.phase = 'getting_environment'; state.progress = 25; - state.message = 'Getting environment ID'; + state.message = this.t('gettingEnvironment'); // Skip if we already have environment ID from project creation if (state.resources.environmentId) { console.log('Environment ID already available from project creation'); - state.message = 'Environment ID already available'; + state.message = this.t('environmentAvailable'); return; } @@ -243,7 +249,7 @@ export class ProductionDeployer { const environment = await this.client.getDefaultEnvironment(state.resources.projectId); state.resources.environmentId = environment.environmentId; - state.message = 'Environment ID retrieved'; + state.message = this.t('environmentRetrieved'); } private async createOrFindApplication( @@ -252,7 +258,7 @@ export class ProductionDeployer { ): Promise { state.phase = 'creating_application'; state.progress = 40; - state.message = 'Creating application'; + state.message = this.t('creatingApplication'); if (!state.resources.environmentId) { throw new Error('Environment ID not available'); @@ -279,7 +285,7 @@ export class ProductionDeployer { ): Promise { state.phase = 'configuring_application'; state.progress = 50; - state.message = 'Configuring application with Docker image'; + state.message = this.t('configuringApplication'); if (!state.resources.applicationId) { throw new Error('Application ID not available'); @@ -332,7 +338,7 @@ export class ProductionDeployer { ): Promise { state.phase = 'creating_domain'; state.progress = 70; - state.message = 'Creating domain'; + state.message = this.t('creatingDomain'); if (!state.resources.applicationId) { throw new Error('Application ID not available'); @@ -359,7 +365,7 @@ export class ProductionDeployer { private async deployApplication(state: DeploymentState): Promise { state.phase = 'deploying'; state.progress = 85; - state.message = 'Triggering deployment'; + state.message = this.t('deployingApplication'); if (!state.resources.applicationId) { throw new Error('Application ID not available'); @@ -375,7 +381,7 @@ export class ProductionDeployer { ): Promise { state.phase = 'verifying_health'; state.progress = 95; - state.message = 'Verifying application status via Dokploy'; + state.message = this.t('verifyingHealth'); if (!state.resources.applicationId) { throw new Error('Application ID not available'); @@ -392,13 +398,13 @@ export class ProductionDeployer { console.log(`Application status: ${appStatus}`); if (appStatus === 'done') { - state.message = 'Waiting for SSL certificate provisioning...'; + state.message = this.t('waitingForSSL'); state.progress = 98; this.notifyProgress(state); await this.sleep(15000); - state.message = 'Application deployed successfully'; + state.message = this.t('deploymentSuccess'); return; } @@ -410,7 +416,7 @@ export class ProductionDeployer { } const elapsed = Math.round((Date.now() - startTime) / 1000); - state.message = `Waiting for application to start (${elapsed}s)...`; + state.message = `${this.t('waitingForStart')} (${elapsed}s)...`; this.notifyProgress(state); await this.sleep(interval);