Skip to content

Commit b7cfcc1

Browse files
authored
Merge pull request #93 from DGouron/feat/45-internationalization-support
feat(i18n): internationalization support FR/EN
2 parents 75a4814 + c41f4fe commit b7cfcc1

File tree

17 files changed

+278
-21
lines changed

17 files changed

+278
-21
lines changed

src/config/projectConfig.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { readFileSync, existsSync } from 'node:fs';
22
import { join } from 'node:path';
3-
import type { AgentDefinition } from '../entities/progress/agentDefinition.type.js';
3+
import type { AgentDefinition } from '@/entities/progress/agentDefinition.type.js';
4+
import type { Language } from '@/entities/language/language.schema.js';
45

56
/**
67
* Project-specific review configuration
@@ -12,6 +13,7 @@ export interface ProjectConfig {
1213
defaultModel: 'sonnet' | 'opus';
1314
reviewSkill: string;
1415
reviewFollowupSkill: string;
16+
language: Language;
1517
agents?: AgentDefinition[];
1618
followupAgents?: AgentDefinition[];
1719
}
@@ -88,6 +90,7 @@ export function loadProjectConfig(localPath: string): ProjectConfig | undefined
8890
defaultModel: parsed.defaultModel === 'opus' ? 'opus' : 'sonnet',
8991
reviewSkill: String(parsed.reviewSkill),
9092
reviewFollowupSkill: String(parsed.reviewFollowupSkill),
93+
language: parsed.language === 'fr' ? 'fr' : 'en',
9194
agents: parsed.agents as AgentDefinition[] | undefined,
9295
followupAgents: parsed.followupAgents as AgentDefinition[] | undefined,
9396
};
@@ -105,6 +108,18 @@ export function getProjectAgents(localPath: string): AgentDefinition[] | undefin
105108
}
106109
}
107110

111+
/**
112+
* Get language from project config, defaulting to 'en'
113+
*/
114+
export function getProjectLanguage(localPath: string): Language {
115+
try {
116+
const config = loadProjectConfig(localPath);
117+
return config?.language ?? 'en';
118+
} catch {
119+
return 'en';
120+
}
121+
}
122+
108123
/**
109124
* Get followup agents from project config or undefined for defaults
110125
*/
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { z } from 'zod';
2+
3+
export const languageSchema = z.enum(['en', 'fr']);
4+
5+
export type Language = z.infer<typeof languageSchema>;

src/frameworks/claude/claudeInvoker.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@ import { writeFileSync, mkdirSync, existsSync, unlinkSync, readFileSync } from '
33
import { join, dirname } from 'node:path';
44
import { fileURLToPath } from 'node:url';
55
import type { Logger } from 'pino';
6-
import type { ReviewJob } from '../queue/pQueueAdapter.js';
7-
import type { ReviewProgress, ProgressEvent } from '../../entities/progress/progress.type.js';
8-
import { ProgressParser } from './progressParser.js';
9-
import { logInfo, logWarn, logError } from '../logging/logBuffer.js';
10-
import { getModel } from '../settings/runtimeSettings.js';
11-
import { getProjectAgents, getFollowupAgents } from '../../config/projectConfig.js';
12-
import { addReviewStats } from '../../services/statsService.js';
13-
import { FileSystemReviewRequestTrackingGateway } from '../../interface-adapters/gateways/fileSystem/reviewRequestTracking.fileSystem.js';
14-
import { ProjectStatsCalculator } from '../../interface-adapters/presenters/projectStats.calculator.js';
15-
import { resolveClaudePath } from '../../shared/services/claudePathResolver.js';
16-
import { getJobContextFilePath } from '../../shared/services/mcpJobContext.js';
6+
import type { ReviewJob } from '@/frameworks/queue/pQueueAdapter.js';
7+
import type { ReviewProgress, ProgressEvent } from '@/entities/progress/progress.type.js';
8+
import { ProgressParser } from '@/frameworks/claude/progressParser.js';
9+
import { logInfo, logWarn, logError } from '@/frameworks/logging/logBuffer.js';
10+
import { getModel } from '@/frameworks/settings/runtimeSettings.js';
11+
import { getProjectAgents, getFollowupAgents } from '@/config/projectConfig.js';
12+
import { addReviewStats } from '@/services/statsService.js';
13+
import { FileSystemReviewRequestTrackingGateway } from '@/interface-adapters/gateways/fileSystem/reviewRequestTracking.fileSystem.js';
14+
import { ProjectStatsCalculator } from '@/interface-adapters/presenters/projectStats.calculator.js';
15+
import { resolveClaudePath } from '@/shared/services/claudePathResolver.js';
16+
import { getJobContextFilePath } from '@/shared/services/mcpJobContext.js';
17+
import { buildLanguageDirective } from '@/frameworks/claude/languageDirective.js';
1718

1819
const currentDir = dirname(fileURLToPath(import.meta.url));
1920

@@ -204,6 +205,8 @@ Use \`POST_INLINE_COMMENT\` to post comments directly on specific lines in the d
204205
- Producing a "plan" instead of executing → Review will be empty
205206
- Using text markers like [PROGRESS:xxx] → Dashboard won't update
206207
- Waiting for user approval → Review will hang forever
208+
209+
${buildLanguageDirective(job.language ?? 'en')}
207210
`.trim();
208211
}
209212

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Language } from '@/entities/language/language.schema.js';
2+
3+
const LANGUAGE_LABELS: Record<Language, string> = {
4+
en: 'English',
5+
fr: 'French',
6+
};
7+
8+
export function buildLanguageDirective(language: Language): string {
9+
const label = LANGUAGE_LABELS[language];
10+
return `## MANDATORY OUTPUT LANGUAGE\n\nCRITICAL: WRITE YOUR ENTIRE REVIEW IN ${label.toUpperCase()}. Every comment, analysis, recommendation, and report section MUST be written in ${label}. This is NON-NEGOTIABLE.`;
11+
}

src/frameworks/queue/pQueueAdapter.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import PQueue from 'p-queue';
22
import type { Logger } from 'pino';
3-
import { loadConfig } from '../config/configLoader.js';
4-
import type { ReviewProgress, ProgressEvent } from '../../entities/progress/progress.type.js';
3+
import { loadConfig } from '@/frameworks/config/configLoader.js';
4+
import type { ReviewProgress, ProgressEvent } from '@/entities/progress/progress.type.js';
5+
import type { Language } from '@/entities/language/language.schema.js';
56

67
export interface ReviewJob {
78
id: string; // Unique identifier: platform:project:mrNumber
@@ -15,6 +16,8 @@ export interface ReviewJob {
1516
targetBranch: string;
1617
// Job type: review or followup
1718
jobType?: 'review' | 'followup';
19+
// Output language for the review
20+
language?: Language;
1821
// Optional MR metadata
1922
title?: string;
2023
description?: string;

src/frameworks/settings/runtimeSettings.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22
* Runtime settings that can be changed without restart
33
*/
44

5+
import type { Language } from '@/entities/language/language.schema.js';
6+
57
export type ClaudeModel = 'sonnet' | 'opus';
68

79
interface RuntimeSettings {
810
model: ClaudeModel;
11+
language: Language;
912
}
1013

1114
const settings: RuntimeSettings = {
12-
model: 'opus', // Default to opus as requested
15+
model: 'opus',
16+
language: 'en',
1317
};
1418

1519
export function getModel(): ClaudeModel {
@@ -23,6 +27,14 @@ export function setModel(model: ClaudeModel): void {
2327
settings.model = model;
2428
}
2529

30+
export function getDefaultLanguage(): Language {
31+
return settings.language;
32+
}
33+
34+
export function setDefaultLanguage(language: Language): void {
35+
settings.language = language;
36+
}
37+
2638
export function getSettings(): RuntimeSettings {
2739
return { ...settings };
2840
}

src/interface-adapters/controllers/http/settings.routes.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { FastifyPluginAsync } from 'fastify';
2-
import { getModel, setModel, getSettings, type ClaudeModel } from '../../../frameworks/settings/runtimeSettings.js';
2+
import { getModel, setModel, getDefaultLanguage, setDefaultLanguage, getSettings, type ClaudeModel } from '@/frameworks/settings/runtimeSettings.js';
3+
import { languageSchema } from '@/entities/language/language.schema.js';
34

45
export const settingsRoutes: FastifyPluginAsync = async (fastify) => {
56
fastify.get('/api/settings', async () => {
@@ -23,4 +24,17 @@ export const settingsRoutes: FastifyPluginAsync = async (fastify) => {
2324
setModel(model);
2425
return { success: true, model: getModel() };
2526
});
27+
28+
fastify.post('/api/settings/language', async (request, reply) => {
29+
const { language } = request.body as { language?: string };
30+
31+
const parsed = languageSchema.safeParse(language);
32+
if (!parsed.success) {
33+
reply.code(400);
34+
return { success: false, error: 'Invalid language. Use: en, fr' };
35+
}
36+
37+
setDefaultLanguage(parsed.data);
38+
return { success: true, language: getDefaultLanguage() };
39+
});
2640
};

src/interface-adapters/controllers/webhook/github.controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { ReviewContextFileSystemGateway } from '../../gateways/reviewContext.fil
2323
import { GitHubThreadFetchGateway, defaultGitHubExecutor } from '../../gateways/threadFetch.github.gateway.js';
2424
import { GitHubDiffMetadataFetchGateway } from '../../gateways/diffMetadataFetch.github.gateway.js';
2525
import { startWatchingReviewContext, stopWatchingReviewContext } from '../../../main/websocket.js';
26-
import { getProjectAgents } from '../../../config/projectConfig.js';
26+
import { getProjectAgents, getProjectLanguage } from '@/config/projectConfig.js';
2727
import { DEFAULT_AGENTS } from '../../../entities/progress/agentDefinition.type.js';
2828

2929
export async function handleGitHubWebhook(
@@ -185,6 +185,7 @@ export async function handleGitHubWebhook(
185185
sourceBranch: filterResult.sourceBranch,
186186
targetBranch: filterResult.targetBranch,
187187
jobType: 'review',
188+
language: getProjectLanguage(repoConfig.localPath),
188189
title: prTitle,
189190
description: event.pull_request?.body,
190191
assignedBy,

src/interface-adapters/controllers/webhook/gitlab.controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { RecordPushUseCase } from '../../../usecases/tracking/recordPush.usecase
1919
import { TransitionStateUseCase } from '../../../usecases/tracking/transitionState.usecase.js';
2020
import { CheckFollowupNeededUseCase } from '../../../usecases/tracking/checkFollowupNeeded.usecase.js';
2121
import { SyncThreadsUseCase } from '../../../usecases/tracking/syncThreads.usecase.js';
22-
import { loadProjectConfig, getProjectAgents, getFollowupAgents } from '../../../config/projectConfig.js';
22+
import { loadProjectConfig, getProjectAgents, getFollowupAgents, getProjectLanguage } from '@/config/projectConfig.js';
2323
import { DEFAULT_AGENTS, DEFAULT_FOLLOWUP_AGENTS } from '../../../entities/progress/agentDefinition.type.js';
2424
import { parseReviewOutput } from '../../../services/statsService.js';
2525
import { parseThreadActions } from '../../../services/threadActionsParser.js';
@@ -434,6 +434,7 @@ export async function handleGitLabWebhook(
434434
sourceBranch: filterResult.sourceBranch,
435435
targetBranch: filterResult.targetBranch,
436436
jobType: 'review',
437+
language: getProjectLanguage(repoConfig.localPath),
437438
// MR metadata for dashboard
438439
title: mrTitle,
439440
description: event.object_attributes?.description,

src/interface-adapters/views/dashboard/index.html

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ <h1>Reviewflow</h1>
6262
</select>
6363
</div>
6464
</div>
65+
<div class="card">
66+
<div class="card-label">Langue</div>
67+
<div class="card-model">
68+
<select id="language-select" class="model-select" onchange="changeLanguage(this.value)">
69+
<option value="en">English</option>
70+
<option value="fr">Français</option>
71+
</select>
72+
</div>
73+
</div>
6574
</div>
6675

6776
<div class="project-loader">
@@ -1240,6 +1249,35 @@ <h1>Reviewflow</h1>
12401249
}
12411250
}
12421251

1252+
async function loadLanguageSetting() {
1253+
try {
1254+
const response = await fetch(`${API_URL}/api/settings`);
1255+
const data = await response.json();
1256+
const select = document.getElementById('language-select');
1257+
if (select && data.language) {
1258+
select.value = data.language;
1259+
}
1260+
} catch (error) {
1261+
console.error('Error loading language setting:', error);
1262+
}
1263+
}
1264+
1265+
async function changeLanguage(language) {
1266+
try {
1267+
const response = await fetch(`${API_URL}/api/settings/language`, {
1268+
method: 'POST',
1269+
headers: { 'Content-Type': 'application/json' },
1270+
body: JSON.stringify({ language })
1271+
});
1272+
const data = await response.json();
1273+
if (data.success) {
1274+
console.log('Language changed to:', language);
1275+
}
1276+
} catch (error) {
1277+
console.error('Error changing language:', error);
1278+
}
1279+
}
1280+
12431281
// Project config loader with persistence
12441282
const STORAGE_KEY_PROJECTS = 'review-flow-projects';
12451283
const STORAGE_KEY_CURRENT = 'review-flow-current-project';
@@ -1511,6 +1549,7 @@ <h1>Reviewflow</h1>
15111549
fetchStatus();
15121550
checkClaudeStatus();
15131551
loadModelSetting();
1552+
loadLanguageSetting();
15141553
initProjectLoader(); // Will check git CLI and load reviews/stats after loading project config
15151554

15161555
// Initialize Lucide icons

0 commit comments

Comments
 (0)