Mise en place de l'anglais comme deuxième langue pour l'application
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* Un message d'une conversation IA (vue front).
|
||||
@@ -45,6 +46,7 @@ export type NarrativeEntityType = 'arc' | 'chapter' | 'scene' | 'character' | 'n
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AiChatService {
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly loreEndpoint = '/api/ai/chat/stream';
|
||||
private readonly campaignEndpoint = '/api/ai/chat/stream-campaign';
|
||||
private readonly sessionEndpoint = '/api/ai/chat/stream-session';
|
||||
@@ -237,9 +239,9 @@ export class AiChatService {
|
||||
private safeParseMessage(json: string): string {
|
||||
try {
|
||||
const obj = JSON.parse(json) as { message?: string };
|
||||
return obj.message ?? 'Erreur inconnue côté serveur.';
|
||||
return obj.message ?? this.translate.instant('services.unknownServerError');
|
||||
} catch {
|
||||
return json || 'Erreur inconnue côté serveur.';
|
||||
return json || this.translate.instant('services.unknownServerError');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
CampaignImportApplyResult,
|
||||
CampaignImportProposal,
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CampaignImportService {
|
||||
constructor(private http: HttpClient) {}
|
||||
constructor(private http: HttpClient, private translate: TranslateService) {}
|
||||
|
||||
importStructureStream(campaignId: string, file: File): Observable<CampaignImportStreamEvent> {
|
||||
return new Observable<CampaignImportStreamEvent>((subscriber) => {
|
||||
@@ -70,7 +71,7 @@ export class CampaignImportService {
|
||||
const dispatch = () => {
|
||||
const name = currentEvent ?? 'message';
|
||||
if (name === 'error') {
|
||||
let message = 'Échec de l\'import.';
|
||||
let message = this.translate.instant('services.importFailed');
|
||||
try { message = (JSON.parse(currentData) as { message?: string }).message ?? message; } catch { /* défaut */ }
|
||||
terminated = true;
|
||||
subscriber.error(new Error(message));
|
||||
@@ -120,7 +121,7 @@ export class CampaignImportService {
|
||||
if (currentEvent !== null || currentData !== '') dispatch();
|
||||
if (!terminated) {
|
||||
subscriber.error(new Error(
|
||||
'L\'import s\'est interrompu avant la fin (connexion coupée ou délai dépassé). Réessayez.'));
|
||||
this.translate.instant('services.importInterrupted')));
|
||||
}
|
||||
} catch (err) {
|
||||
if (!terminated) subscriber.error(err);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { forkJoin, Subscription } from 'rxjs';
|
||||
import { CampaignService } from './campaign.service';
|
||||
import { CharacterService } from './character.service';
|
||||
@@ -30,7 +31,8 @@ export class CampaignSidebarService {
|
||||
private npcService: NpcService,
|
||||
private randomTableService: RandomTableService,
|
||||
private enemyService: EnemyService,
|
||||
private layoutService: LayoutService
|
||||
private layoutService: LayoutService,
|
||||
private translate: TranslateService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -51,7 +53,7 @@ export class CampaignSidebarService {
|
||||
this.enemyService
|
||||
)
|
||||
}).subscribe(({ campaign, allCampaigns, treeData }) => {
|
||||
this.layoutService.show(buildCampaignSidebarConfig(campaign, allCampaigns, treeData, campaignId));
|
||||
this.layoutService.show(buildCampaignSidebarConfig(campaign, allCampaigns, treeData, campaignId, this.translate));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { GameSystem, GameSystemCreate, RulesImportResponse, RulesImportStreamEvent } from './game-system.model';
|
||||
|
||||
/**
|
||||
@@ -10,7 +11,7 @@ import { GameSystem, GameSystemCreate, RulesImportResponse, RulesImportStreamEve
|
||||
export class GameSystemService {
|
||||
private apiUrl = '/api/game-systems';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
constructor(private http: HttpClient, private translate: TranslateService) {}
|
||||
|
||||
getAll(): Observable<GameSystem[]> {
|
||||
return this.http.get<GameSystem[]>(this.apiUrl);
|
||||
@@ -101,7 +102,7 @@ export class GameSystemService {
|
||||
const dispatch = () => {
|
||||
const name = currentEvent ?? 'message';
|
||||
if (name === 'error') {
|
||||
let message = 'Échec de l\'import.';
|
||||
let message = this.translate.instant('services.importFailed');
|
||||
try { message = (JSON.parse(currentData) as { message?: string }).message ?? message; } catch { /* garde le défaut */ }
|
||||
terminated = true;
|
||||
subscriber.error(new Error(message));
|
||||
@@ -151,7 +152,7 @@ export class GameSystemService {
|
||||
if (currentEvent !== null || currentData !== '') dispatch();
|
||||
if (!terminated) {
|
||||
subscriber.error(new Error(
|
||||
'L\'import s\'est interrompu avant la fin (connexion coupée ou délai dépassé). Réessayez.'));
|
||||
this.translate.instant('services.importInterrupted')));
|
||||
}
|
||||
} catch (err) {
|
||||
if (!terminated) subscriber.error(err);
|
||||
|
||||
101
web/src/app/services/language.service.ts
Normal file
101
web/src/app/services/language.service.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* Langue affichée par l'interface.
|
||||
*
|
||||
* Source de vérité = ngx-translate (TranslateService). Ce service ajoute :
|
||||
* - la liste des langues proposées dans le sélecteur (LANGUAGES) ;
|
||||
* - la persistance du choix dans localStorage (par appareil) ;
|
||||
* - la résolution de la langue initiale au démarrage (localStorage → langue
|
||||
* du navigateur → repli français).
|
||||
*
|
||||
* Le changement est appliqué à chaud : translate.use() recharge le bon JSON et
|
||||
* rafraîchit tous les pipes `translate` — aucun rechargement de page.
|
||||
*/
|
||||
export interface AppLanguage {
|
||||
/** Code ISO court utilisé par les fichiers i18n (fr.json, en.json). */
|
||||
code: string;
|
||||
/** Clé i18n du libellé natif affiché dans le sélecteur. */
|
||||
labelKey: string;
|
||||
/** Drapeau emoji pour un repère visuel léger. */
|
||||
flag: string;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'loremind.lang';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class LanguageService {
|
||||
private readonly translate = inject(TranslateService);
|
||||
|
||||
/** Langues proposées dans l'application. Ajouter une langue = une ligne ici + un JSON. */
|
||||
readonly languages: readonly AppLanguage[] = [
|
||||
{ code: 'fr', labelKey: 'language.fr', flag: '🇫🇷' },
|
||||
{ code: 'en', labelKey: 'language.en', flag: '🇬🇧' },
|
||||
];
|
||||
|
||||
readonly defaultLang = 'fr';
|
||||
|
||||
/**
|
||||
* Appelé une fois au démarrage (APP_INITIALIZER) : enregistre les langues
|
||||
* connues, fixe le repli et applique la langue résolue avant le premier rendu.
|
||||
*/
|
||||
init(): Promise<unknown> {
|
||||
const codes = this.languages.map((l) => l.code);
|
||||
this.translate.addLangs(codes);
|
||||
this.translate.setFallbackLang(this.defaultLang);
|
||||
|
||||
const lang = this.resolveInitialLang();
|
||||
// use() renvoie un Observable qui émet quand le JSON est chargé : on
|
||||
// l'attend pour éviter un flash de clés non traduites au boot.
|
||||
return new Promise((resolve) => {
|
||||
this.translate.use(lang).subscribe({
|
||||
next: () => resolve(true),
|
||||
error: () => resolve(false),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Code de la langue active. */
|
||||
get current(): string {
|
||||
return this.translate.getCurrentLang() || this.defaultLang;
|
||||
}
|
||||
|
||||
/** Change la langue à chaud et mémorise le choix. */
|
||||
use(code: string): void {
|
||||
if (!this.languages.some((l) => l.code === code)) {
|
||||
return;
|
||||
}
|
||||
this.translate.use(code);
|
||||
this.persist(code);
|
||||
}
|
||||
|
||||
private resolveInitialLang(): string {
|
||||
const stored = this.read();
|
||||
if (stored && this.languages.some((l) => l.code === stored)) {
|
||||
return stored;
|
||||
}
|
||||
const browser = this.translate.getBrowserLang();
|
||||
if (browser && this.languages.some((l) => l.code === browser)) {
|
||||
return browser;
|
||||
}
|
||||
return this.defaultLang;
|
||||
}
|
||||
|
||||
private read(): string | null {
|
||||
try {
|
||||
return localStorage.getItem(STORAGE_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private persist(code: string): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, code);
|
||||
} catch {
|
||||
// localStorage indisponible (mode privé strict) : la langue reste valable
|
||||
// pour la session courante, simplement non mémorisée.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable, NgZone } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Notebook, NotebookArchive, NotebookDetail, NotebookSource, NotebookChatEvent } from './notebook.model';
|
||||
|
||||
/**
|
||||
@@ -11,7 +12,7 @@ import { Notebook, NotebookArchive, NotebookDetail, NotebookSource, NotebookChat
|
||||
export class NotebookService {
|
||||
private readonly apiUrl = '/api/notebooks';
|
||||
|
||||
constructor(private http: HttpClient, private zone: NgZone) {}
|
||||
constructor(private http: HttpClient, private zone: NgZone, private translate: TranslateService) {}
|
||||
|
||||
listByCampaign(campaignId: string): Observable<Notebook[]> {
|
||||
return this.http.get<Notebook[]>(`${this.apiUrl}/campaign/${campaignId}`);
|
||||
@@ -143,7 +144,7 @@ export class NotebookService {
|
||||
this.zone.run(() => subscriber.complete());
|
||||
} catch (err) {
|
||||
if ((err as Error).name !== 'AbortError') {
|
||||
emit({ type: 'error', message: (err as Error).message || 'Erreur réseau' });
|
||||
emit({ type: 'error', message: (err as Error).message || this.translate.instant('services.networkError') });
|
||||
}
|
||||
this.zone.run(() => subscriber.complete());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user