feat: Integrate ngx-translate for multilingual support and add English and French translations
This commit is contained in:
1 parent
36c98db93f
commit
9f8167fc58
14 files changed
+265
-34
No files matched your search
+4
-2
@@ -19,7 +19,8 @@
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
},
|
||||
"src/assets"
|
||||
],
|
||||
"styles": ["src/custom-theme.scss", "src/styles.css"],
|
||||
"server": "src/main.server.ts",
|
||||
@@ -85,7 +86,8 @@
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
},
|
||||
"src/assets"
|
||||
],
|
||||
"styles": ["src/styles.css"]
|
||||
}
|
||||
|
||||
Generated
+29
@@ -24,6 +24,8 @@
|
||||
"@fortawesome/free-brands-svg-icons": "^7.0.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.0.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
||||
"@ngx-translate/core": "^14.0.0",
|
||||
"@ngx-translate/http-loader": "^7.0.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
@@ -2804,6 +2806,33 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngx-translate/core": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-14.0.0.tgz",
|
||||
"integrity": "sha512-UevdwNCXMRCdJv//0kC8h2eSfmi02r29xeE8E9gJ1Al4D4jEJ7eiLPdjslTMc21oJNGguqqWeEVjf64SFtvw2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": ">=13.0.0",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngx-translate/http-loader": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-7.0.0.tgz",
|
||||
"integrity": "sha512-j+NpXXlcGVdyUNyY/qsJrqqeAdJdizCd+GKh3usXExSqy1aE9866jlAIL+xrfDU4w+LiMoma5pgE4emvFebZmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=13.0.0",
|
||||
"@ngx-translate/core": ">=14.0.0",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
||||
+3
-1
@@ -46,7 +46,9 @@
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript-eslint": "^8.46.3"
|
||||
"typescript-eslint": "^8.46.3",
|
||||
"@ngx-translate/core": "^14.0.0",
|
||||
"@ngx-translate/http-loader": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^20.3.7",
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
provideBrowserGlobalErrorListeners,
|
||||
provideZonelessChangeDetection,
|
||||
isDevMode,
|
||||
importProvidersFrom,
|
||||
} from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
@@ -11,6 +12,9 @@ import { provideClientHydration, withEventReplay } from '@angular/platform-brows
|
||||
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
|
||||
import { provideServiceWorker } from '@angular/service-worker';
|
||||
import { imageCacheInterceptor } from './interceptors/image-cache.interceptor';
|
||||
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
@@ -23,5 +27,17 @@ export const appConfig: ApplicationConfig = {
|
||||
enabled: !isDevMode(),
|
||||
registrationStrategy: 'registerImmediately',
|
||||
}),
|
||||
// Provide ngx-translate through importProvidersFrom so it is available
|
||||
// for standalone component templates via the translate pipe.
|
||||
importProvidersFrom(
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: (http: HttpClient) =>
|
||||
new TranslateHttpLoader(http, '/assets/i18n/', '.json'),
|
||||
deps: [HttpClient],
|
||||
},
|
||||
}),
|
||||
),
|
||||
],
|
||||
};
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
<mat-toolbar color="primary" class="hero-toolbar" role="banner">
|
||||
<mat-icon aria-hidden="true">casino</mat-icon>
|
||||
<span class="title">Random League Squad</span>
|
||||
<span class="title">{{ 'app.title' | translate }}</span>
|
||||
</mat-toolbar>
|
||||
|
||||
<section class="app" aria-labelledby="app-intro">
|
||||
<p id="app-intro" class="subtitle mat-body-large">
|
||||
Roll a unique champion for every lane in one click.
|
||||
{{ 'app.subtitle' | translate }}
|
||||
</p>
|
||||
<mat-card class="randomizer-card">
|
||||
<mat-card-content>
|
||||
|
||||
+29
-1
@@ -4,10 +4,17 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { ChampionRandomizerComponent } from './components/champion-randomizer.component';
|
||||
import { ImagePreloadService } from './services/image-preload.service';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [MatToolbarModule, MatIconModule, MatCardModule, ChampionRandomizerComponent],
|
||||
imports: [
|
||||
MatToolbarModule,
|
||||
MatIconModule,
|
||||
MatCardModule,
|
||||
ChampionRandomizerComponent,
|
||||
TranslateModule,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './app.html',
|
||||
styleUrls: ['./app.css'],
|
||||
@@ -15,4 +22,25 @@ import { ImagePreloadService } from './services/image-preload.service';
|
||||
export class App {
|
||||
// Initialize the image preload service
|
||||
private readonly imagePreload = inject(ImagePreloadService);
|
||||
private readonly translate = inject(TranslateService);
|
||||
|
||||
constructor() {
|
||||
// Initialize language: prefer saved value, otherwise try the user's
|
||||
// browser language (navigator.languages / navigator.language), fall back to English.
|
||||
this.translate.setDefaultLang('en');
|
||||
let lang: string | null = null;
|
||||
try {
|
||||
lang = typeof localStorage !== 'undefined' ? localStorage.getItem('lang') : null;
|
||||
} catch {}
|
||||
if (!lang && typeof navigator !== 'undefined') {
|
||||
const candidate =
|
||||
(navigator.languages && navigator.languages[0]) || navigator.language || '';
|
||||
if (candidate.startsWith('fr')) {
|
||||
lang = 'fr';
|
||||
} else {
|
||||
lang = 'en';
|
||||
}
|
||||
}
|
||||
this.translate.use(lang || 'en');
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
(click)="onRoll()"
|
||||
>
|
||||
<mat-icon aria-hidden="true">casino</mat-icon>
|
||||
<span>Roll Champions</span>
|
||||
<span>{{ 'control.roll' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -21,7 +21,7 @@
|
||||
(click)="onCopyDraft()"
|
||||
>
|
||||
<mat-icon aria-hidden="true">content_copy</mat-icon>
|
||||
<span>Copy Draft</span>
|
||||
<span>{{ 'control.copy' | translate }}</span>
|
||||
</button>
|
||||
@if (state.fearlessDraftEnabled()) {
|
||||
<button
|
||||
@@ -33,7 +33,7 @@
|
||||
(click)="onReset()"
|
||||
>
|
||||
<mat-icon aria-hidden="true">restart_alt</mat-icon>
|
||||
<span>Reset Blacklist</span>
|
||||
<span>{{ 'control.reset' | translate }}</span>
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
@@ -44,13 +44,14 @@
|
||||
(click)="openSettings()"
|
||||
>
|
||||
<mat-icon aria-hidden="true">settings</mat-icon>
|
||||
<span>Settings</span>
|
||||
<span>{{ 'control.settings' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="hint mat-body-small">
|
||||
<mat-icon aria-hidden="true" class="iconFlex">info</mat-icon>
|
||||
<span style="height: 24px; display: flex; align-items: center">
|
||||
Each roll keeps champions unique across every lane.
|
||||
{{ 'control.hint' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,7 +62,7 @@
|
||||
@if (state.isLoading()) {
|
||||
<div class="status mat-body-small" aria-live="polite">
|
||||
<mat-icon aria-hidden="true">autorenew</mat-icon>
|
||||
<span>Loading champion data…</span>
|
||||
<span>{{ 'control.loading' | translate }}</span>
|
||||
</div>
|
||||
} @else if (state.loadError(); as errorMessage) {
|
||||
<div class="status error mat-body-small" aria-live="polite">
|
||||
@@ -69,7 +70,7 @@
|
||||
<span>{{ errorMessage }}</span>
|
||||
<button type="button" mat-button color="accent" (click)="onRetry()">
|
||||
<mat-icon aria-hidden="true">refresh</mat-icon>
|
||||
<span>Retry</span>
|
||||
<span>{{ 'control.retry' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { SettingsDialogComponent } from '../settings-dialog/settings-dialog.component';
|
||||
import { RandomizerStateService } from '../../services/randomizer-state.service';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-control-header',
|
||||
imports: [MatButtonModule, MatIconModule, MatProgressSpinnerModule],
|
||||
imports: [MatButtonModule, MatIconModule, MatProgressSpinnerModule, TranslateModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './control-header.component.html',
|
||||
styleUrl: './control-header.component.css',
|
||||
@@ -16,6 +17,19 @@ import { RandomizerStateService } from '../../services/randomizer-state.service'
|
||||
export class ControlHeaderComponent {
|
||||
private readonly dialog = inject(MatDialog);
|
||||
protected readonly state = inject(RandomizerStateService);
|
||||
private readonly translate = inject(TranslateService);
|
||||
|
||||
// Expose current language for UI or binding
|
||||
get currentLang(): string {
|
||||
return this.translate.currentLang || this.translate.getDefaultLang() || 'en';
|
||||
}
|
||||
|
||||
setLang(lang: string): void {
|
||||
this.translate.use(lang);
|
||||
try {
|
||||
localStorage.setItem('lang', lang);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onRoll(): void {
|
||||
this.state.rollAssignments();
|
||||
@@ -43,6 +57,8 @@ export class ControlHeaderComponent {
|
||||
// Update the service state immediately
|
||||
this.state.setFearlessDraftEnabled(value);
|
||||
},
|
||||
currentLang: this.currentLang,
|
||||
setLang: (lang: string) => this.setLang(lang),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,16 +3,14 @@
|
||||
<mat-card-header class="mat-header">
|
||||
<mat-card-title-group>
|
||||
<mat-card-title>{{ laneLabel() }}</mat-card-title>
|
||||
<mat-card-subtitle>Lane currently disabled</mat-card-subtitle>
|
||||
<mat-card-subtitle>{{ 'lane.disabled_subtitle' | translate }}</mat-card-subtitle>
|
||||
</mat-card-title-group>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="disabled-message">
|
||||
<mat-icon class="disabled-icon" aria-hidden="true">block</mat-icon>
|
||||
<p class="mat-body-large">This lane has been disabled</p>
|
||||
<p class="mat-body-small">
|
||||
You chose to exclude this lane from the random selection.
|
||||
</p>
|
||||
<p class="mat-body-large">{{ 'lane.disabled_title' | translate }}</p>
|
||||
<p class="mat-body-small">{{ 'lane.disabled_detail' | translate }}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -23,7 +21,7 @@
|
||||
(click)="onToggleDisableLane()"
|
||||
>
|
||||
<mat-icon aria-hidden="true">add_circle</mat-icon>
|
||||
Enable Lane
|
||||
{{ 'lane.enable' | translate }}
|
||||
</button>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -33,7 +31,7 @@
|
||||
<mat-card-title-group>
|
||||
<mat-card-title>{{ laneLabel() }}</mat-card-title>
|
||||
<mat-card-subtitle>
|
||||
{{ champion() ? 'Unique pick locked in' : 'Champion needed for this lane' }}
|
||||
{{ champion() ? ('lane.locked' | translate) : ('lane.needed' | translate) }}
|
||||
</mat-card-subtitle>
|
||||
</mat-card-title-group>
|
||||
<button
|
||||
@@ -65,10 +63,10 @@
|
||||
</mat-chip-set>
|
||||
} @else {
|
||||
<p class="champion-name champion-name-empty mat-title-medium">
|
||||
No champion available
|
||||
{{ 'lane.no_champion' | translate }}
|
||||
</p>
|
||||
<p class="champion-roles mat-body-small">
|
||||
Roll again once champions finish loading.
|
||||
{{ 'lane.roll_again_hint' | translate }}
|
||||
</p>
|
||||
}
|
||||
@if (canReRoll()) {
|
||||
@@ -82,7 +80,7 @@
|
||||
(click)="onChangeChampion(false)"
|
||||
>
|
||||
<mat-icon aria-hidden="true">refresh</mat-icon>
|
||||
Re-Roll !
|
||||
{{ 'lane.reroll' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
@@ -105,9 +103,9 @@
|
||||
<mat-icon aria-hidden="true">lock_open_right</mat-icon>
|
||||
}
|
||||
@if (!state.isShiftPressed()) {
|
||||
<p>No re-rolls left</p>
|
||||
<p>{{ 'lane.no_rerolls_left' | translate }}</p>
|
||||
} @else {
|
||||
<p>Force Re-Roll!</p>
|
||||
<p>{{ 'lane.force_reroll' | translate }}</p>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -4,12 +4,20 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Lane } from '../../data/champions.data';
|
||||
import { RandomizerStateService } from '../../services/randomizer-state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lane-card',
|
||||
imports: [NgOptimizedImage, MatButtonModule, MatCardModule, MatChipsModule, MatIconModule],
|
||||
imports: [
|
||||
NgOptimizedImage,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatChipsModule,
|
||||
MatIconModule,
|
||||
TranslateModule,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './lane-card.component.html',
|
||||
styleUrl: './lane-card.component.css',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<h2 mat-dialog-title>Settings</h2>
|
||||
<h2 mat-dialog-title>{{ 'settings.title' | translate }}</h2>
|
||||
<mat-dialog-content>
|
||||
<div class="setting-item">
|
||||
<label for="fearless-draft-toggle">Fearless Draft</label>
|
||||
<label for="fearless-draft-toggle">{{ 'settings.fearless_draft' | translate }}</label>
|
||||
<!-- Bind directly to the injected data via the change handler so the
|
||||
parent is updated immediately when the user toggles the switch. -->
|
||||
<mat-slide-toggle
|
||||
@@ -10,10 +10,23 @@
|
||||
color="primary"
|
||||
(change)="onToggle($event.checked)"
|
||||
></mat-slide-toggle>
|
||||
<p class="setting-description">
|
||||
When enabled, champions selected in one draft are blacklisted from future rerolls until
|
||||
reset.
|
||||
</p>
|
||||
<p class="setting-description">{{ 'settings.fearless_draft_desc' | translate }}</p>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="language-select">{{ 'settings.language' | translate }}</label>
|
||||
<mat-form-field appearance="outline" style="width: 100%; margin-top: 8px">
|
||||
<mat-select
|
||||
id="language-select"
|
||||
[value]="currentLang"
|
||||
(selectionChange)="onLangChange($event.value)"
|
||||
>
|
||||
@for (lang of languages; track lang) {
|
||||
<mat-option [value]="lang">{{
|
||||
'settings.languages.' + lang | translate
|
||||
}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
|
||||
export interface SettingsData {
|
||||
fearlessDraftEnabled: boolean;
|
||||
@@ -10,11 +13,21 @@ export interface SettingsData {
|
||||
export interface SettingsDialogData {
|
||||
fearlessDraftEnabled: boolean;
|
||||
setFearless: (value: boolean) => void;
|
||||
// currentLang is the current active language (e.g. 'en' or 'fr')
|
||||
currentLang?: string;
|
||||
// setLang callback to change the language at runtime
|
||||
setLang?: (lang: string) => void;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings-dialog',
|
||||
imports: [MatDialogModule, MatSlideToggleModule],
|
||||
imports: [
|
||||
MatDialogModule,
|
||||
MatSlideToggleModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
TranslateModule,
|
||||
],
|
||||
templateUrl: './settings-dialog.component.html',
|
||||
styleUrl: './settings-dialog.component.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@@ -23,6 +36,10 @@ export class SettingsDialogComponent {
|
||||
// Inject the dialog data which contains the current value and a setter callback.
|
||||
private data = inject(MAT_DIALOG_DATA) as SettingsDialogData;
|
||||
private dialogRef = inject(MatDialogRef<SettingsDialogComponent>);
|
||||
private translate = inject(TranslateService);
|
||||
|
||||
// Supported languages; keep in sync with assets/i18n
|
||||
protected readonly languages = ['en', 'fr'];
|
||||
|
||||
// Called when the toggle changes — immediately call the provided setter so
|
||||
// the parent receives the change right away (no Save/Cancel required).
|
||||
@@ -32,8 +49,31 @@ export class SettingsDialogComponent {
|
||||
this.data.fearlessDraftEnabled = checked;
|
||||
}
|
||||
|
||||
onLangChange(lang: string): void {
|
||||
// If the parent provided a setter, call it; otherwise change directly
|
||||
if (this.data.setLang) {
|
||||
this.data.setLang(lang);
|
||||
} else {
|
||||
this.translate.use(lang);
|
||||
try {
|
||||
localStorage.setItem('lang', lang);
|
||||
} catch {}
|
||||
}
|
||||
// update local copy
|
||||
this.data.currentLang = lang;
|
||||
}
|
||||
|
||||
// Small helper used by the template to read the current value.
|
||||
get fearlessDraftEnabled(): boolean {
|
||||
return this.data.fearlessDraftEnabled;
|
||||
}
|
||||
|
||||
get currentLang(): string {
|
||||
return (
|
||||
this.data.currentLang ||
|
||||
this.translate.currentLang ||
|
||||
this.translate.getDefaultLang() ||
|
||||
'en'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "Random League Squad",
|
||||
"subtitle": "Roll a unique champion for every lane in one click."
|
||||
},
|
||||
"control": {
|
||||
"roll": "Roll Champions",
|
||||
"copy": "Copy Draft",
|
||||
"reset": "Reset Blacklist",
|
||||
"settings": "Settings",
|
||||
"hint": "Each roll keeps champions unique across every lane.",
|
||||
"loading": "Loading champion data…",
|
||||
"retry": "Retry"
|
||||
},
|
||||
"lane": {
|
||||
"disabled_subtitle": "Lane currently disabled",
|
||||
"disabled_title": "This lane has been disabled",
|
||||
"disabled_detail": "You chose to exclude this lane from the random selection.",
|
||||
"enable": "Enable Lane",
|
||||
"locked": "Unique pick locked in",
|
||||
"needed": "Champion needed for this lane",
|
||||
"no_champion": "No champion available",
|
||||
"roll_again_hint": "Roll again once champions finish loading.",
|
||||
"reroll": "Re-Roll !",
|
||||
"no_rerolls_left": "No re-rolls left",
|
||||
"force_reroll": "Force Re-Roll!",
|
||||
"roles_for": "Roles for {{name}}"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"fearless_draft": "Fearless Draft",
|
||||
"fearless_draft_desc": "When enabled, champions selected in one draft are blacklisted from future rerolls until reset.",
|
||||
"language": "Language",
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"fr": "Français"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "Escouade aléatoire de League",
|
||||
"subtitle": "Attribuez un champion unique pour chaque lane en un clic."
|
||||
},
|
||||
"control": {
|
||||
"roll": "Lancer les champions",
|
||||
"copy": "Copier le draft",
|
||||
"reset": "Réinitialiser la liste noire",
|
||||
"settings": "Paramètres",
|
||||
"hint": "Chaque tir garantit des champions uniques pour chaque lane.",
|
||||
"loading": "Chargement des données des champions…",
|
||||
"retry": "Réessayer"
|
||||
},
|
||||
"lane": {
|
||||
"disabled_subtitle": "Lane actuellement désactivée",
|
||||
"disabled_title": "Cette lane a été désactivée",
|
||||
"disabled_detail": "Vous avez choisi d'exclure cette lane de la sélection aléatoire.",
|
||||
"enable": "Activer la lane",
|
||||
"locked": "Choix unique verrouillé",
|
||||
"needed": "Champion requis pour cette lane",
|
||||
"no_champion": "Aucun champion disponible",
|
||||
"roll_again_hint": "Relancez une fois que les champions ont fini de charger.",
|
||||
"reroll": "Relancer !",
|
||||
"no_rerolls_left": "Plus de relances disponibles",
|
||||
"force_reroll": "Forcer la relance !",
|
||||
"roles_for": "Rôles pour {{name}}"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Paramètres",
|
||||
"fearless_draft": "Draft sans peur",
|
||||
"fearless_draft_desc": "Lorsqu'activé, les champions sélectionnés dans un draft sont mis en liste noire pour les relances futures jusqu'à réinitialisation.",
|
||||
"language": "Langue",
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"fr": "Français"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user