Skip to content
Merged

Dev #182

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/api/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import express, { Express, Request, Response } from 'express';
import morgan from 'morgan';
import { stream as loggerStream } from './config/logger';
import { metricsMiddleware } from './middleware/metrics.middleware';
import { languageMiddleware } from './middleware/language.middleware';
import metricsRouter from './routes/metrics.routes';
import admin from 'firebase-admin';
import cors from 'cors';
Expand Down Expand Up @@ -113,6 +114,10 @@ app.use(
// Audit log for sensitive routes.
app.use(auditLogger);

// Resolve the user's UI language (query > body > Accept-Language) and expose it to
// all downstream services so AI generation replies in the right language.
app.use(languageMiddleware);

app.use('/projects', projectRoutes);
app.use('/project', brandingRoutes);
app.use('/project', diagramRoutes);
Expand Down
2 changes: 2 additions & 0 deletions apps/api/api/interfaces/express.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import admin from 'firebase-admin';

export interface CustomRequest extends Request {
user?: admin.auth.DecodedIdToken;
/** Resolved UI language ('en' | 'fr'), set by languageMiddleware. */
language?: string;
policyWarning?: {
requiresFinalization: boolean;
finalizeEndpoint: string;
Expand Down
24 changes: 24 additions & 0 deletions apps/api/api/middleware/language.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Request, Response, NextFunction } from 'express';
import { normalizeLanguage, runWithLanguage } from '../utils/request-language';

/**
* Resolve the user's UI language for the request and expose it two ways:
* - `req.language` (typed on CustomRequest) for direct access in controllers;
* - an AsyncLocalStorage context so PromptService (and any downstream service)
* can read it without threading the value through every call.
*
* Resolution priority: `?lang=` query > `language` body field > Accept-Language
* header > default 'en'.
*/
export function languageMiddleware(req: Request, _res: Response, next: NextFunction): void {
const fromQuery = typeof req.query.lang === 'string' ? req.query.lang : undefined;
const fromBody =
req.body && typeof req.body.language === 'string' ? (req.body.language as string) : undefined;
const headerValue = req.headers['accept-language'];
const fromHeader = typeof headerValue === 'string' ? headerValue.split(',')[0] : undefined;

const language = normalizeLanguage(fromQuery || fromBody || fromHeader);
(req as Request & { language?: string }).language = language;

runWithLanguage(language, () => next());
}
5 changes: 4 additions & 1 deletion apps/api/api/services/BusinessPlan/businessPlan.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { GenericService, IPromptStep, ISectionResult } from '../common/generic.s
import { SectionModel } from '../../models/section.model';
import { PdfService } from '../pdf.service';
import { cacheService, CacheOptions } from '../cache.service';
import { getRequestLanguage } from '../../utils/request-language';
import crypto from 'crypto';
import { AGENT_COVER_PROMPT } from './prompts/agent-cover.prompt';
import { AGENT_COMPANY_SUMMARY_PROMPT } from './prompts/agent-company-summary.prompt';
Expand Down Expand Up @@ -89,7 +90,9 @@ export class BusinessPlanService extends GenericService {
const typography = project.analysisResultModel?.branding?.typography || {
primary: 'Arial, sans-serif',
};
const language = 'fr';
// Use the user's request language instead of a hard-coded 'fr' so the plan is
// generated in the language selected in the UI (falls back to 'en').
const language = getRequestLanguage() === 'fr' ? 'French' : 'English';

// Create brand context for all agents
const brandContext = `Brand: ${brandName}\nLogo SVG: ${logoSvg}\nBrand Colors: ${JSON.stringify(
Expand Down
49 changes: 49 additions & 0 deletions apps/api/api/services/prompt.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dotenv.config();

import { LLMProvider, LLMOptions, AI_CONFIG } from '../config/ai.config';
import { withGeminiFallback } from '../utils/gemini-fallback';
import { getRequestLanguage } from '../utils/request-language';
export { LLMProvider, LLMOptions };


Expand All @@ -24,6 +25,12 @@ export interface PromptConfig {
userId?: string;
promptType?: string;
skipQuotaCheck?: boolean;
/**
* User UI language ('en' | 'fr'). When set, a language directive is injected so
* the model generates content in the requested language. Resolved from the
* request (query `lang` / body `language` / Accept-Language header) upstream.
*/
language?: string;
}

export interface AIChatMessage {
Expand All @@ -44,6 +51,7 @@ export interface PromptRequest {
userId?: string;
promptType?: string;
skipQuotaCheck?: boolean;
language?: string;
}

export interface AIResponse {
Expand Down Expand Up @@ -422,6 +430,27 @@ export class PromptService {
}
}

/**
* Build a strong directive that forces the model to answer in the user's language.
* Returns an empty string when no (or an unknown) language is provided, leaving
* existing behavior unchanged.
*/
private buildLanguageDirective(language?: string): string {
if (!language) {
return '';
}
const normalized = language.toLowerCase();
const label = normalized.startsWith('fr')
? 'French (Français)'
: normalized.startsWith('en')
? 'English'
: null;
if (!label) {
return '';
}
return `RESPONSE LANGUAGE (CRITICAL): You MUST write ALL generated content — every section, title, sentence, label and value — in ${label}. Do not mix languages. This instruction overrides any language implied by the examples or prompts below.`;
}

public async runPrompt(request: PromptConfig, messages: AIChatMessage[]): Promise<string> {
logger.info(
`Running prompt for provider: ${request.provider}, model: ${
Expand All @@ -436,6 +465,7 @@ export class PromptService {
userId,
promptType,
skipQuotaCheck = false,
language,
} = request;

if (!messages || messages.length === 0) {
Expand Down Expand Up @@ -492,6 +522,25 @@ export class PromptService {
logger.info('Applied prompt modifications');
}

// Force the output language. This is the single choke point for every AI
// feature/provider, so one directive here guarantees generated content is in
// the user's language (prevents wrong-language output). An explicit
// config.language wins; otherwise fall back to the request-scoped language.
const effectiveLanguage = language ?? getRequestLanguage();
const languageDirective = this.buildLanguageDirective(effectiveLanguage);
if (languageDirective && modifiedMessages.length > 0) {
// Append to the LAST message rather than inserting a new system message:
// this keeps message roles/adjacency intact (Gemini rejects consecutive
// same-role turns) and benefits from recency for stronger adherence.
const lastIdx = modifiedMessages.length - 1;
const last = modifiedMessages[lastIdx];
modifiedMessages = [
...modifiedMessages.slice(0, lastIdx),
{ ...last, content: `${last.content}\n\n${languageDirective}` },
];
logger.info(`Injected language directive (language=${effectiveLanguage}).`);
}

try {
let result: string;
switch (provider) {
Expand Down
32 changes: 32 additions & 0 deletions apps/api/api/utils/request-language.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { AsyncLocalStorage } from 'async_hooks';

export type SupportedLanguage = 'en' | 'fr';

interface LanguageStore {
language: SupportedLanguage;
}

/**
* Request-scoped language store. The language middleware seeds this per request so
* that any service (notably PromptService) can read the user's language without it
* being threaded through every function signature.
*/
const storage = new AsyncLocalStorage<LanguageStore>();

export function runWithLanguage<T>(language: SupportedLanguage, fn: () => T): T {
return storage.run({ language }, fn);
}

/** Current request's language, or undefined outside a request context. */
export function getRequestLanguage(): SupportedLanguage | undefined {
return storage.getStore()?.language;
}

/** Normalize an arbitrary language hint to a supported language (defaults to 'en'). */
export function normalizeLanguage(value: unknown): SupportedLanguage {
return String(value ?? '')
.toLowerCase()
.startsWith('fr')
? 'fr'
: 'en';
}
20 changes: 4 additions & 16 deletions apps/appgen/apps/we-dev-client/src/api/appInfo.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
import { resolveInitialLocale } from "../utils/localeCookie";

export const authService = {
async appInfo() {
let language = "en";
try {
const settingsConfig = JSON.parse(
localStorage.getItem("settingsConfig") || "{}"
);
if (settingsConfig.language) {
language = settingsConfig.language;
} else {
// Get browser language setting
const browserLang = navigator.language.toLowerCase();
// Set to Chinese if browser language is Chinese, otherwise English
language = browserLang.startsWith("zh") ? "zh" : "en";
}
} catch (error) {
console.log(error);
}
// Shared cross-app cookie is the source of truth; falls back to browser lang.
const language = resolveInitialLocale();

const res = await fetch(
`${process.env.REACT_APP_BASE_URL}/api/appInfo?language=${language}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export const BaseChat = ({ uuid: propUuid }: { uuid?: string }) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const { otherConfig } = useChatStore();
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const [checkCount, setCheckCount] = useState(0);
const [visible, setVisible] = useState(false);
const [baseModal, setBaseModal] = useState<IModelOption>({
Expand Down Expand Up @@ -493,6 +493,9 @@ export const BaseChat = ({ uuid: propUuid }: { uuid?: string }) => {
body: {
model: baseModal.value,
mode: mode,
// User UI language so the AI answers/generates content in the right language.
// (Distinct from otherConfig.backendLanguage, which is the target programming language.)
language: i18n.language,
otherConfig: {
...otherConfig,
extra: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ export function GeneralSettings() {
onChange={(value) => handleLanguageChange(value)}
options={[
{ value: "en", label: "English" },
{ value: "zh", label: "中文" },
{ value: "fr", label: "Français" },
]}
/>
</div>
Expand Down Expand Up @@ -643,7 +643,7 @@ export function GeneralSettings() {
onChange={(value) => handleLanguageChange(value)}
options={[
{ value: "en", label: "English" },
{ value: "zh", label: "中文" },
{ value: "fr", label: "Français" },
]}
/>
</div>
Expand Down
14 changes: 8 additions & 6 deletions apps/appgen/apps/we-dev-client/src/hooks/useInit.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import useThemeStore from '@/stores/themeSlice';
import useUserStore from '@/stores/userSlice';
import i18n from '@/utils/i18';
import { readLocaleCookie } from '@/utils/localeCookie';
import { useEffect } from 'react';
import useMCPStore from '@/stores/useMCPSlice';
import { eventEmitter } from '@/components/AiChat/utils/EventEmitter';
Expand Down Expand Up @@ -90,17 +91,18 @@ const useInit = (): { isDarkMode: boolean } => {
// Version web - pas d'accès au système de fichiers local
console.log('Local filesystem not available in web mode');

// Language priority: shared cross-app cookie > legacy settingsConfig > browser.
// The shared `idem_lang` cookie is the source of truth across all Idem apps.
const settingsConfig = JSON.parse(localStorage.getItem('settingsConfig') || '{}');
if (settingsConfig.language) {
const cookieLang = readLocaleCookie();
if (cookieLang) {
i18n.changeLanguage(cookieLang);
} else if (settingsConfig.language === 'en' || settingsConfig.language === 'fr') {
i18n.changeLanguage(settingsConfig.language);
} else {
// Get browser language settings
const browserLang = navigator.language.toLowerCase();
// If Chinese environment (including simplified and traditional), set to Chinese, otherwise set to English
const defaultLang = browserLang.startsWith('zh') ? 'zh' : 'en';

const defaultLang = browserLang.startsWith('fr') ? 'fr' : 'en';
i18n.changeLanguage(defaultLang);
// Save to local settings
}

// Version web - pas de gestion de proxy système
Expand Down
Loading