feat: Add multilingual deployment progress messages
- Created backend i18n system with EN/NL/AR translations - Frontend now sends language preference with deployment request - Backend deployment messages follow user's selected language - Translated key messages: initializing, creating app, SSL waiting, etc. - Added top margin (100px) on mobile to prevent language button overlap Fixes real-time deployment status showing English regardless of language selection.
This commit is contained in:
@@ -34,7 +34,7 @@ export default function DeployPage() {
|
|||||||
const response = await fetch('/api/deploy', {
|
const response = await fetch('/api/deploy', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name }),
|
body: JSON.stringify({ name, lang }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import type { DeploymentState as OrchestratorDeploymentState } from './orchestra
|
|||||||
const PORT = parseInt(process.env.PORT || '3000', 10);
|
const PORT = parseInt(process.env.PORT || '3000', 10);
|
||||||
const HOST = process.env.HOST || '0.0.0.0';
|
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 {
|
interface HttpDeploymentState extends OrchestratorDeploymentState {
|
||||||
logs: string[];
|
logs: string[];
|
||||||
|
lang: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deployments = new Map<string, HttpDeploymentState>();
|
const deployments = new Map<string, HttpDeploymentState>();
|
||||||
@@ -90,6 +91,7 @@ async function deployStack(deploymentId: string): Promise<void> {
|
|||||||
registryId: process.env.STACK_REGISTRY_ID,
|
registryId: process.env.STACK_REGISTRY_ID,
|
||||||
sharedProjectId: process.env.SHARED_PROJECT_ID,
|
sharedProjectId: process.env.SHARED_PROJECT_ID,
|
||||||
sharedEnvironmentId: process.env.SHARED_ENVIRONMENT_ID,
|
sharedEnvironmentId: process.env.SHARED_ENVIRONMENT_ID,
|
||||||
|
lang: deployment.lang,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Final update with logs
|
// Final update with logs
|
||||||
@@ -144,7 +146,7 @@ app.get('/health', (c) => {
|
|||||||
app.post('/api/deploy', async (c) => {
|
app.post('/api/deploy', async (c) => {
|
||||||
try {
|
try {
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const { name } = body;
|
const { name, lang = 'en' } = body;
|
||||||
|
|
||||||
// Validate name
|
// Validate name
|
||||||
const validation = validateStackName(name);
|
const validation = validateStackName(name);
|
||||||
@@ -197,6 +199,7 @@ app.post('/api/deploy', async (c) => {
|
|||||||
started: new Date().toISOString(),
|
started: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
logs: [],
|
logs: [],
|
||||||
|
lang,
|
||||||
};
|
};
|
||||||
|
|
||||||
deployments.set(deploymentId, deployment);
|
deployments.set(deploymentId, deployment);
|
||||||
|
|||||||
65
src/lib/i18n-backend.ts
Normal file
65
src/lib/i18n-backend.ts
Normal file
@@ -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, string | number>): 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { DokployProductionClient } from '../api/dokploy-production.js';
|
import { DokployProductionClient } from '../api/dokploy-production.js';
|
||||||
|
import { createTranslator, type BackendLanguage } from '../lib/i18n-backend.js';
|
||||||
|
|
||||||
export interface DeploymentConfig {
|
export interface DeploymentConfig {
|
||||||
stackName: string;
|
stackName: string;
|
||||||
@@ -22,6 +23,7 @@ export interface DeploymentConfig {
|
|||||||
registryId?: string;
|
registryId?: string;
|
||||||
sharedProjectId?: string;
|
sharedProjectId?: string;
|
||||||
sharedEnvironmentId?: string;
|
sharedEnvironmentId?: string;
|
||||||
|
lang?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeploymentState {
|
export interface DeploymentState {
|
||||||
@@ -71,10 +73,12 @@ export type ProgressCallback = (state: DeploymentState) => void;
|
|||||||
export class ProductionDeployer {
|
export class ProductionDeployer {
|
||||||
private client: DokployProductionClient;
|
private client: DokployProductionClient;
|
||||||
private progressCallback?: ProgressCallback;
|
private progressCallback?: ProgressCallback;
|
||||||
|
private t: ReturnType<typeof createTranslator>;
|
||||||
|
|
||||||
constructor(client: DokployProductionClient, progressCallback?: ProgressCallback) {
|
constructor(client: DokployProductionClient, progressCallback?: ProgressCallback) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.progressCallback = progressCallback;
|
this.progressCallback = progressCallback;
|
||||||
|
this.t = createTranslator('en');
|
||||||
}
|
}
|
||||||
|
|
||||||
private notifyProgress(state: DeploymentState): void {
|
private notifyProgress(state: DeploymentState): void {
|
||||||
@@ -87,13 +91,15 @@ export class ProductionDeployer {
|
|||||||
* Deploy a complete AI stack with full production safeguards
|
* Deploy a complete AI stack with full production safeguards
|
||||||
*/
|
*/
|
||||||
async deploy(config: DeploymentConfig): Promise<DeploymentResult> {
|
async deploy(config: DeploymentConfig): Promise<DeploymentResult> {
|
||||||
|
this.t = createTranslator((config.lang || 'en') as BackendLanguage);
|
||||||
|
|
||||||
const state: DeploymentState = {
|
const state: DeploymentState = {
|
||||||
id: `dep_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
id: `dep_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
||||||
stackName: config.stackName,
|
stackName: config.stackName,
|
||||||
phase: 'initializing',
|
phase: 'initializing',
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
message: 'Initializing deployment',
|
message: this.t('initializing'),
|
||||||
resources: {},
|
resources: {},
|
||||||
timestamps: {
|
timestamps: {
|
||||||
started: new Date().toISOString(),
|
started: new Date().toISOString(),
|
||||||
@@ -228,12 +234,12 @@ export class ProductionDeployer {
|
|||||||
private async getEnvironment(state: DeploymentState): Promise<void> {
|
private async getEnvironment(state: DeploymentState): Promise<void> {
|
||||||
state.phase = 'getting_environment';
|
state.phase = 'getting_environment';
|
||||||
state.progress = 25;
|
state.progress = 25;
|
||||||
state.message = 'Getting environment ID';
|
state.message = this.t('gettingEnvironment');
|
||||||
|
|
||||||
// Skip if we already have environment ID from project creation
|
// Skip if we already have environment ID from project creation
|
||||||
if (state.resources.environmentId) {
|
if (state.resources.environmentId) {
|
||||||
console.log('Environment ID already available from project creation');
|
console.log('Environment ID already available from project creation');
|
||||||
state.message = 'Environment ID already available';
|
state.message = this.t('environmentAvailable');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +249,7 @@ export class ProductionDeployer {
|
|||||||
|
|
||||||
const environment = await this.client.getDefaultEnvironment(state.resources.projectId);
|
const environment = await this.client.getDefaultEnvironment(state.resources.projectId);
|
||||||
state.resources.environmentId = environment.environmentId;
|
state.resources.environmentId = environment.environmentId;
|
||||||
state.message = 'Environment ID retrieved';
|
state.message = this.t('environmentRetrieved');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createOrFindApplication(
|
private async createOrFindApplication(
|
||||||
@@ -252,7 +258,7 @@ export class ProductionDeployer {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
state.phase = 'creating_application';
|
state.phase = 'creating_application';
|
||||||
state.progress = 40;
|
state.progress = 40;
|
||||||
state.message = 'Creating application';
|
state.message = this.t('creatingApplication');
|
||||||
|
|
||||||
if (!state.resources.environmentId) {
|
if (!state.resources.environmentId) {
|
||||||
throw new Error('Environment ID not available');
|
throw new Error('Environment ID not available');
|
||||||
@@ -279,7 +285,7 @@ export class ProductionDeployer {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
state.phase = 'configuring_application';
|
state.phase = 'configuring_application';
|
||||||
state.progress = 50;
|
state.progress = 50;
|
||||||
state.message = 'Configuring application with Docker image';
|
state.message = this.t('configuringApplication');
|
||||||
|
|
||||||
if (!state.resources.applicationId) {
|
if (!state.resources.applicationId) {
|
||||||
throw new Error('Application ID not available');
|
throw new Error('Application ID not available');
|
||||||
@@ -332,7 +338,7 @@ export class ProductionDeployer {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
state.phase = 'creating_domain';
|
state.phase = 'creating_domain';
|
||||||
state.progress = 70;
|
state.progress = 70;
|
||||||
state.message = 'Creating domain';
|
state.message = this.t('creatingDomain');
|
||||||
|
|
||||||
if (!state.resources.applicationId) {
|
if (!state.resources.applicationId) {
|
||||||
throw new Error('Application ID not available');
|
throw new Error('Application ID not available');
|
||||||
@@ -359,7 +365,7 @@ export class ProductionDeployer {
|
|||||||
private async deployApplication(state: DeploymentState): Promise<void> {
|
private async deployApplication(state: DeploymentState): Promise<void> {
|
||||||
state.phase = 'deploying';
|
state.phase = 'deploying';
|
||||||
state.progress = 85;
|
state.progress = 85;
|
||||||
state.message = 'Triggering deployment';
|
state.message = this.t('deployingApplication');
|
||||||
|
|
||||||
if (!state.resources.applicationId) {
|
if (!state.resources.applicationId) {
|
||||||
throw new Error('Application ID not available');
|
throw new Error('Application ID not available');
|
||||||
@@ -375,7 +381,7 @@ export class ProductionDeployer {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
state.phase = 'verifying_health';
|
state.phase = 'verifying_health';
|
||||||
state.progress = 95;
|
state.progress = 95;
|
||||||
state.message = 'Verifying application status via Dokploy';
|
state.message = this.t('verifyingHealth');
|
||||||
|
|
||||||
if (!state.resources.applicationId) {
|
if (!state.resources.applicationId) {
|
||||||
throw new Error('Application ID not available');
|
throw new Error('Application ID not available');
|
||||||
@@ -392,13 +398,13 @@ export class ProductionDeployer {
|
|||||||
console.log(`Application status: ${appStatus}`);
|
console.log(`Application status: ${appStatus}`);
|
||||||
|
|
||||||
if (appStatus === 'done') {
|
if (appStatus === 'done') {
|
||||||
state.message = 'Waiting for SSL certificate provisioning...';
|
state.message = this.t('waitingForSSL');
|
||||||
state.progress = 98;
|
state.progress = 98;
|
||||||
this.notifyProgress(state);
|
this.notifyProgress(state);
|
||||||
|
|
||||||
await this.sleep(15000);
|
await this.sleep(15000);
|
||||||
|
|
||||||
state.message = 'Application deployed successfully';
|
state.message = this.t('deploymentSuccess');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,7 +416,7 @@ export class ProductionDeployer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
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);
|
this.notifyProgress(state);
|
||||||
|
|
||||||
await this.sleep(interval);
|
await this.sleep(interval);
|
||||||
|
|||||||
Reference in New Issue
Block a user