diff --git a/client/package.json b/client/package.json index c7e69e03..d6a090c7 100644 --- a/client/package.json +++ b/client/package.json @@ -9,7 +9,7 @@ "watch": "ng build --watch --configuration development", "test": "ng test", "test:ci": "ng test --no-watch --no-progress --code-coverage --browsers=ChromeHeadless", - "i18n:extract": "transloco-keys-manager extract -d \"\" -l de -R", + "i18n:extract": "transloco-keys-manager extract -d \"\" -l de en it -R", "i18n:find": "transloco-keys-manager find", "cypress": "cypress open", "sentry:sourcemaps": "sentry-cli sourcemaps inject --org localcrag --project localcrag-client ./dist/client && sentry-cli sourcemaps upload --org localcrag --project localcrag-client ./dist/client", diff --git a/client/src/app/models/account-settings.ts b/client/src/app/models/account-settings.ts index b067669c..597caa9c 100644 --- a/client/src/app/models/account-settings.ts +++ b/client/src/app/models/account-settings.ts @@ -1,15 +1,20 @@ +import { LanguageCode } from '../utility/types/language'; + export class AccountSettings { commentReplyMailsEnabled: boolean; + language: LanguageCode; public static deserialize(payload: any): AccountSettings { const accountSettings = new AccountSettings(); accountSettings.commentReplyMailsEnabled = payload.commentReplyMailsEnabled; + accountSettings.language = payload.language; return accountSettings; } public static serialize(accountSettings: AccountSettings): any { return { commentReplyMailsEnabled: accountSettings.commentReplyMailsEnabled, + language: accountSettings.language, }; } } diff --git a/client/src/app/models/comment.ts b/client/src/app/models/comment.ts index 3251dcc5..1e66f42b 100644 --- a/client/src/app/models/comment.ts +++ b/client/src/app/models/comment.ts @@ -72,17 +72,4 @@ export class Comment extends AbstractModel { parentId: comment.parentId ?? null, }; } - - // TODO not used - public static serializeForUpdate(comment: Comment): any { - return { - message: comment.message, - }; - } -} - -// TODO not used -export interface PaginatedComments { - items: Comment[]; - hasNext: boolean; } diff --git a/client/src/app/models/instance-settings.ts b/client/src/app/models/instance-settings.ts index 7778f906..bfe86fb2 100644 --- a/client/src/app/models/instance-settings.ts +++ b/client/src/app/models/instance-settings.ts @@ -1,6 +1,7 @@ import { File } from './file'; import { FaDefaultFormat } from '../enums/fa-default-format'; import { StartingPosition } from '../enums/starting-position'; +import { LanguageCode } from '../utility/types/language'; export class InstanceSettings { timeUpdated: Date; @@ -28,6 +29,7 @@ export class InstanceSettings { defaultStartingPosition: StartingPosition; rankingPastWeeks: number | null; disableFAInAscents: boolean; + language: LanguageCode; public static deserialize(payload: any): InstanceSettings { const instanceSettings = new InstanceSettings(); @@ -65,6 +67,7 @@ export class InstanceSettings { instanceSettings.defaultStartingPosition = payload.defaultStartingPosition; instanceSettings.rankingPastWeeks = payload.rankingPastWeeks; instanceSettings.disableFAInAscents = payload.disableFAInAscents; + instanceSettings.language = payload.language; return instanceSettings; } @@ -100,6 +103,7 @@ export class InstanceSettings { defaultStartingPosition: instanceSettings.defaultStartingPosition, rankingPastWeeks: instanceSettings.rankingPastWeeks, disableFAInAscents: instanceSettings.disableFAInAscents, + language: instanceSettings.language, }; } } diff --git a/client/src/app/models/user.ts b/client/src/app/models/user.ts index 75c4c3c5..0cf828bf 100644 --- a/client/src/app/models/user.ts +++ b/client/src/app/models/user.ts @@ -1,5 +1,6 @@ import { AbstractModel } from './abstract-model'; import { File } from './file'; +import { LanguageCode } from '../utility/types/language'; /** * Model of a user. @@ -18,6 +19,7 @@ export class User extends AbstractModel { member: boolean; activatedAt: Date; avatar: File; + accountLanguage: LanguageCode; fullname: string; routerLink: string; @@ -46,6 +48,7 @@ export class User extends AbstractModel { user.fullname = `${user.firstname} ${user.lastname}`; user.avatar = payload.avatar ? File.deserialize(payload.avatar) : null; user.routerLink = `/users/${user.slug}`; + user.accountLanguage = payload.accountLanguage; return user; } diff --git a/client/src/app/modules/archive/archive-button/archive-button.component.ts b/client/src/app/modules/archive/archive-button/archive-button.component.ts index ce5285aa..9e700354 100644 --- a/client/src/app/modules/archive/archive-button/archive-button.component.ts +++ b/client/src/app/modules/archive/archive-button/archive-button.component.ts @@ -13,7 +13,6 @@ import { ArchiveService } from '../../../services/crud/archive.service'; import { GymModeDirective } from '../../shared/directives/gym-mode.directive'; import { marker } from '@jsverse/transloco-keys-manager/marker'; import { ConfirmationService } from 'primeng/api'; -import { environment } from '../../../../environments/environment'; @Component({ selector: 'lc-archive-button', @@ -92,30 +91,28 @@ export class ArchiveButtonComponent { } confirmArchiveTopoImage(event: Event) { - this.translocoService.load(`${environment.language}`).subscribe(() => { - this.confirmationService.confirm({ - target: event.target, - message: this.translocoService.translate( - this.topoImage.archived - ? marker('archive.askAlsoUnArchiveLines') - : marker('archive.askAlsoArchiveLines'), - ), - acceptLabel: this.translocoService.translate( - marker('archive.yesWithLines'), - ), - acceptButtonStyleClass: 'p-button-primary', - rejectButtonStyleClass: 'p-button-secondary', - rejectLabel: this.translocoService.translate( - marker('archive.noWithoutLines'), - ), - icon: 'pi pi-exclamation-triangle', - accept: () => { - this.doArchiveTopoImage(true); - }, - reject: () => { - this.doArchiveTopoImage(false); - }, - }); + this.confirmationService.confirm({ + target: event.target, + message: this.translocoService.translate( + this.topoImage.archived + ? marker('archive.askAlsoUnArchiveLines') + : marker('archive.askAlsoArchiveLines'), + ), + acceptLabel: this.translocoService.translate( + marker('archive.yesWithLines'), + ), + acceptButtonStyleClass: 'p-button-primary', + rejectButtonStyleClass: 'p-button-secondary', + rejectLabel: this.translocoService.translate( + marker('archive.noWithoutLines'), + ), + icon: 'pi pi-exclamation-triangle', + accept: () => { + this.doArchiveTopoImage(true); + }, + reject: () => { + this.doArchiveTopoImage(false); + }, }); } diff --git a/client/src/app/modules/area/area-form/area-form.component.ts b/client/src/app/modules/area/area-form/area-form.component.ts index 877e5da1..6a7b1a5d 100644 --- a/client/src/app/modules/area/area-form/area-form.component.ts +++ b/client/src/app/modules/area/area-form/area-form.component.ts @@ -27,7 +27,6 @@ import { ConfirmationService, SelectItem } from 'primeng/api'; import { catchError, map } from 'rxjs/operators'; import { forkJoin, of } from 'rxjs'; import { toastNotification } from '../../../ngrx/actions/notifications.actions'; -import { environment } from '../../../../environments/environment'; import { marker } from '@jsverse/transloco-keys-manager/marker'; import { Area } from '../../../models/area'; import { AreasService } from '../../../services/crud/areas.service'; @@ -302,22 +301,18 @@ export class AreaFormComponent implements OnInit { * @param event Click event. */ confirmDeleteArea(event: Event) { - this.translocoService.load(`${environment.language}`).subscribe(() => { - this.confirmationService.confirm({ - target: event.target, - message: this.translocoService.translate( - marker('area.askReallyWantToDeleteArea'), - ), - acceptLabel: this.translocoService.translate(marker('area.yesDelete')), - acceptButtonStyleClass: 'p-button-danger', - rejectLabel: this.translocoService.translate( - marker('area.noDontDelete'), - ), - icon: 'pi pi-exclamation-triangle', - accept: () => { - this.deleteArea(); - }, - }); + this.confirmationService.confirm({ + target: event.target, + message: this.translocoService.translate( + marker('area.askReallyWantToDeleteArea'), + ), + acceptLabel: this.translocoService.translate(marker('area.yesDelete')), + acceptButtonStyleClass: 'p-button-danger', + rejectLabel: this.translocoService.translate(marker('area.noDontDelete')), + icon: 'pi pi-exclamation-triangle', + accept: () => { + this.deleteArea(); + }, }); } diff --git a/client/src/app/modules/area/area-list/area-list.component.ts b/client/src/app/modules/area/area-list/area-list.component.ts index b162bec0..5a69712f 100644 --- a/client/src/app/modules/area/area-list/area-list.component.ts +++ b/client/src/app/modules/area/area-list/area-list.component.ts @@ -4,7 +4,6 @@ import { ConfirmationService, PrimeIcons, SelectItem } from 'primeng/api'; import { forkJoin, Observable } from 'rxjs'; import { select, Store } from '@ngrx/store'; import { TranslocoDirective, TranslocoService } from '@jsverse/transloco'; -import { environment } from '../../../../environments/environment'; import { marker } from '@jsverse/transloco-keys-manager/marker'; import { selectIsMobile } from '../../../ngrx/selectors/device.selectors'; import { Area } from '../../../models/area'; @@ -99,36 +98,35 @@ export class AreaListComponent implements OnInit { * Loads new data. */ refreshData() { - forkJoin([ - this.areasService.getAreas(this.sectorSlug), - this.translocoService.load(`${environment.language}`), - ]).subscribe(([areas]) => { - this.areas = areas; - this.loading = LoadingState.DEFAULT; - this.sortOptions = [ - { - icon: PrimeIcons.SORT_AMOUNT_DOWN_ALT, - label: this.translocoService.translate(marker('sortAscending')), - value: '!orderIndex', - }, - { - icon: PrimeIcons.SORT_AMOUNT_DOWN, - label: this.translocoService.translate(marker('sortDescending')), - value: 'orderIndex', - }, - { - icon: PrimeIcons.SORT_ALPHA_DOWN, - label: this.translocoService.translate(marker('sortAZ')), - value: '!name', - }, - { - icon: 'pi pi-sort-alpha-down-alt', - label: this.translocoService.translate(marker('sortZA')), - value: 'name', - }, - ]; - this.sortKey = this.sortOptions[0]; - }); + forkJoin([this.areasService.getAreas(this.sectorSlug)]).subscribe( + ([areas]) => { + this.areas = areas; + this.loading = LoadingState.DEFAULT; + this.sortOptions = [ + { + icon: PrimeIcons.SORT_AMOUNT_DOWN_ALT, + label: this.translocoService.translate(marker('sortAscending')), + value: '!orderIndex', + }, + { + icon: PrimeIcons.SORT_AMOUNT_DOWN, + label: this.translocoService.translate(marker('sortDescending')), + value: 'orderIndex', + }, + { + icon: PrimeIcons.SORT_ALPHA_DOWN, + label: this.translocoService.translate(marker('sortAZ')), + value: '!name', + }, + { + icon: 'pi pi-sort-alpha-down-alt', + label: this.translocoService.translate(marker('sortZA')), + value: 'name', + }, + ]; + this.sortKey = this.sortOptions[0]; + }, + ); } /** diff --git a/client/src/app/modules/area/area/area.component.ts b/client/src/app/modules/area/area/area.component.ts index a7e4751a..d929ba38 100644 --- a/client/src/app/modules/area/area/area.component.ts +++ b/client/src/app/modules/area/area/area.component.ts @@ -29,7 +29,6 @@ import { Breadcrumb } from 'primeng/breadcrumb'; import { Tab, TabList, Tabs } from 'primeng/tabs'; import { SetActiveTabDirective } from '../../shared/directives/set-active-tab.directive'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { Badge } from 'primeng/badge'; @Component({ selector: 'lc-area', @@ -46,7 +45,6 @@ import { Badge } from 'primeng/badge'; Tab, RouterLink, RouterOutlet, - Badge, ], providers: [{ provide: TRANSLOCO_SCOPE, useValue: 'area' }], }) @@ -101,7 +99,6 @@ export class AreaComponent implements OnInit { }), ), this.store.pipe(select(selectIsLoggedIn), take(1)), - this.translocoService.load(`${environment.language}`), ]).subscribe(([crag, sector, area, isLoggedIn]) => { this.crag = crag; this.sector = sector; diff --git a/client/src/app/modules/ascent/ascent-list/ascent-list.component.ts b/client/src/app/modules/ascent/ascent-list/ascent-list.component.ts index 71ae37d6..12f883ed 100644 --- a/client/src/app/modules/ascent/ascent-list/ascent-list.component.ts +++ b/client/src/app/modules/ascent/ascent-list/ascent-list.component.ts @@ -351,24 +351,20 @@ export class AscentListComponent implements OnInit, OnChanges { } confirmDeleteAscent(event: Event, ascent: Ascent) { - this.translocoService.load(`${environment.language}`).subscribe(() => { - this.confirmationService.confirm({ - target: event.target, - message: this.translocoService.translate( - marker('ascent.askReallyWantToDeleteAscent'), - ), - acceptLabel: this.translocoService.translate( - marker('ascent.yesDelete'), - ), - acceptButtonStyleClass: 'p-button-danger', - rejectLabel: this.translocoService.translate( - marker('ascent.noDontDelete'), - ), - icon: 'pi pi-exclamation-triangle', - accept: () => { - this.deleteAscent(ascent); - }, - }); + this.confirmationService.confirm({ + target: event.target, + message: this.translocoService.translate( + marker('ascent.askReallyWantToDeleteAscent'), + ), + acceptLabel: this.translocoService.translate(marker('ascent.yesDelete')), + acceptButtonStyleClass: 'p-button-danger', + rejectLabel: this.translocoService.translate( + marker('ascent.noDontDelete'), + ), + icon: 'pi pi-exclamation-triangle', + accept: () => { + this.deleteAscent(ascent); + }, }); } diff --git a/client/src/app/modules/blog/post-form/post-form.component.ts b/client/src/app/modules/blog/post-form/post-form.component.ts index 731d5c82..a2e4bafe 100644 --- a/client/src/app/modules/blog/post-form/post-form.component.ts +++ b/client/src/app/modules/blog/post-form/post-form.component.ts @@ -15,7 +15,6 @@ import { ConfirmationService } from 'primeng/api'; import { catchError } from 'rxjs/operators'; import { of } from 'rxjs'; import { marker } from '@jsverse/transloco-keys-manager/marker'; -import { environment } from '../../../../environments/environment'; import { toastNotification } from '../../../ngrx/actions/notifications.actions'; import { Post } from '../../../models/post'; import { PostsService } from '../../../services/crud/posts.service'; @@ -181,22 +180,20 @@ export class PostFormComponent implements OnInit { * @param event Click event. */ confirmDeletePost(event: Event) { - this.translocoService.load(`${environment.language}`).subscribe(() => { - this.confirmationService.confirm({ - target: event.target, - message: this.translocoService.translate( - marker('posts.askReallyWantToDeletePost'), - ), - acceptLabel: this.translocoService.translate(marker('posts.yesDelete')), - acceptButtonStyleClass: 'p-button-danger', - rejectLabel: this.translocoService.translate( - marker('posts.noDontDelete'), - ), - icon: 'pi pi-exclamation-triangle', - accept: () => { - this.deletePost(); - }, - }); + this.confirmationService.confirm({ + target: event.target, + message: this.translocoService.translate( + marker('posts.askReallyWantToDeletePost'), + ), + acceptLabel: this.translocoService.translate(marker('posts.yesDelete')), + acceptButtonStyleClass: 'p-button-danger', + rejectLabel: this.translocoService.translate( + marker('posts.noDontDelete'), + ), + icon: 'pi pi-exclamation-triangle', + accept: () => { + this.deletePost(); + }, }); } diff --git a/client/src/app/modules/blog/post-list/post-list.component.ts b/client/src/app/modules/blog/post-list/post-list.component.ts index a1d39463..2ba1c313 100644 --- a/client/src/app/modules/blog/post-list/post-list.component.ts +++ b/client/src/app/modules/blog/post-list/post-list.component.ts @@ -5,7 +5,6 @@ import { forkJoin, Observable } from 'rxjs'; import { select, Store } from '@ngrx/store'; import { TranslocoDirective, TranslocoService } from '@jsverse/transloco'; import { selectIsMobile } from '../../../ngrx/selectors/device.selectors'; -import { environment } from '../../../../environments/environment'; import { marker } from '@jsverse/transloco-keys-manager/marker'; import { Post } from '../../../models/post'; import { PostsService } from '../../../services/crud/posts.service'; @@ -79,10 +78,7 @@ export class PostListComponent implements OnInit { * Loads new data. */ refreshData() { - forkJoin([ - this.postsService.getPosts(), - this.translocoService.load(`${environment.language}`), - ]).subscribe(([posts]) => { + forkJoin([this.postsService.getPosts()]).subscribe(([posts]) => { this.posts = posts; this.loading = LoadingState.DEFAULT; this.sortOptions = [ diff --git a/client/src/app/modules/core/account-form/account-form.component.html b/client/src/app/modules/core/account-form/account-form.component.html index b5775c4f..c0a28198 100644 --- a/client/src/app/modules/core/account-form/account-form.component.html +++ b/client/src/app/modules/core/account-form/account-form.component.html @@ -115,10 +115,6 @@

{{ t("accountFormTitle") }}

} - - {{ t("NotificationsTitle") }} - - diff --git a/client/src/app/modules/core/account-settings-form/account-settings-form.component.html b/client/src/app/modules/core/account-settings-form/account-settings-form.component.html index 09a6039f..d004349f 100644 --- a/client/src/app/modules/core/account-settings-form/account-settings-form.component.html +++ b/client/src/app/modules/core/account-settings-form/account-settings-form.component.html @@ -1,6 +1,20 @@
+
+ +
+ +
+ +
+
+ +
diff --git a/client/src/app/modules/core/account-settings-form/account-settings-form.component.ts b/client/src/app/modules/core/account-settings-form/account-settings-form.component.ts index 0655e7cf..a5415d07 100644 --- a/client/src/app/modules/core/account-settings-form/account-settings-form.component.ts +++ b/client/src/app/modules/core/account-settings-form/account-settings-form.component.ts @@ -11,6 +11,10 @@ import { AccountService } from '../../../services/crud/account.service'; import { AccountSettings } from '../../../models/account-settings'; import { toastNotification } from '../../../ngrx/actions/notifications.actions'; import { Store } from '@ngrx/store'; +import { LanguageSelectComponent } from '../../shared/forms/controls/language-select/language-select.component'; +import { Tooltip } from 'primeng/tooltip'; +import { Divider } from 'primeng/divider'; +import { LanguageService } from '../../../services/core/language.service'; @Component({ selector: 'lc-account-settings-form', @@ -22,6 +26,9 @@ import { Store } from '@ngrx/store'; TranslocoDirective, FormDirective, Button, + LanguageSelectComponent, + Tooltip, + Divider, ], templateUrl: './account-settings-form.component.html', styleUrl: './account-settings-form.component.scss', @@ -37,10 +44,12 @@ export class AccountSettingsFormComponent implements OnInit { private accountService = inject(AccountService); private fb = inject(FormBuilder); private store = inject(Store); + private languageService = inject(LanguageService); private buildForm() { this.accountSettingsForm = this.fb.group({ commentReplyMailsEnabled: [null], + language: [null], }); } @@ -58,6 +67,7 @@ export class AccountSettingsFormComponent implements OnInit { this.accountSettingsForm.enable(); this.accountSettingsForm.patchValue({ commentReplyMailsEnabled: this.accountSettings.commentReplyMailsEnabled, + language: this.accountSettings.language, }); } @@ -68,6 +78,7 @@ export class AccountSettingsFormComponent implements OnInit { accountSettings.commentReplyMailsEnabled = this.accountSettingsForm.get( 'commentReplyMailsEnabled', ).value; + accountSettings.language = this.accountSettingsForm.get('language').value; this.accountService.updateAccountSettings(accountSettings).subscribe({ next: () => { this.store.dispatch(toastNotification('ACCOUNT_SETTINGS_UPDATED')); diff --git a/client/src/app/modules/core/app.config.ts b/client/src/app/modules/core/app.config.ts index 40dce831..a0e07c73 100644 --- a/client/src/app/modules/core/app.config.ts +++ b/client/src/app/modules/core/app.config.ts @@ -27,13 +27,11 @@ import { provideTransloco, Translation, TranslocoLoader, - TranslocoService, } from '@jsverse/transloco'; import { provideStore, Store } from '@ngrx/store'; import { InstanceSettingsService } from '../../services/crud/instance-settings.service'; import { MenuItemsService } from '../../services/crud/menu-items.service'; -import { selectGymMode } from '../../ngrx/selectors/instance-settings.selectors'; -import { forkJoin } from 'rxjs'; +import { concatMap } from 'rxjs'; import { map } from 'rxjs/operators'; import { updateInstanceSettings } from '../../ngrx/actions/instance-settings.actions'; import { MessageService } from 'primeng/api'; @@ -46,6 +44,7 @@ import { AppLevelAlertsEffects } from 'src/app/ngrx/effects/app-level-alerts.eff import { NotificationsEffects } from '../../ngrx/effects/notifications.effects'; import { MatomoInitializerService, provideMatomo } from 'ngx-matomo-client'; import { provideTranslocoMessageformat } from '@jsverse/transloco-messageformat'; +import { LanguageService } from '../../services/core/language.service'; /** * Transloco HTTP loader. @@ -66,56 +65,34 @@ export class TranslocoHttpLoader implements TranslocoLoader { } } -const preloadTranslations = (transloco: TranslocoService, store: Store) => { - return () => { - store.select(selectGymMode).subscribe((gymMode) => { - if (gymMode && !transloco.getActiveLang().endsWith('-gym')) { - transloco.setActiveLang(environment.language + '-gym'); - transloco.setFallbackLangForMissingTranslation({ - fallbackLang: environment.language, - }); - } else if (!gymMode && transloco.getActiveLang().endsWith('-gym')) { - transloco.setActiveLang(environment.language); - } - }); - - return forkJoin( - [ - '', - 'crag/', - 'sector/', - 'area/', - 'line/', - 'topoImage/', - 'linePath/', - 'maps/', - ].flatMap((path) => [ - transloco.load(path + environment.language), - transloco.load(path + environment.language + '-gym'), - ]), - ); - }; -}; - -const preloadMenus = (menuItemsService: MenuItemsService) => { - return () => { - return menuItemsService.getMenuItems(); - }; -}; - -const preloadInstanceSettings = ( +/** + * Initializes instance settings and language on app startup. + * @param instanceSettingsService + * @param languageService + * @param store + */ +const initInstanceSettingsAndLanguage = ( instanceSettingsService: InstanceSettingsService, + languageService: LanguageService, store: Store, ) => { return () => { return instanceSettingsService.getInstanceSettings().pipe( map((instanceSettings) => { store.dispatch(updateInstanceSettings({ settings: instanceSettings })); + return instanceSettings.language; }), + concatMap(languageService.initApp.bind(languageService)), ); }; }; +const preloadMenus = (menuItemsService: MenuItemsService) => { + return () => { + return menuItemsService.getMenuItems(); + }; +}; + const initMatomo = ( actions: Actions, matomoInitializer: MatomoInitializerService, @@ -132,6 +109,12 @@ const initMatomo = ( }; }; +export function localeFactory(): string { + return ( + localStorage.getItem('preferredLanguage') || navigator.language || 'en-US' + ); +} + export const appConfig: ApplicationConfig = { providers: [ { @@ -171,7 +154,7 @@ export const appConfig: ApplicationConfig = { }, { provide: LOCALE_ID, - useValue: environment.language, + useValue: localeFactory, }, provideHttpClient(withInterceptorsFromDi()), providePrimeNG({ @@ -185,16 +168,23 @@ export const appConfig: ApplicationConfig = { }), MessageService, provideAnimationsAsync(), - provideAppInitializer(() => { - const initializerFn = preloadTranslations( - inject(TranslocoService), - inject(Store), - ); - return initializerFn(); + provideTransloco({ + config: { + availableLangs: ['de', 'de-gym', 'en', 'en-gym', 'it', 'it-gym'], + defaultLang: 'de', + fallbackLang: 'en', + prodMode: environment.production, + reRenderOnLangChange: true, + missingHandler: { + useFallbackTranslation: true, + }, + }, + loader: TranslocoHttpLoader, }), provideAppInitializer(() => { - const initializerFn = preloadInstanceSettings( + const initializerFn = initInstanceSettingsAndLanguage( inject(InstanceSettingsService), + inject(LanguageService), inject(Store), ); return initializerFn(); @@ -221,19 +211,6 @@ export const appConfig: ApplicationConfig = { provideMatomo({ mode: 'deferred', }), - provideTransloco({ - config: { - availableLangs: ['de', 'de-gym'], - defaultLang: environment.language, - fallbackLang: 'de', - prodMode: environment.production, - reRenderOnLangChange: true, - missingHandler: { - useFallbackTranslation: true, - }, - }, - loader: TranslocoHttpLoader, - }), provideTranslocoMessageformat(), ], }; diff --git a/client/src/app/modules/core/instance-settings-form/instance-settings-form.component.html b/client/src/app/modules/core/instance-settings-form/instance-settings-form.component.html index e56ad456..a7edf342 100644 --- a/client/src/app/modules/core/instance-settings-form/instance-settings-form.component.html +++ b/client/src/app/modules/core/instance-settings-form/instance-settings-form.component.html @@ -215,6 +215,22 @@ >
+
+
+ + +
+ +
+ {{ t("imagesSettings") }} diff --git a/client/src/app/modules/core/instance-settings-form/instance-settings-form.component.ts b/client/src/app/modules/core/instance-settings-form/instance-settings-form.component.ts index ca0107f9..41f68b2b 100644 --- a/client/src/app/modules/core/instance-settings-form/instance-settings-form.component.ts +++ b/client/src/app/modules/core/instance-settings-form/instance-settings-form.component.ts @@ -41,6 +41,7 @@ import { IfErrorDirective } from '../../shared/forms/if-error.directive'; import { FaDefaultFormat } from '../../../enums/fa-default-format'; import { SingleImageUploadComponent } from '../../shared/forms/controls/single-image-upload/single-image-upload.component'; import { StartingPosition } from '../../../enums/starting-position'; +import { LanguageSelectComponent } from '../../shared/forms/controls/language-select/language-select.component'; @Component({ selector: 'lc-instance-settings-form', @@ -65,6 +66,7 @@ import { StartingPosition } from '../../../enums/starting-position'; FormControlDirective, IfErrorDirective, SingleImageUploadComponent, + LanguageSelectComponent, ], templateUrl: './instance-settings-form.component.html', styleUrl: './instance-settings-form.component.scss', @@ -171,6 +173,7 @@ export class InstanceSettingsFormComponent implements OnInit { faDefaultFormat: [null], defaultStartingPosition: [null, [Validators.required]], rankingPastWeeks: [null], + language: [null], }); } @@ -200,6 +203,7 @@ export class InstanceSettingsFormComponent implements OnInit { faDefaultFormat: this.instanceSettings.faDefaultFormat, defaultStartingPosition: this.instanceSettings.defaultStartingPosition, rankingPastWeeks: this.instanceSettings.rankingPastWeeks, + language: this.instanceSettings.language, }); } @@ -254,6 +258,8 @@ export class InstanceSettingsFormComponent implements OnInit { ).value; instanceSettings.rankingPastWeeks = this.instanceSettingsForm.get('rankingPastWeeks').value; + instanceSettings.language = + this.instanceSettingsForm.get('language').value; this.instanceSettingsService .updateInstanceSettings(instanceSettings) .subscribe({ diff --git a/client/src/app/modules/core/menu/menu.component.html b/client/src/app/modules/core/menu/menu.component.html index d2ccf2e7..559f8f3b 100644 --- a/client/src/app/modules/core/menu/menu.component.html +++ b/client/src/app/modules/core/menu/menu.component.html @@ -25,6 +25,13 @@ {{ t("searchPlaceholder") }}
+ ; skippedHierarchyLayers$: Observable; ref: DynamicDialogRef | undefined; + language: LanguageCode; private menuItemsService = inject(MenuItemsService); private translocoService = inject(TranslocoService); private dialogService = inject(DialogService); private actions = inject(Actions); private store = inject(Store); + private languageService = inject(LanguageService); ngOnInit() { this.logoImage$ = this.store.pipe(select(selectLogoImage)); @@ -80,6 +91,7 @@ export class MenuComponent implements OnInit { this.skippedHierarchyLayers$ = this.store.select( selectSkippedHierarchyLayers, ); + this.language = this.languageService.calculatedLanguage; this.buildMenu(); this.buildUserMenu(); this.actions @@ -97,6 +109,11 @@ export class MenuComponent implements OnInit { }); } + updateLanguage(language: LanguageCode) { + this.language = language; + this.languageService.setPreferredLanguage(language); + } + buildUserMenu() { this.store .select(selectAuthState) diff --git a/client/src/app/modules/crag/crag-form/crag-form.component.ts b/client/src/app/modules/crag/crag-form/crag-form.component.ts index b55fa1d0..05a2387e 100644 --- a/client/src/app/modules/crag/crag-form/crag-form.component.ts +++ b/client/src/app/modules/crag/crag-form/crag-form.component.ts @@ -274,22 +274,18 @@ export class CragFormComponent implements OnInit { * @param event Click event. */ confirmDeleteCrag(event: Event) { - this.translocoService.load(`${environment.language}`).subscribe(() => { - this.confirmationService.confirm({ - target: event.target, - message: this.translocoService.translate( - marker('crag.askReallyWantToDeleteCrag'), - ), - acceptLabel: this.translocoService.translate(marker('crag.yesDelete')), - acceptButtonStyleClass: 'p-button-danger', - rejectLabel: this.translocoService.translate( - marker('crag.noDontDelete'), - ), - icon: 'pi pi-exclamation-triangle', - accept: () => { - this.deleteCrag(); - }, - }); + this.confirmationService.confirm({ + target: event.target, + message: this.translocoService.translate( + marker('crag.askReallyWantToDeleteCrag'), + ), + acceptLabel: this.translocoService.translate(marker('crag.yesDelete')), + acceptButtonStyleClass: 'p-button-danger', + rejectLabel: this.translocoService.translate(marker('crag.noDontDelete')), + icon: 'pi pi-exclamation-triangle', + accept: () => { + this.deleteCrag(); + }, }); } diff --git a/client/src/app/modules/crag/crag-list/crag-list.component.ts b/client/src/app/modules/crag/crag-list/crag-list.component.ts index 18f24761..5ad5e7ae 100644 --- a/client/src/app/modules/crag/crag-list/crag-list.component.ts +++ b/client/src/app/modules/crag/crag-list/crag-list.component.ts @@ -7,7 +7,6 @@ import { TranslocoDirective, TranslocoService } from '@jsverse/transloco'; import { marker } from '@jsverse/transloco-keys-manager/marker'; import { forkJoin, Observable } from 'rxjs'; import { select, Store } from '@ngrx/store'; -import { environment } from '../../../../environments/environment'; import { selectIsMobile } from '../../../ngrx/selectors/device.selectors'; import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; import { OrderItemsComponent } from '../../shared/components/order-items/order-items.component'; @@ -87,10 +86,7 @@ export class CragListComponent implements OnInit { * Loads new data. */ refreshData() { - forkJoin([ - this.cragsService.getCrags(), - this.translocoService.load(`${environment.language}`), - ]).subscribe(([crags]) => { + forkJoin([this.cragsService.getCrags()]).subscribe(([crags]) => { this.crags = crags; this.loading = LoadingState.DEFAULT; this.sortOptions = [ diff --git a/client/src/app/modules/crag/crag/crag.component.ts b/client/src/app/modules/crag/crag/crag.component.ts index 04551f7d..5958980f 100644 --- a/client/src/app/modules/crag/crag/crag.component.ts +++ b/client/src/app/modules/crag/crag/crag.component.ts @@ -76,7 +76,6 @@ export class CragComponent implements OnInit { }), ), this.store.pipe(select(selectIsModerator), take(1)), - this.translocoService.load(`${environment.language}`), ]).subscribe(([crag, isModerator]) => { this.crag = crag; this.store diff --git a/client/src/app/modules/gallery/gallery/gallery.component.ts b/client/src/app/modules/gallery/gallery/gallery.component.ts index ff14335e..e4afe4c5 100644 --- a/client/src/app/modules/gallery/gallery/gallery.component.ts +++ b/client/src/app/modules/gallery/gallery/gallery.component.ts @@ -21,7 +21,6 @@ import { marker } from '@jsverse/transloco-keys-manager/marker'; import { ButtonModule } from 'primeng/button'; import { HasPermissionDirective } from '../../shared/directives/has-permission.directive'; import { ConfirmPopupModule } from 'primeng/confirmpopup'; -import { environment } from '../../../../environments/environment'; import { toastNotification } from '../../../ngrx/actions/notifications.actions'; import { Store } from '@ngrx/store'; import { ConfirmationService } from 'primeng/api'; @@ -172,24 +171,20 @@ export class GalleryComponent implements OnInit { } confirmDeleteImage(event: Event, image: GalleryImage) { - this.translocoService.load(`${environment.language}`).subscribe(() => { - this.confirmationService.confirm({ - target: event.target, - message: this.translocoService.translate( - marker('gallery.askReallyWantToDeleteGalleryImage'), - ), - acceptLabel: this.translocoService.translate( - marker('gallery.yesDelete'), - ), - acceptButtonStyleClass: 'p-button-danger', - rejectLabel: this.translocoService.translate( - marker('gallery.noDontDelete'), - ), - icon: 'pi pi-exclamation-triangle', - accept: () => { - this.deleteImage(image); - }, - }); + this.confirmationService.confirm({ + target: event.target, + message: this.translocoService.translate( + marker('gallery.askReallyWantToDeleteGalleryImage'), + ), + acceptLabel: this.translocoService.translate(marker('gallery.yesDelete')), + acceptButtonStyleClass: 'p-button-danger', + rejectLabel: this.translocoService.translate( + marker('gallery.noDontDelete'), + ), + icon: 'pi pi-exclamation-triangle', + accept: () => { + this.deleteImage(image); + }, }); } diff --git a/client/src/app/modules/line/line-form/line-form.component.ts b/client/src/app/modules/line/line-form/line-form.component.ts index 3bda6661..33db2914 100644 --- a/client/src/app/modules/line/line-form/line-form.component.ts +++ b/client/src/app/modules/line/line-form/line-form.component.ts @@ -26,7 +26,6 @@ import { ConfirmationService } from 'primeng/api'; import { catchError, map, take } from 'rxjs/operators'; import { forkJoin, of } from 'rxjs'; import { toastNotification } from '../../../ngrx/actions/notifications.actions'; -import { environment } from '../../../../environments/environment'; import { marker } from '@jsverse/transloco-keys-manager/marker'; import { Line } from '../../../models/line'; import { LinesService } from '../../../services/crud/lines.service'; @@ -580,22 +579,18 @@ export class LineFormComponent implements OnInit { * @param event Click event. */ confirmDeleteLine(event: Event) { - this.translocoService.load(`${environment.language}`).subscribe(() => { - this.confirmationService.confirm({ - target: event.target, - message: this.translocoService.translate( - marker('line.askReallyWantToDeleteLine'), - ), - acceptLabel: this.translocoService.translate(marker('line.yesDelete')), - acceptButtonStyleClass: 'p-button-danger', - rejectLabel: this.translocoService.translate( - marker('line.noDontDelete'), - ), - icon: 'pi pi-exclamation-triangle', - accept: () => { - this.deleteLine(); - }, - }); + this.confirmationService.confirm({ + target: event.target, + message: this.translocoService.translate( + marker('line.askReallyWantToDeleteLine'), + ), + acceptLabel: this.translocoService.translate(marker('line.yesDelete')), + acceptButtonStyleClass: 'p-button-danger', + rejectLabel: this.translocoService.translate(marker('line.noDontDelete')), + icon: 'pi pi-exclamation-triangle', + accept: () => { + this.deleteLine(); + }, }); } diff --git a/client/src/app/modules/line/line/line.component.ts b/client/src/app/modules/line/line/line.component.ts index 971230c0..ad15007c 100644 --- a/client/src/app/modules/line/line/line.component.ts +++ b/client/src/app/modules/line/line/line.component.ts @@ -129,7 +129,6 @@ export class LineComponent implements OnInit { ), this.store.pipe(select(selectIsModerator), take(1)), this.store.pipe(select(selectInstanceSettingsState), take(1)), - this.translocoService.load(`${environment.language}`), ]).subscribe( ([crag, sector, area, line, isModerator, instanceSettings]) => { this.crag = crag; diff --git a/client/src/app/modules/menu-pages/menu-items-form/menu-items-form.component.ts b/client/src/app/modules/menu-pages/menu-items-form/menu-items-form.component.ts index 2f8f925c..3ab643c1 100644 --- a/client/src/app/modules/menu-pages/menu-items-form/menu-items-form.component.ts +++ b/client/src/app/modules/menu-pages/menu-items-form/menu-items-form.component.ts @@ -25,7 +25,6 @@ import { } from '@jsverse/transloco'; import { ConfirmationService } from 'primeng/api'; import { marker } from '@jsverse/transloco-keys-manager/marker'; -import { environment } from '../../../../environments/environment'; import { catchError } from 'rxjs/operators'; import { forkJoin, Observable, of } from 'rxjs'; import { toastNotification } from '../../../ngrx/actions/notifications.actions'; @@ -298,24 +297,22 @@ export class MenuItemsFormComponent implements OnInit { * @param event Click event. */ confirmDeleteMenuItem(event: Event) { - this.translocoService.load(`${environment.language}`).subscribe(() => { - this.confirmationService.confirm({ - target: event.target, - message: this.translocoService.translate( - marker('menuItems.askReallyWantToDeleteMenuItem'), - ), - acceptLabel: this.translocoService.translate( - marker('menuItems.yesDelete'), - ), - acceptButtonStyleClass: 'p-button-danger', - rejectLabel: this.translocoService.translate( - marker('menuItems.noDontDelete'), - ), - icon: 'pi pi-exclamation-triangle', - accept: () => { - this.deleteMenuItem(); - }, - }); + this.confirmationService.confirm({ + target: event.target, + message: this.translocoService.translate( + marker('menuItems.askReallyWantToDeleteMenuItem'), + ), + acceptLabel: this.translocoService.translate( + marker('menuItems.yesDelete'), + ), + acceptButtonStyleClass: 'p-button-danger', + rejectLabel: this.translocoService.translate( + marker('menuItems.noDontDelete'), + ), + icon: 'pi pi-exclamation-triangle', + accept: () => { + this.deleteMenuItem(); + }, }); } diff --git a/client/src/app/modules/menu-pages/menu-items-list/menu-items-list.component.ts b/client/src/app/modules/menu-pages/menu-items-list/menu-items-list.component.ts index 8b6ca43c..00f8a647 100644 --- a/client/src/app/modules/menu-pages/menu-items-list/menu-items-list.component.ts +++ b/client/src/app/modules/menu-pages/menu-items-list/menu-items-list.component.ts @@ -10,7 +10,6 @@ import { } from '@jsverse/transloco'; import { selectIsMobile } from '../../../ngrx/selectors/device.selectors'; import { marker } from '@jsverse/transloco-keys-manager/marker'; -import { environment } from '../../../../environments/environment'; import { MenuItem } from '../../../models/menu-item'; import { MenuItemsService } from '../../../services/crud/menu-items.service'; import { MenuItemPosition } from '../../../enums/menu-item-position'; @@ -82,18 +81,17 @@ export class MenuItemsListComponent implements OnInit { * Loads new data. */ refreshData() { - forkJoin([ - this.menuItemsService.getMenuItems(), - this.translocoService.load(`${environment.language}`), - ]).subscribe(([menuItems]) => { - this.menuItemsTop = menuItems.filter( - (menuItem) => menuItem.position === MenuItemPosition.TOP, - ); - this.menuItemsBottom = menuItems.filter( - (menuItem) => menuItem.position === MenuItemPosition.BOTTOM, - ); - this.loading = LoadingState.DEFAULT; - }); + forkJoin([this.menuItemsService.getMenuItems()]).subscribe( + ([menuItems]) => { + this.menuItemsTop = menuItems.filter( + (menuItem) => menuItem.position === MenuItemPosition.TOP, + ); + this.menuItemsBottom = menuItems.filter( + (menuItem) => menuItem.position === MenuItemPosition.BOTTOM, + ); + this.loading = LoadingState.DEFAULT; + }, + ); } reorderMenuItems(position: MenuItemPosition) { diff --git a/client/src/app/modules/menu-pages/menu-pages-form/menu-pages-form.component.ts b/client/src/app/modules/menu-pages/menu-pages-form/menu-pages-form.component.ts index cde9881b..0f8374ad 100644 --- a/client/src/app/modules/menu-pages/menu-pages-form/menu-pages-form.component.ts +++ b/client/src/app/modules/menu-pages/menu-pages-form/menu-pages-form.component.ts @@ -15,7 +15,6 @@ import { Title } from '@angular/platform-browser'; import { TranslocoDirective, TranslocoService } from '@jsverse/transloco'; import { ConfirmationService } from 'primeng/api'; import { marker } from '@jsverse/transloco-keys-manager/marker'; -import { environment } from '../../../../environments/environment'; import { catchError } from 'rxjs/operators'; import { of } from 'rxjs'; import { toastNotification } from '../../../ngrx/actions/notifications.actions'; @@ -183,24 +182,22 @@ export class MenuPagesFormComponent implements OnInit { * @param event Click event. */ confirmDeleteMenuPage(event: Event) { - this.translocoService.load(`${environment.language}`).subscribe(() => { - this.confirmationService.confirm({ - target: event.target, - message: this.translocoService.translate( - marker('menuPages.askReallyWantToDeleteMenuPage'), - ), - acceptLabel: this.translocoService.translate( - marker('menuPages.yesDelete'), - ), - acceptButtonStyleClass: 'p-button-danger', - rejectLabel: this.translocoService.translate( - marker('menuPages.noDontDelete'), - ), - icon: 'pi pi-exclamation-triangle', - accept: () => { - this.deleteMenuPage(); - }, - }); + this.confirmationService.confirm({ + target: event.target, + message: this.translocoService.translate( + marker('menuPages.askReallyWantToDeleteMenuPage'), + ), + acceptLabel: this.translocoService.translate( + marker('menuPages.yesDelete'), + ), + acceptButtonStyleClass: 'p-button-danger', + rejectLabel: this.translocoService.translate( + marker('menuPages.noDontDelete'), + ), + icon: 'pi pi-exclamation-triangle', + accept: () => { + this.deleteMenuPage(); + }, }); } diff --git a/client/src/app/modules/menu-pages/menu-pages-list/menu-pages-list.component.ts b/client/src/app/modules/menu-pages/menu-pages-list/menu-pages-list.component.ts index caed252b..6905b6a8 100644 --- a/client/src/app/modules/menu-pages/menu-pages-list/menu-pages-list.component.ts +++ b/client/src/app/modules/menu-pages/menu-pages-list/menu-pages-list.component.ts @@ -12,7 +12,6 @@ import { select, Store } from '@ngrx/store'; import { Title } from '@angular/platform-browser'; import { selectIsMobile } from '../../../ngrx/selectors/device.selectors'; import { marker } from '@jsverse/transloco-keys-manager/marker'; -import { environment } from '../../../../environments/environment'; import { MenuPage } from '../../../models/menu-page'; import { MenuPagesService } from '../../../services/crud/menu-pages.service'; import { FormsModule } from '@angular/forms'; @@ -70,24 +69,23 @@ export class MenuPagesListComponent implements OnInit { * Loads new data. */ refreshData() { - forkJoin([ - this.menuPagesService.getMenuPages(), - this.translocoService.load(`${environment.language}`), - ]).subscribe(([menuPages]) => { - this.menuPages = menuPages; - this.loading = LoadingState.DEFAULT; - this.sortOptions = [ - { - label: this.translocoService.translate(marker('sortNewToOld')), - value: 'timeCreated', - }, - { - label: this.translocoService.translate(marker('sortOldToNew')), - value: '!timeCreated', - }, - ]; - this.sortKey = this.sortOptions[0]; - }); + forkJoin([this.menuPagesService.getMenuPages()]).subscribe( + ([menuPages]) => { + this.menuPages = menuPages; + this.loading = LoadingState.DEFAULT; + this.sortOptions = [ + { + label: this.translocoService.translate(marker('sortNewToOld')), + value: 'timeCreated', + }, + { + label: this.translocoService.translate(marker('sortOldToNew')), + value: '!timeCreated', + }, + ]; + this.sortKey = this.sortOptions[0]; + }, + ); } /** diff --git a/client/src/app/modules/region/region/region.component.ts b/client/src/app/modules/region/region/region.component.ts index e8378ee6..c226f978 100644 --- a/client/src/app/modules/region/region/region.component.ts +++ b/client/src/app/modules/region/region/region.component.ts @@ -10,7 +10,6 @@ import { Title } from '@angular/platform-browser'; import { forkJoin, of } from 'rxjs'; import { catchError, take } from 'rxjs/operators'; import { selectIsLoggedIn } from '../../../ngrx/selectors/auth.selectors'; -import { environment } from '../../../../environments/environment'; import { marker } from '@jsverse/transloco-keys-manager/marker'; import { Region } from '../../../models/region'; import { RegionService } from '../../../services/crud/region.service'; @@ -56,7 +55,6 @@ export class RegionComponent implements OnInit { }), ), this.store.pipe(select(selectIsLoggedIn), take(1)), - this.translocoService.load(`${environment.language}`), ]).subscribe(([region, isLoggedIn]) => { this.region = region; this.store.select(selectInstanceName).subscribe((instanceName) => { diff --git a/client/src/app/modules/scale/scale-form/scale-form.component.ts b/client/src/app/modules/scale/scale-form/scale-form.component.ts index 07bacbe6..52fb99c5 100644 --- a/client/src/app/modules/scale/scale-form/scale-form.component.ts +++ b/client/src/app/modules/scale/scale-form/scale-form.component.ts @@ -31,7 +31,6 @@ import { ToolbarModule } from 'primeng/toolbar'; import { ButtonModule } from 'primeng/button'; import { ConfirmPopupModule } from 'primeng/confirmpopup'; import { ConfirmationService } from 'primeng/api'; -import { environment } from '../../../../environments/environment'; import { marker } from '@jsverse/transloco-keys-manager/marker'; import { toastNotification } from '../../../ngrx/actions/notifications.actions'; import { Store } from '@ngrx/store'; @@ -385,22 +384,20 @@ export class ScaleFormComponent implements OnInit { } confirmDeleteScale(event: Event) { - this.translocoService.load(environment.language).subscribe(() => { - this.confirmationService.confirm({ - target: event.target, - message: this.translocoService.translate( - marker('scale.scaleForm.confirmDeleteMessage'), - ), - acceptLabel: this.translocoService.translate( - marker('scale.scaleForm.acceptConfirmDelete'), - ), - acceptButtonStyleClass: 'p-button-danger', - rejectLabel: this.translocoService.translate( - marker('scale.scaleForm.cancel'), - ), - icon: 'pi pi-exclamation-triangle', - accept: this.deleteScale.bind(this), - }); + this.confirmationService.confirm({ + target: event.target, + message: this.translocoService.translate( + marker('scale.scaleForm.confirmDeleteMessage'), + ), + acceptLabel: this.translocoService.translate( + marker('scale.scaleForm.acceptConfirmDelete'), + ), + acceptButtonStyleClass: 'p-button-danger', + rejectLabel: this.translocoService.translate( + marker('scale.scaleForm.cancel'), + ), + icon: 'pi pi-exclamation-triangle', + accept: this.deleteScale.bind(this), }); } diff --git a/client/src/app/modules/sector/sector-form/sector-form.component.ts b/client/src/app/modules/sector/sector-form/sector-form.component.ts index 4257b40a..d051593a 100644 --- a/client/src/app/modules/sector/sector-form/sector-form.component.ts +++ b/client/src/app/modules/sector/sector-form/sector-form.component.ts @@ -294,24 +294,20 @@ export class SectorFormComponent implements OnInit { * @param event Click event. */ confirmDeleteSector(event: Event) { - this.translocoService.load(`${environment.language}`).subscribe(() => { - this.confirmationService.confirm({ - target: event.target, - message: this.translocoService.translate( - marker('sector.askReallyWantToDeleteSector'), - ), - acceptLabel: this.translocoService.translate( - marker('sector.yesDelete'), - ), - acceptButtonStyleClass: 'p-button-danger', - rejectLabel: this.translocoService.translate( - marker('sector.noDontDelete'), - ), - icon: 'pi pi-exclamation-triangle', - accept: () => { - this.deleteSector(); - }, - }); + this.confirmationService.confirm({ + target: event.target, + message: this.translocoService.translate( + marker('sector.askReallyWantToDeleteSector'), + ), + acceptLabel: this.translocoService.translate(marker('sector.yesDelete')), + acceptButtonStyleClass: 'p-button-danger', + rejectLabel: this.translocoService.translate( + marker('sector.noDontDelete'), + ), + icon: 'pi pi-exclamation-triangle', + accept: () => { + this.deleteSector(); + }, }); } diff --git a/client/src/app/modules/sector/sector-list/sector-list.component.ts b/client/src/app/modules/sector/sector-list/sector-list.component.ts index e7a21ffa..72a65a03 100644 --- a/client/src/app/modules/sector/sector-list/sector-list.component.ts +++ b/client/src/app/modules/sector/sector-list/sector-list.component.ts @@ -3,7 +3,6 @@ import { LoadingState } from '../../../enums/loading-state'; import { forkJoin, Observable } from 'rxjs'; import { select, Store } from '@ngrx/store'; import { TranslocoDirective, TranslocoService } from '@jsverse/transloco'; -import { environment } from '../../../../environments/environment'; import { marker } from '@jsverse/transloco-keys-manager/marker'; import { selectIsMobile } from '../../../ngrx/selectors/device.selectors'; import { Sector } from '../../../models/sector'; @@ -95,36 +94,35 @@ export class SectorListComponent implements OnInit { * Loads new data. */ refreshData() { - forkJoin([ - this.sectorsService.getSectors(this.cragSlug), - this.translocoService.load(`${environment.language}`), - ]).subscribe(([sectors]) => { - this.sectors = sectors; - this.loading = LoadingState.DEFAULT; - this.sortOptions = [ - { - icon: PrimeIcons.SORT_AMOUNT_DOWN_ALT, - label: this.translocoService.translate(marker('sortAscending')), - value: '!orderIndex', - }, - { - icon: PrimeIcons.SORT_AMOUNT_DOWN, - label: this.translocoService.translate(marker('sortDescending')), - value: 'orderIndex', - }, - { - icon: PrimeIcons.SORT_ALPHA_DOWN, - label: this.translocoService.translate(marker('sortAZ')), - value: '!name', - }, - { - icon: 'pi pi-sort-alpha-down-alt', - label: this.translocoService.translate(marker('sortZA')), - value: 'name', - }, - ]; - this.sortKey = this.sortOptions[0]; - }); + forkJoin([this.sectorsService.getSectors(this.cragSlug)]).subscribe( + ([sectors]) => { + this.sectors = sectors; + this.loading = LoadingState.DEFAULT; + this.sortOptions = [ + { + icon: PrimeIcons.SORT_AMOUNT_DOWN_ALT, + label: this.translocoService.translate(marker('sortAscending')), + value: '!orderIndex', + }, + { + icon: PrimeIcons.SORT_AMOUNT_DOWN, + label: this.translocoService.translate(marker('sortDescending')), + value: 'orderIndex', + }, + { + icon: PrimeIcons.SORT_ALPHA_DOWN, + label: this.translocoService.translate(marker('sortAZ')), + value: '!name', + }, + { + icon: 'pi pi-sort-alpha-down-alt', + label: this.translocoService.translate(marker('sortZA')), + value: 'name', + }, + ]; + this.sortKey = this.sortOptions[0]; + }, + ); } /** diff --git a/client/src/app/modules/sector/sector/sector.component.ts b/client/src/app/modules/sector/sector/sector.component.ts index 9a9bcd74..da48c25c 100644 --- a/client/src/app/modules/sector/sector/sector.component.ts +++ b/client/src/app/modules/sector/sector/sector.component.ts @@ -87,7 +87,6 @@ export class SectorComponent implements OnInit { }), ), this.store.pipe(select(selectIsLoggedIn), take(1)), - this.translocoService.load(`${environment.language}`), ]).subscribe(([crag, sector, isLoggedIn]) => { this.crag = crag; this.sector = sector; diff --git a/client/src/app/modules/shared/forms/controls/language-select/language-select.component.html b/client/src/app/modules/shared/forms/controls/language-select/language-select.component.html new file mode 100644 index 00000000..0a64091a --- /dev/null +++ b/client/src/app/modules/shared/forms/controls/language-select/language-select.component.html @@ -0,0 +1,25 @@ + + +
+ + @if (!noLabel) { + {{ code | transloco }} + } +
+
+ +
+ + @if (!noLabel) { +
{{ code | transloco }}
+ } +
+
+
diff --git a/client/src/app/modules/shared/forms/controls/language-select/language-select.component.scss b/client/src/app/modules/shared/forms/controls/language-select/language-select.component.scss new file mode 100644 index 00000000..ade7341b --- /dev/null +++ b/client/src/app/modules/shared/forms/controls/language-select/language-select.component.scss @@ -0,0 +1,23 @@ +lc-language-select { + p-select { + height: 100%; + align-items: center; + } + + &.no-label .p-select-dropdown { + display: none; + } + + .language-option { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .flag { + width: 1.25rem; + height: auto; + border-radius: 2px; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1); + } +} diff --git a/client/src/app/modules/shared/forms/controls/language-select/language-select.component.ts b/client/src/app/modules/shared/forms/controls/language-select/language-select.component.ts new file mode 100644 index 00000000..052ae6de --- /dev/null +++ b/client/src/app/modules/shared/forms/controls/language-select/language-select.component.ts @@ -0,0 +1,72 @@ +import { + Component, + forwardRef, + HostBinding, + Input, + ViewEncapsulation, +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { + LANGUAGE_CODES, + LanguageCode, +} from '../../../../../utility/types/language'; +import { FormsModule } from '@angular/forms'; +import { Select } from 'primeng/select'; +import { TranslocoPipe } from '@jsverse/transloco'; + +@Component({ + selector: 'lc-language-select', + standalone: true, + imports: [FormsModule, Select, TranslocoPipe], + templateUrl: './language-select.component.html', + styleUrls: ['./language-select.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => LanguageSelectComponent), + multi: true, + }, + ], + encapsulation: ViewEncapsulation.None, +}) +export class LanguageSelectComponent implements ControlValueAccessor { + readonly languages = [...LANGUAGE_CODES]; + value: LanguageCode | null = null; + disabled = false; + + @Input() placeholder: string | undefined; + @HostBinding('class.no-label') + @Input() + noLabel: boolean = false; + + private onChange: (value: LanguageCode | null) => void = () => {}; + private onTouched: () => void = () => {}; + + writeValue(value: LanguageCode | null): void { + this.value = value; + } + + registerOnChange(fn: (value: LanguageCode | null) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + getFlagSrc(code: LanguageCode | null): string | undefined { + if (!code) return undefined; + return `assets/flags/${code}.svg`; + } + + onSelect(code: LanguageCode | null): void { + if (this.disabled) return; + this.value = code; + this.onChange(code); + this.onTouched(); + } +} diff --git a/client/src/app/modules/topo-images/topo-image-list/topo-image-list.component.ts b/client/src/app/modules/topo-images/topo-image-list/topo-image-list.component.ts index 587b3ff4..6214e138 100644 --- a/client/src/app/modules/topo-images/topo-image-list/topo-image-list.component.ts +++ b/client/src/app/modules/topo-images/topo-image-list/topo-image-list.component.ts @@ -15,7 +15,6 @@ import { TranslocoDirective, TranslocoService, } from '@jsverse/transloco'; -import { environment } from '../../../../environments/environment'; import { marker } from '@jsverse/transloco-keys-manager/marker'; import { selectIsMobile } from '../../../ngrx/selectors/device.selectors'; import { TopoImage } from '../../../models/topo-image'; @@ -189,7 +188,6 @@ export class TopoImageListComponent implements OnInit { '?' + filters.toString(), ), this.ticksService.getTicks(null, null, area.id), - this.translocoService.load(`${environment.language}`), ]); }), ) @@ -275,24 +273,22 @@ export class TopoImageListComponent implements OnInit { * @param topoImage Topo image to delete. */ confirmDeleteTopoImage(event: Event, topoImage: TopoImage) { - this.translocoService.load(`${environment.language}`).subscribe(() => { - this.confirmationService.confirm({ - target: event.target, - message: this.translocoService.translate( - marker('topoImage.askReallyWantToDeleteTopoImage'), - ), - acceptLabel: this.translocoService.translate( - marker('topoImage.yesDelete'), - ), - acceptButtonStyleClass: 'p-button-danger', - rejectLabel: this.translocoService.translate( - marker('topoImage.noDontDelete'), - ), - icon: 'pi pi-exclamation-triangle', - accept: () => { - this.deleteTopoImage(topoImage); - }, - }); + this.confirmationService.confirm({ + target: event.target, + message: this.translocoService.translate( + marker('topoImage.askReallyWantToDeleteTopoImage'), + ), + acceptLabel: this.translocoService.translate( + marker('topoImage.yesDelete'), + ), + acceptButtonStyleClass: 'p-button-danger', + rejectLabel: this.translocoService.translate( + marker('topoImage.noDontDelete'), + ), + icon: 'pi pi-exclamation-triangle', + accept: () => { + this.deleteTopoImage(topoImage); + }, }); } @@ -325,24 +321,22 @@ export class TopoImageListComponent implements OnInit { ) { event.stopPropagation(); event.preventDefault(); - this.translocoService.load(`${environment.language}`).subscribe(() => { - this.confirmationService.confirm({ - target: event.target, - message: this.translocoService.translate( - marker('topoImage.askReallyWantToDeleteLinePath'), - ), - acceptLabel: this.translocoService.translate( - marker('topoImage.yesDelete'), - ), - acceptButtonStyleClass: 'p-button-danger', - rejectLabel: this.translocoService.translate( - marker('topoImage.noDontDelete'), - ), - icon: 'pi pi-exclamation-triangle', - accept: () => { - this.deleteLinePath(linePath, topoImage); - }, - }); + this.confirmationService.confirm({ + target: event.target, + message: this.translocoService.translate( + marker('topoImage.askReallyWantToDeleteLinePath'), + ), + acceptLabel: this.translocoService.translate( + marker('topoImage.yesDelete'), + ), + acceptButtonStyleClass: 'p-button-danger', + rejectLabel: this.translocoService.translate( + marker('topoImage.noDontDelete'), + ), + icon: 'pi pi-exclamation-triangle', + accept: () => { + this.deleteLinePath(linePath, topoImage); + }, }); } diff --git a/client/src/app/modules/user/user-list/user-list.component.ts b/client/src/app/modules/user/user-list/user-list.component.ts index d45be884..f342b3f6 100644 --- a/client/src/app/modules/user/user-list/user-list.component.ts +++ b/client/src/app/modules/user/user-list/user-list.component.ts @@ -28,7 +28,6 @@ import { ChipModule } from 'primeng/chip'; import { MenuModule } from 'primeng/menu'; import { take } from 'rxjs/operators'; import { toastNotification } from '../../../ngrx/actions/notifications.actions'; -import { environment } from '../../../../environments/environment'; import { ConfirmPopupModule } from 'primeng/confirmpopup'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; import { UserPromotionTargets } from '../../../enums/user-promotion-targets'; @@ -204,25 +203,23 @@ export class UserListComponent implements OnInit { } confirmDeleteUser(user: User) { - this.translocoService.load(`${environment.language}`).subscribe(() => { - this.confirmationService.confirm({ - header: this.translocoService.translate( - marker('users.askReallyWantToDeleteUserTitle'), - ), - message: this.translocoService.translate( - marker('users.askReallyWantToDeleteUser'), - { username: user.fullname }, - ), - acceptLabel: this.translocoService.translate(marker('users.yesDelete')), - acceptButtonStyleClass: 'p-button-danger', - rejectLabel: this.translocoService.translate( - marker('users.noDontDelete'), - ), - icon: 'pi pi-exclamation-triangle', - accept: () => { - this.deleteUser(user); - }, - }); + this.confirmationService.confirm({ + header: this.translocoService.translate( + marker('users.askReallyWantToDeleteUserTitle'), + ), + message: this.translocoService.translate( + marker('users.askReallyWantToDeleteUser'), + { username: user.fullname }, + ), + acceptLabel: this.translocoService.translate(marker('users.yesDelete')), + acceptButtonStyleClass: 'p-button-danger', + rejectLabel: this.translocoService.translate( + marker('users.noDontDelete'), + ), + icon: 'pi pi-exclamation-triangle', + accept: () => { + this.deleteUser(user); + }, }); } diff --git a/client/src/app/ngrx/reducers/instance-settings.reducers.ts b/client/src/app/ngrx/reducers/instance-settings.reducers.ts index ddd92963..a91f93fe 100644 --- a/client/src/app/ngrx/reducers/instance-settings.reducers.ts +++ b/client/src/app/ngrx/reducers/instance-settings.reducers.ts @@ -3,6 +3,7 @@ import { File } from '../../models/file'; import { updateInstanceSettings } from '../actions/instance-settings.actions'; import { FaDefaultFormat } from '../../enums/fa-default-format'; import { StartingPosition } from '../../enums/starting-position'; +import { LanguageCode } from '../../utility/types/language'; export interface InstanceSettingsState { instanceName: string; @@ -27,6 +28,7 @@ export interface InstanceSettingsState { defaultStartingPosition: StartingPosition; rankingPastWeeks: number | null; disableFAInAscents: boolean; + language: LanguageCode; } export const initialInstanceSettingsState: InstanceSettingsState = { @@ -52,6 +54,7 @@ export const initialInstanceSettingsState: InstanceSettingsState = { defaultStartingPosition: StartingPosition.STAND, rankingPastWeeks: null, disableFAInAscents: false, + language: null, }; const instanceSettingsReducer = createReducer( @@ -80,6 +83,7 @@ const instanceSettingsReducer = createReducer( defaultStartingPosition: action.settings.defaultStartingPosition, rankingPastWeeks: action.settings.rankingPastWeeks, disableFAInAscents: action.settings.disableFAInAscents, + language: action.settings.language, })), ); diff --git a/client/src/app/ngrx/selectors/instance-settings.selectors.ts b/client/src/app/ngrx/selectors/instance-settings.selectors.ts index 4d7e18ed..c0beef4b 100644 --- a/client/src/app/ngrx/selectors/instance-settings.selectors.ts +++ b/client/src/app/ngrx/selectors/instance-settings.selectors.ts @@ -60,3 +60,8 @@ export const selectDisableFAInAscents = createSelector( selectInstanceSettingsState, (instanceSettingsState) => instanceSettingsState.disableFAInAscents, ); + +export const selectInstanceLanguage = createSelector( + selectInstanceSettingsState, + (instanceSettingsState) => instanceSettingsState.language, +); diff --git a/client/src/app/services/core/language.service.ts b/client/src/app/services/core/language.service.ts new file mode 100644 index 00000000..41a1dc46 --- /dev/null +++ b/client/src/app/services/core/language.service.ts @@ -0,0 +1,174 @@ +import { Injectable, inject, DestroyRef } from '@angular/core'; +import { Translation, TranslocoService } from '@jsverse/transloco'; +import { LANGUAGE_CODES, LanguageCode } from '../../utility/types/language'; +import { forkJoin, Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; +import { selectInstanceLanguage } from '../../ngrx/selectors/instance-settings.selectors'; +import { Actions, ofType } from '@ngrx/effects'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { loginSuccess } from '../../ngrx/actions/auth.actions'; +import { map } from 'rxjs/operators'; + +@Injectable({ providedIn: 'root' }) +export class LanguageService { + private store = inject(Store); + private transloco = inject(TranslocoService); + private actions$ = inject(Actions); + private destroyRef = inject(DestroyRef); + private gymMode = false; + + // Language of the instance + private instanceLanguage: LanguageCode; + // Language of a registered user + private userLanguage: LanguageCode; + // Either userLanguage or selected language for anonymous users + private preferredLanguage: LanguageCode; + // Browser language (if existing in supported languages) + readonly browserLanguage: LanguageCode; + // Currently rendered language, can contain '-gym' suffix + private renderedLanguage: string; + + constructor() { + this.browserLanguage = navigator.language.slice(0, 2) as LanguageCode; + if (!LANGUAGE_CODES.includes(this.browserLanguage)) { + this.browserLanguage = null; + } + } + + /** + * Calculates the language code that should be rendered based on priority: + * user > preferred > browser > instance + */ + get calculatedLanguage(): LanguageCode { + return ( + this.preferredLanguage || + this.userLanguage || + this.browserLanguage || + this.instanceLanguage + ); + } + + /** + * Sets the instance language. + * @param lang The language code to set as instance language. + * @param skipChangeLanguage If true, the language change will not be triggered. + */ + setInstanceLanguage(lang: LanguageCode, skipChangeLanguage = false) { + this.instanceLanguage = lang; + if (!skipChangeLanguage) { + this.changeLanguage().subscribe(); + } + } + + /** + * Sets the user language. + * @param lang The language code to set as user language. + */ + setUserLanguage(lang: LanguageCode) { + this.userLanguage = lang; + this.preferredLanguage = lang; + // In contrast to setInstanceLanguage and setPreferredLanguage, we always want to change language here + // as this is never called during app initialization and thus doesn't trigger a redundant / race condition change + this.changeLanguage().subscribe(); + + // Persist preference so user language stays after logging out + localStorage.setItem('preferredLanguage', lang); + } + + /** + * Sets the preferred language. + * @param lang The language code to set as preferred language. + * @param skipChangeLanguage If true, the language change will not be triggered. + */ + setPreferredLanguage(lang: LanguageCode, skipChangeLanguage = false) { + this.preferredLanguage = lang; + if (!skipChangeLanguage) { + this.changeLanguage().subscribe(); + } + + // Persist preference so it can be restored on next visit for anonymous users + localStorage.setItem('preferredLanguage', lang); + } + + /** + * Changes the app language based on user, preferred and instance language and browser settings. + */ + changeLanguage() { + // Set language based on priority: user > preferred > browser > instance + const lang = this.calculatedLanguage; + + // Do nothing if current language is the same + if (this.renderedLanguage === (this.gymMode ? `${lang}-gym` : lang)) { + return new Observable((subscriber) => { + subscriber.next([]); + subscriber.complete(); + }); + } + + // Set active language and preload translations + this.transloco.setActiveLang(this.gymMode ? `${lang}-gym` : lang); + this.renderedLanguage = this.gymMode ? `${lang}-gym` : lang; + this.transloco.setFallbackLangForMissingTranslation({ + fallbackLang: lang, + }); + return this.preloadScopes(lang); + } + + /** + * Initializes app language settings: + * - Init instance and preferred language (user or selected language from nav select, both stored in local storage) + * - Trigger language change + * - Then listen for login events to set user language and instance language changes for dynamic changes + * @param instanceLanguage The instances default language. + */ + initApp(instanceLanguage: LanguageCode): Observable { + this.setInstanceLanguage(instanceLanguage, true); + this.restorePreferredLanguage(); + return this.changeLanguage().pipe( + map(() => { + this.store.select(selectInstanceLanguage).subscribe((language) => { + if (language) { + this.setInstanceLanguage(language); + } + }); + this.actions$ + .pipe(ofType(loginSuccess), takeUntilDestroyed(this.destroyRef)) + .subscribe((action) => { + this.setUserLanguage(action.loginResponse.user.accountLanguage); + }); + }), + ); + } + + /** + * Restores preferred language from local storage. + * Can either be user language (logged in) or selected language (anonymous). + */ + restorePreferredLanguage() { + const saved = localStorage.getItem('preferredLanguage'); + if (!saved) return; + this.setPreferredLanguage(saved as LanguageCode, true); + } + + /** + * Loads all translation scopes for the given language. + * @param lang Language code for which to load the scopes. + */ + preloadScopes(lang: LanguageCode): Observable { + return forkJoin( + [ + '', + 'crag/', + 'sector/', + 'area/', + 'line/', + 'topoImage/', + 'linePath/', + 'maps/', + ].flatMap((path) => [ + this.transloco.load(path + lang), + ...(this.gymMode ? [this.transloco.load(path + lang + '-gym')] : []), + ]), + ); + } +} diff --git a/client/src/app/services/crud/account.service.ts b/client/src/app/services/crud/account.service.ts index dc490353..d59c6df1 100644 --- a/client/src/app/services/crud/account.service.ts +++ b/client/src/app/services/crud/account.service.ts @@ -2,8 +2,9 @@ import { inject, Injectable } from '@angular/core'; import { ApiService } from '../core/api.service'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, tap } from 'rxjs/operators'; import { AccountSettings } from '../../models/account-settings'; +import { LanguageService } from '../core/language.service'; @Injectable({ providedIn: 'root', @@ -11,6 +12,7 @@ import { AccountSettings } from '../../models/account-settings'; export class AccountService { private api = inject(ApiService); private http = inject(HttpClient); + private languageService = inject(LanguageService); public getAccountSettings(): Observable { return this.http @@ -26,6 +28,11 @@ export class AccountService { this.api.account.updateSettings(), AccountSettings.serialize(accountSettings), ) - .pipe(map(AccountSettings.deserialize)); + .pipe( + map(AccountSettings.deserialize), + tap((settings: AccountSettings) => { + this.languageService.setUserLanguage(settings.language); + }), + ); } } diff --git a/client/src/app/utility/types/language.ts b/client/src/app/utility/types/language.ts new file mode 100644 index 00000000..075d5810 --- /dev/null +++ b/client/src/app/utility/types/language.ts @@ -0,0 +1,9 @@ +import { marker } from '@jsverse/transloco-keys-manager/marker'; + +export const LANGUAGE_CODES = [ + marker('de'), + marker('en'), + marker('it'), +] as const; + +export type LanguageCode = (typeof LANGUAGE_CODES)[number]; diff --git a/client/src/assets/flags/de.svg b/client/src/assets/flags/de.svg new file mode 100644 index 00000000..d27eacd0 --- /dev/null +++ b/client/src/assets/flags/de.svg @@ -0,0 +1,6 @@ + + diff --git a/client/src/assets/flags/en.svg b/client/src/assets/flags/en.svg new file mode 100644 index 00000000..c15be0f0 --- /dev/null +++ b/client/src/assets/flags/en.svg @@ -0,0 +1,11 @@ + + diff --git a/client/src/assets/flags/it.svg b/client/src/assets/flags/it.svg new file mode 100644 index 00000000..af95b0a8 --- /dev/null +++ b/client/src/assets/flags/it.svg @@ -0,0 +1,6 @@ + + diff --git a/client/src/assets/i18n/area/en.json b/client/src/assets/i18n/area/en.json new file mode 100644 index 00000000..1b856df6 --- /dev/null +++ b/client/src/assets/i18n/area/en.json @@ -0,0 +1,31 @@ +{ + "areaList.noAreasFoundEmptyMessage": "", + "areaList.newAreaButtonLabel": "", + "areaList.reorderAreasButtonLabel": "", + "area.gradeDistribution": "", + "area.description": "", + "areaForm.createAreaDescription": "", + "areaForm.areaNameLabel": "", + "areaForm.areaNamePlaceholder": "", + "areaForm.required": "", + "areaForm.maxlength120": "", + "areaForm.areaShortDescriptionLabel": "", + "areaForm.areaShortDescriptionPlaceholder": "", + "areaForm.areaDescriptionLabel": "", + "areaForm.areaDescriptionPlaceholder": "", + "areaForm.areaPortraitImageLabel": "", + "areaForm.mapMarkersLabel": "", + "areaForm.secretOptionsLabel": "", + "areaForm.secretLabel": "", + "areaForm.closedLabel": "", + "areaForm.aPublicAreaWillSetParentsToPublic": "", + "areaForm.openingAClosedAreaWillSetParentsToOpen": "", + "areaForm.closedReasonLabel": "", + "areaForm.closedReasonPlaceholder": "", + "areaForm.createAreaButtonLabel": "", + "areaForm.editAreaButtonLabel": "", + "areaForm.cancelButtonLabel": "", + "areaForm.deleteAreaButtonLabel": "", + "areaForm.editAreaTitle": "", + "areaForm.createAreaTitle": "" +} diff --git a/client/src/assets/i18n/area/it.json b/client/src/assets/i18n/area/it.json new file mode 100644 index 00000000..1b856df6 --- /dev/null +++ b/client/src/assets/i18n/area/it.json @@ -0,0 +1,31 @@ +{ + "areaList.noAreasFoundEmptyMessage": "", + "areaList.newAreaButtonLabel": "", + "areaList.reorderAreasButtonLabel": "", + "area.gradeDistribution": "", + "area.description": "", + "areaForm.createAreaDescription": "", + "areaForm.areaNameLabel": "", + "areaForm.areaNamePlaceholder": "", + "areaForm.required": "", + "areaForm.maxlength120": "", + "areaForm.areaShortDescriptionLabel": "", + "areaForm.areaShortDescriptionPlaceholder": "", + "areaForm.areaDescriptionLabel": "", + "areaForm.areaDescriptionPlaceholder": "", + "areaForm.areaPortraitImageLabel": "", + "areaForm.mapMarkersLabel": "", + "areaForm.secretOptionsLabel": "", + "areaForm.secretLabel": "", + "areaForm.closedLabel": "", + "areaForm.aPublicAreaWillSetParentsToPublic": "", + "areaForm.openingAClosedAreaWillSetParentsToOpen": "", + "areaForm.closedReasonLabel": "", + "areaForm.closedReasonPlaceholder": "", + "areaForm.createAreaButtonLabel": "", + "areaForm.editAreaButtonLabel": "", + "areaForm.cancelButtonLabel": "", + "areaForm.deleteAreaButtonLabel": "", + "areaForm.editAreaTitle": "", + "areaForm.createAreaTitle": "" +} diff --git a/client/src/assets/i18n/crag/en.json b/client/src/assets/i18n/crag/en.json new file mode 100644 index 00000000..33c61595 --- /dev/null +++ b/client/src/assets/i18n/crag/en.json @@ -0,0 +1,32 @@ +{ + "cragList.noCragsFoundEmptyMessage": "", + "cragList.newCragButtonLabel": "", + "cragList.reorderCragsButtonLabel": "", + "crag.gradeDistribution": "", + "crag.description": "", + "crag.season": "", + "cragForm.createCragDescription": "", + "cragForm.cragNameLabel": "", + "cragForm.cragNamePlaceholder": "", + "cragForm.required": "", + "cragForm.maxlength120": "", + "cragForm.cragShortDescriptionLabel": "", + "cragForm.cragShortDescriptionPlaceholder": "", + "cragForm.cragDescriptionLabel": "", + "cragForm.cragDescriptionPlaceholder": "", + "cragForm.cragRulesLabel": "", + "cragForm.cragRulesPlaceholder": "", + "cragForm.cragPortraitImageLabel": "", + "cragForm.mapMarkersLabel": "", + "cragForm.cragOptionsLabel": "", + "cragForm.secretLabel": "", + "cragForm.closedLabel": "", + "cragForm.closedReasonLabel": "", + "cragForm.closedReasonPlaceholder": "", + "cragForm.createCragButtonLabel": "", + "cragForm.editCragButtonLabel": "", + "cragForm.cancelButtonLabel": "", + "cragForm.deleteCragButtonLabel": "", + "cragForm.editCragTitle": "", + "cragForm.createCragTitle": "" +} diff --git a/client/src/assets/i18n/crag/it.json b/client/src/assets/i18n/crag/it.json new file mode 100644 index 00000000..33c61595 --- /dev/null +++ b/client/src/assets/i18n/crag/it.json @@ -0,0 +1,32 @@ +{ + "cragList.noCragsFoundEmptyMessage": "", + "cragList.newCragButtonLabel": "", + "cragList.reorderCragsButtonLabel": "", + "crag.gradeDistribution": "", + "crag.description": "", + "crag.season": "", + "cragForm.createCragDescription": "", + "cragForm.cragNameLabel": "", + "cragForm.cragNamePlaceholder": "", + "cragForm.required": "", + "cragForm.maxlength120": "", + "cragForm.cragShortDescriptionLabel": "", + "cragForm.cragShortDescriptionPlaceholder": "", + "cragForm.cragDescriptionLabel": "", + "cragForm.cragDescriptionPlaceholder": "", + "cragForm.cragRulesLabel": "", + "cragForm.cragRulesPlaceholder": "", + "cragForm.cragPortraitImageLabel": "", + "cragForm.mapMarkersLabel": "", + "cragForm.cragOptionsLabel": "", + "cragForm.secretLabel": "", + "cragForm.closedLabel": "", + "cragForm.closedReasonLabel": "", + "cragForm.closedReasonPlaceholder": "", + "cragForm.createCragButtonLabel": "", + "cragForm.editCragButtonLabel": "", + "cragForm.cancelButtonLabel": "", + "cragForm.deleteCragButtonLabel": "", + "cragForm.editCragTitle": "", + "cragForm.createCragTitle": "" +} diff --git a/client/src/assets/i18n/de.json b/client/src/assets/i18n/de.json index d27b8702..e0ab9d5b 100644 --- a/client/src/assets/i18n/de.json +++ b/client/src/assets/i18n/de.json @@ -252,6 +252,8 @@ "instanceSettings.instanceSettingsForm.defaultStartingPosition": "Standard-Startposition", "instanceSettings.instanceSettingsForm.rankingPastWeeksLabel": "Zeitraum für Rankings", "instanceSettings.instanceSettingsForm.rankingPastWeeksTooltip": "Beschränkt die Rankings auf Begehungen aus den letzten X Wochen. Wähle \"Alle Wochen\" für ein Allzeit-Ranking.", + "instanceSettings.instanceSettingsForm.languageLabel": "Standardsprache", + "instanceSettings.instanceSettingsForm.instanceLanguageTooltip": "Die angezeigte Sprache von LocalCrag entscheidet sich in folgender Reihenfolge: 1. Nutzerpräferenz, 2. Browsersprache, 3. Standardsprache dieser Einstellung.", "instanceSettings.instanceSettingsForm.imagesSettings": "Bilder", "instanceSettings.instanceSettingsForm.logoImageLabel": "Logo", "instanceSettings.instanceSettingsForm.faviconImageLabel": "Favicon", @@ -310,6 +312,8 @@ "activateAccount.activateAccountDescription": "Du bist bereits eingeloggt. Logge dich aus um einen anderen Account zu aktivieren.", "activateAccount.activateNowButtonLabel": "Jetzt ausloggen", "activateAccount.cancelButtonLabel": "Abbrechen", + "accountSettingsForm.languageLabel": "Sprache", + "accountSettingsForm.notificationsLabel": "Benachrichtigungen", "accountSettingsForm.commentReplyMailsEnabled": "E-Mail bei Antworten auf meine Kommentare erhalten", "accountSettingsForm.saveAccountSettingsButtonLabel": "Speichern", "accountForm.accountFormTitle": "Accounteinstellungen", @@ -325,7 +329,6 @@ "accountForm.emailsDontMatchAlertText": "E-Mail Adressen stimmen nicht überein.", "accountForm.saveAccountSettingsButtonLabel": "Speichern", "accountForm.emailAddressChangeInfoText": "Damit die Änderung Deiner E-Mail Adresse wirksam wird, musst Du sie mittels dem Link bestätigen, den wir Dir gerade per Mail zugesandt haben.", - "accountForm.NotificationsTitle": "Benachrichtigungen", "accountForm.dangerZoneTitle": "Gefahrenzone", "accountForm.superadminsCannotDeleteOwnUser": "Superadmins können ihren eigenen Benutzer nicht löschen.", "accountForm.deleteAccountInfoText": "Das Löschen Deines Accounts ist dauerhaft und kann nicht rückgängig gemacht werden.", @@ -543,6 +546,9 @@ "notifications.SCALE_DELETED_MESSAGE": "Skala erfolreich entfernt", "notifications.SCALE_DELETED_ERROR_TITLE": "Fehler beim Entfernen", "notifications.SCALE_DELETED_ERROR_MESSAGE": "Beim Entfernen ist ein Fehler aufgetreten. Wird die Skala noch von Linien oder Hierarchieebenen genutzt?", + "de": "Deutsch", + "en": "Englisch", + "it": "Italienisch", "CLOSED_PROJECT": "Geschlossenes Projekt", "OPEN_PROJECT": "Offenes Projekt", "UNGRADED": "Nicht bewertet", diff --git a/client/src/assets/i18n/en.json b/client/src/assets/i18n/en.json new file mode 100644 index 00000000..0c093a1c --- /dev/null +++ b/client/src/assets/i18n/en.json @@ -0,0 +1,816 @@ +{ + "users.list.noUsersFoundEmptyMessage": "", + "users.list.userActivatedAt": "", + "users.list.userNotActivatedYet": "", + "users.list.superadmin": "", + "users.list.admin": "", + "users.list.moderator": "", + "users.list.member": "", + "users.list.userActions": "", + "users.list.userListTitle": "", + "user.charts.gradeDistribution": "", + "user.charts.completion": "", + "user.charts.lineType": "", + "user.charts.noLinesInThisGradeRange": "", + "todos.priorityButton.mediumPriority": "", + "todos.priorityButton.lowPriority": "", + "todos.priorityButton.highPriority": "", + "todos.todoList.noTodosFoundMessage": "", + "todos.todoList.orderByLabel": "", + "todos.todoList.filterLabel": "", + "todos.todoList.lineType": "", + "todos.todoList.loadMore": "", + "todos.todoList.todos": "", + "todoButton.onTodoList": "", + "todoButton.addTodo": "", + "singleImageUploader.browseFilesButtonLabel": "", + "latLabel": "", + "latPlaceholder": "", + "invalidLat": "", + "lngLabel": "", + "lngPlaceholder": "", + "invalidLng": "", + "getCoordinatesPosition": "", + "coordinatesLoadedSuccessfullyWithAccuracyX": "", + "coordinatesLoadedSuccessfullyWithAccuracyXTryingAccuracyImprovement": "", + "coordinatesCouldNotBeLoaded": "", + "advancedColorPicker.globalColor": "", + "advancedColorPicker.customColor": "", + "advancedColorPicker.accept": "", + "advancedColorPicker.cancel": "", + "secretSpotTag.secret": "", + "noDataAvailable": "", + "orderItems.orderItemImageAlt": "", + "orderItems.cancelButtonLabel": "", + "orderItems.saveButtonLabel": "", + "lines": "", + "leveledGradeDistributionUngraded": "", + "leveledGradeDistributionProjects": "", + "totalLines": "", + "plusXUngradedYProjects": "", + "plusXUngraded": "", + "plusXProjects": "", + "coordinatesButton.coordinates": "", + "closedSpotTag.closed": "", + "closedSpotAlert.closedWithReason": "", + "closedSpotAlert.closedWithoutReason": "", + "defaultScalesLabel": "", + "BOULDER": "", + "SPORT": "", + "TRAD": "", + "scale.scaleList.noScalesFoundEmptyMessage": "", + "scale.scaleList.createScale": "", + "scale.scaleList.editScales": "", + "scale.scaleForm.lineTypeLabel": "", + "scale.scaleForm.BOULDER": "", + "scale.scaleForm.SPORT": "", + "scale.scaleForm.TRAD": "", + "scale.scaleForm.nameInputLabel": "", + "scale.scaleForm.required": "", + "scale.scaleForm.gradeNameLabel": "", + "scale.scaleForm.gradeValueLabel": "", + "scale.scaleForm.valuesNotUnique": "", + "scale.scaleForm.namesNotFilled": "", + "scale.scaleForm.addGrade": "", + "scale.scaleForm.reorderGrades": "", + "scale.scaleForm.gradeBracketsLabel": "", + "scale.scaleForm.stackedChartBrackets": "", + "scale.scaleForm.gradeBracketsDescriptionStackedChart": "", + "scale.scaleForm.gradeBracketsInputLabel": "", + "scale.scaleForm.gradeBracketsErrorMsg": "", + "scale.scaleForm.gradeBracketsInvalidLength": "", + "scale.scaleForm.addBracket": "", + "scale.scaleForm.barChartBrackets": "", + "scale.scaleForm.gradeBracketsDescriptionBarChart": "", + "scale.scaleForm.barChartBracketNameLabel": "", + "scale.scaleForm.saveScale": "", + "scale.scaleForm.deleteScale": "", + "scale.scaleForm.cancel": "", + "scale.scaleForm.editScale": "", + "scale.scaleForm.createTitle": "", + "region.region.gradeDistribution": "", + "region.region.description": "", + "region.regionForm.regionNameLabel": "", + "region.regionForm.regionNamePlaceholder": "", + "region.regionForm.required": "", + "region.regionForm.maxlength120": "", + "region.regionForm.regionDescriptionLabel": "", + "region.regionForm.regionDescriptionPlaceholder": "", + "region.regionForm.regionRulesLabel": "", + "region.regionForm.regionRulesPlaceholder": "", + "region.regionForm.editRegionButtonLabel": "", + "region.regionForm.cancelButtonLabel": "", + "region.regionForm.editRegionTitle": "", + "ascents.ascentList.noRankingsFoundMessage": "", + "ascents.ascentList.includeSecretSpots": "", + "ascents.ascentList.aboutRankingsButtonLabel": "", + "ascents.ascentList.aboutRankingsText": "", + "ascents.ascentList.aboutRankingsTimeRangeSuffixSingular": "", + "ascents.ascentList.aboutRankingsTimeRangeSuffix": "", + "ascents.ascentList.aboutRankingsHeader": "", + "menuPages.menuPageList.noMenuPagesFoundEmptyMessage": "", + "menuPages.menuPageList.newMenuPageButtonLabel": "", + "menuPages.menuPageList.visitMenuPageButtonLabel": "", + "menuPages.menuPageList.editMenuPageButtonLabel": "", + "menuPages.menuPageList.menuPagesListTitle": "", + "menuPages.menuPageForm.menuPageTitleLabel": "", + "menuPages.menuPageForm.menuPageTitlePlaceholder": "", + "menuPages.menuPageForm.required": "", + "menuPages.menuPageForm.maxlength120": "", + "menuPages.menuPageForm.menuPageTextLabel": "", + "menuPages.menuPageForm.createMenuPageButtonLabel": "", + "menuPages.menuPageForm.editMenuPageButtonLabel": "", + "menuPages.menuPageForm.cancelButtonLabel": "", + "menuPages.menuPageForm.deleteMenuPageButtonLabel": "", + "menuPages.menuPageForm.editMenuPageTitle": "", + "menuPages.menuPageForm.createMenuPageTitle": "", + "menuPages.menuPageList.noMenuItemsFoundEmptyMessage": "", + "menuPages.menuPageList.topMenu": "", + "menuPages.menuPageList.newMenuItemButtonLabel": "", + "menuPages.menuPageList.reorderMenuItemsButtonLabel": "", + "menuPages.menuPageList.editMenuItemButtonLabel": "", + "menuPages.menuPageList.bottomMenu": "", + "menuPages.menuPageList.menuItemsListTitle": "", + "menuItems.menuItemForm.createMenuItemDescription": "", + "menuItems.menuItemForm.typeLabel": "", + "menuItems.menuItemForm.required": "", + "menuItems.menuItemForm.positionLabel": "", + "menuItems.menuItemForm.menuPageLabel": "", + "menuItems.menuItemForm.iconLabel": "", + "menuItems.menuItemForm.titleLabel": "", + "menuItems.menuItemForm.titlePlaceholder": "", + "menuItems.menuItemForm.maxlength120": "", + "menuItems.menuItemForm.urlLabel": "", + "menuItems.menuItemForm.urlPlaceholder": "", + "menuItems.menuItemForm.invalidHttpUrl": "", + "menuItems.menuItemForm.createMenuItemButtonLabel": "", + "menuItems.menuItemForm.editMenuItemButtonLabel": "", + "menuItems.menuItemForm.cancelButtonLabel": "", + "menuItems.menuItemForm.deleteMenuItemButtonLabel": "", + "menuItems.menuItemForm.editMenuItemTitle": "", + "menuItems.menuItemForm.createMenuItemTitle": "", + "map.map": "", + "linePathEditor.undo": "", + "linePathEditor.restart": "", + "history.openObject": "", + "history.loadMore": "", + "history.noHistory": "", + "history.historyTitle": "", + "gallery.galleryImage.createdBy": "", + "gallery.galleryForm.addGalleryImageDescription": "", + "gallery.galleryForm.editGalleryImageDescription": "", + "gallery.galleryForm.galleryImageLabel": "", + "gallery.galleryForm.required": "", + "gallery.galleryForm.tagsLabel": "", + "gallery.galleryForm.tagsPlaceholder": "", + "gallery.galleryForm.min1TagRequired": "", + "gallery.galleryForm.addGalleryImageButtonLabel": "", + "gallery.galleryForm.editGalleryImageButtonLabel": "", + "gallery.galleryForm.addImage": "", + "gallery.galleryForm.loadMore": "", + "gallery.galleryForm.thisGalleryIsEmpty": "", + "searchDialog.BOULDER": "", + "searchDialog.SPORT": "", + "searchDialog.TRAD": "", + "searchDialog.gradeCircleNotGraded": "", + "searchDialog.gradeCircleProject": "", + "searchDialog.area": "", + "searchDialog.sector": "", + "searchDialog.crag": "", + "searchDialog.user": "", + "searchDialog.searchInputPlaceholder": "", + "resetPassword.title": "", + "resetPassword.description": "", + "resetPassword.newPasswordPlaceholder": "", + "resetPassword.required": "", + "resetPassword.minLength8": "", + "resetPassword.maxlength120": "", + "resetPassword.confirmNewPasswordPlaceholder": "", + "resetPassword.passwordsDontMatchAlertText": "", + "resetPassword.changePasswordButtonLabel": "", + "registerCheckMailbox.title": "", + "registerCheckMailbox.description": "", + "register.registerTitle": "", + "register.description": "", + "register.firstnamePlaceholder": "", + "register.required": "", + "register.maxlength120": "", + "register.lastnamePlaceholder": "", + "register.emailPlaceholder": "", + "register.invalidEmailHint": "", + "register.emailTaken": "", + "register.emailConfirmPlaceholder": "", + "register.emailsDontMatchAlertText": "", + "register.registerButtonLabel": "", + "register.cancelButtonLabel": "", + "refreshLoginModal.passwordPlaceholder": "", + "refreshLoginModal.required": "", + "refreshLoginModal.logoutButtonLabel": "", + "refreshLoginModal.refreshLoginButtonLabel": "", + "refreshLoginModal.title": "", + "notFound.title": "", + "notFound.description": "", + "menu.logoImageAlt": "", + "menu.searchPlaceholder": "Search", + "login.title": "", + "login.description": "", + "login.emailPlaceholder": "", + "login.required": "", + "login.passwordPlaceholder": "", + "login.loginButtonLabel": "", + "login.forgotPasswordButtonLabel": "", + "login.registerButtonLabel": "", + "instanceSettings.instanceSettingsForm.commonSettings": "", + "instanceSettings.instanceSettingsForm.instanceNameLabel": "", + "instanceSettings.instanceSettingsForm.instanceNamePlaceholder": "", + "instanceSettings.instanceSettingsForm.required": "", + "instanceSettings.instanceSettingsForm.maxlength120": "", + "instanceSettings.instanceSettingsForm.copyrightOwnerLabel": "", + "instanceSettings.instanceSettingsForm.copyrightOwnerPlaceholder": "", + "instanceSettings.instanceSettingsForm.gymModeLabel": "", + "instanceSettings.instanceSettingsForm.gymModeLabelFalse": "", + "instanceSettings.instanceSettingsForm.gymModeLabelTrue": "", + "instanceSettings.instanceSettingsForm.displayUserGradesLabel": "", + "instanceSettings.instanceSettingsForm.displayUserGradesFalse": "", + "instanceSettings.instanceSettingsForm.displayUserGradesTrue": "", + "instanceSettings.instanceSettingsForm.displayUserRatingsLabel": "", + "instanceSettings.instanceSettingsForm.displayUserRatingsFalse": "", + "instanceSettings.instanceSettingsForm.displayUserRatingsTrue": "", + "instanceSettings.instanceSettingsForm.disableFAInAscentsLabel": "", + "instanceSettings.instanceSettingsForm.disableFAInAscentsDescription": "", + "instanceSettings.instanceSettingsForm.disableFAInAscentsFalse": "", + "instanceSettings.instanceSettingsForm.disableFAInAscentsTrue": "", + "instanceSettings.instanceSettingsForm.skippedHierarchicalLayersLabel": "", + "instanceSettings.instanceSettingsForm.skippedHierarchicalLayersDescription": "", + "instanceSettings.instanceSettingsForm.skippedHierarchicalLayersItemLabel0": "", + "instanceSettings.instanceSettingsForm.skippedHierarchicalLayersItemLabel1": "", + "instanceSettings.instanceSettingsForm.skippedHierarchicalLayersItemLabel2": "", + "instanceSettings.instanceSettingsForm.faDefaultFormat": "", + "instanceSettings.instanceSettingsForm.faDefaultFormatTooltip": "", + "instanceSettings.instanceSettingsForm.YEAR": "", + "instanceSettings.instanceSettingsForm.DATE": "", + "instanceSettings.instanceSettingsForm.defaultStartingPosition": "", + "instanceSettings.instanceSettingsForm.rankingPastWeeksLabel": "", + "instanceSettings.instanceSettingsForm.rankingPastWeeksTooltip": "", + "instanceSettings.instanceSettingsForm.languageLabel": "", + "instanceSettings.instanceSettingsForm.instanceLanguageTooltip": "", + "instanceSettings.instanceSettingsForm.imagesSettings": "", + "instanceSettings.instanceSettingsForm.logoImageLabel": "", + "instanceSettings.instanceSettingsForm.faviconImageLabel": "", + "instanceSettings.instanceSettingsForm.mainBgImageLabel": "", + "instanceSettings.instanceSettingsForm.authBgImageLabel": "", + "instanceSettings.instanceSettingsForm.arrowsSettings": "", + "instanceSettings.instanceSettingsForm.arrowColorLabel": "", + "instanceSettings.instanceSettingsForm.arrowTextColorLabel": "", + "instanceSettings.instanceSettingsForm.arrowHighlightColorLabel": "", + "instanceSettings.instanceSettingsForm.arrowHighlightTextColorLabel": "", + "instanceSettings.instanceSettingsForm.chartSettings": "", + "instanceSettings.instanceSettingsForm.barChartColorLabel": "", + "instanceSettings.instanceSettingsForm.matomoSettings": "", + "instanceSettings.instanceSettingsForm.matomoTrackerUrlLabel": "", + "instanceSettings.instanceSettingsForm.matomoTrackerUrlPlaceholder": "", + "instanceSettings.instanceSettingsForm.matomoSiteIdLabel": "", + "instanceSettings.instanceSettingsForm.matomoSiteIdPlaceholder": "", + "instanceSettings.instanceSettingsForm.mapSettings": "", + "instanceSettings.instanceSettingsForm.maptilerApiKeyLabel": "", + "instanceSettings.instanceSettingsForm.maptilerApiKeyPlaceholder": "", + "instanceSettings.instanceSettingsForm.editInstanceSettingsButtonLabel": "", + "instanceSettings.instanceSettingsForm.editInstanceSettings": "", + "forgotPasswordCheckMailbox.title": "", + "forgotPasswordCheckMailbox.description": "", + "forgotPassword.title": "", + "forgotPassword.description": "", + "forgotPassword.emailPlaceholder": "", + "forgotPassword.required": "", + "forgotPassword.sendEmailButtonLabel": "", + "footer.copyright": "", + "accountForm.deleteAccountDialogDescription": "", + "accountForm.deleteAccountDialogEmailPlaceholder": "", + "accountForm.deleteAccountDialogCancel": "", + "accountForm.deleteAccountDialogConfirm": "", + "changePassword.changePassword": "", + "changePassword.changeYourPasswordHere": "", + "changePassword.oldPasswordPlaceholder": "", + "changePassword.required": "", + "changePassword.minLength8": "", + "changePassword.newPasswordPlaceholder": "", + "changePassword.confirmNewPasswordPlaceholder": "", + "changePassword.passwordsDontMatch": "", + "changePassword.changePasswordButtonLabel": "", + "changeEmail.changeEmailTitle": "", + "changeEmail.emailChangedSuccessfully": "", + "changeEmail.emailChangeError": "", + "changeEmail.takeMeToTheRock": "", + "appLevelAlerts.yourSessionExpiresInMinutesBefore": "", + "appLevelAlerts.yourSessionExpiresInMinutesAfter": "", + "appLevelAlerts.pleaseLogInAgain": "", + "appLevelAlerts.renewLogin": "", + "appLevelAlerts.thisWebsiteUsesCookies": "", + "appLevelAlerts.readMoreAboutCookies": "", + "appLevelAlerts.AcceptCookiesButton": "", + "activateAccount.activateAccountTitle": "", + "activateAccount.activateAccountDescription": "", + "activateAccount.activateNowButtonLabel": "", + "activateAccount.cancelButtonLabel": "", + "accountSettingsForm.languageLabel": "", + "accountSettingsForm.notificationsLabel": "", + "accountSettingsForm.commentReplyMailsEnabled": "", + "accountSettingsForm.saveAccountSettingsButtonLabel": "", + "accountForm.accountFormTitle": "", + "accountForm.accountFormDescription": "", + "accountForm.firstnamePlaceholder": "", + "accountForm.required": "", + "accountForm.maxlength120": "", + "accountForm.lastnamePlaceholder": "", + "accountForm.emailPlaceholder": "", + "accountForm.invalidEmailHint": "", + "accountForm.emailTaken": "", + "accountForm.emailConfirmPlaceholder": "", + "accountForm.emailsDontMatchAlertText": "", + "accountForm.saveAccountSettingsButtonLabel": "", + "accountForm.emailAddressChangeInfoText": "", + "accountForm.dangerZoneTitle": "", + "accountForm.superadminsCannotDeleteOwnUser": "", + "accountForm.deleteAccountInfoText": "", + "accountForm.deleteAccountButtonLabel": "", + "comments.loadMore": "", + "comments.thisCommentFeedIsEmpty": "", + "comments.editor.writeYourComment": "", + "comments.editor.maxlength2000": "", + "comments.editor.cancel": "", + "comments.editor.postComment": "", + "comments.commentDeleted": "", + "comments.reply": "", + "comments.viewReplies": "", + "comments.loadMoreReplies": "", + "posts.postList.noPostsFoundEmptyMessage": "", + "posts.postList.newPostButtonLabel": "", + "posts.postList.editPostButtonLabel": "", + "posts.postList.byUserAtDate": "", + "posts.postList.deletedUser": "", + "posts.postList.postListTitle": "", + "posts.postForm.postTitleLabel": "", + "posts.postForm.postTitlePlaceholder": "", + "posts.postForm.required": "", + "posts.postForm.maxlength120": "", + "posts.postForm.postTextLabel": "", + "posts.postForm.createPostButtonLabel": "", + "posts.postForm.editPostButtonLabel": "", + "posts.postForm.cancelButtonLabel": "", + "posts.postForm.deletePostButtonLabel": "", + "posts.postForm.editPostTitle": "", + "posts.postForm.createPostTitle": "", + "tickButton.lineClimbed": "", + "tickButton.addAscent": "", + "projectClimbedForm.projectClimbedInfoMessage": "", + "projectClimbedForm.message": "", + "projectClimbedForm.required": "", + "projectClimbedForm.sendProjectClimbedMessageButtonLabel": "", + "ascents.ascents.ascents": "", + "ascents.ascentList.noAscentsFoundMessage": "", + "ascents.ascentList.orderByLabel": "", + "ascents.ascentList.lineType": "", + "ascents.ascentList.soft": "", + "ascents.ascentList.hard": "", + "ascents.ascentList.fa": "", + "ascents.ascentList.flash": "", + "ascents.ascentList.withKneepad": "", + "ascents.ascentList.loadMore": "", + "ascentForm.ascentDate": "", + "ascentForm.yearInFutureValidationError": "", + "ascentForm.required": "", + "ascentForm.dateInFutureValidationError": "", + "ascentForm.onlyYear": "", + "ascentForm.today": "", + "ascentForm.lastSaturday": "", + "ascentForm.lastSunday": "", + "ascentForm.personalGrade": "", + "ascentForm.soft": "", + "ascentForm.gradePlaceholder": "", + "ascentForm.hard": "", + "ascentForm.unusualGradeDifferenceWarning": "", + "ascentForm.rating": "", + "ascentForm.comment": "", + "ascentForm.otherInfo": "", + "ascentForm.flash": "", + "ascentForm.withKneepad": "", + "ascentForm.fa": "", + "ascentForm.firstAscentExplanation": "", + "ascentForm.addAscentButtonLabel": "", + "ascentForm.editAscentButtonLabel": "", + "ascentCount.ascent": "", + "ascentCount.ascents": "", + "archiveButton.archive": "", + "archiveButton.unarchive": "", + "notifications.COMMENT_CREATED_SUCCESS_TITLE": "", + "notifications.COMMENT_CREATED_SUCCESS_MESSAGE": "", + "notifications.COMMENT_UPDATED_SUCCESS_TITLE": "", + "notifications.COMMENT_UPDATED_SUCCESS_MESSAGE": "", + "notifications.COMMENT_DELETE_SUCCESS_TITLE": "", + "notifications.COMMENT_DELETE_SUCCESS_MESSAGE": "", + "notifications.MAP_MARKER_REMOVED_TITLE": "", + "notifications.MAP_MARKER_REMOVED_MESSAGE": "", + "notifications.MAP_MARKER_ADDED_TITLE": "", + "notifications.MAP_MARKER_ADDED_MESSAGE": "", + "notifications.GALLERY_IMAGE_CREATED_TITLE": "", + "notifications.GALLERY_IMAGE_CREATED_MESSAGE": "", + "notifications.GALLERY_IMAGE_UPDATED_TITLE": "", + "notifications.GALLERY_IMAGE_UPDATED_MESSAGE": "", + "notifications.GALLERY_IMAGE_DELETED_TITLE": "", + "notifications.GALLERY_IMAGE_DELETED_MESSAGE": "", + "notifications.PROJECT_CLIMBED_MESSAGE_SENT_TITLE": "", + "notifications.PROJECT_CLIMBED_MESSAGE_SENT_MESSAGE": "", + "notifications.TODO_DELETED_TITLE": "", + "notifications.TODO_DELETED_MESSAGE": "", + "notifications.TODO_PRIORITY_UPDATED_TITLE": "", + "notifications.TODO_PRIORITY_UPDATED_MESSAGE": "", + "notifications.TODO_ADD_ERROR_TITLE": "", + "notifications.TODO_ADD_ERROR_MESSAGE": "", + "notifications.TODO_ADDED_TITLE": "", + "notifications.TODO_ADDED_MESSAGE": "", + "notifications.ASCENT_ADDED_TITLE": "", + "notifications.ASCENT_ADDED_MESSAGE": "", + "notifications.ASCENT_UPDATED_TITLE": "", + "notifications.ASCENT_UPDATED_MESSAGE": "", + "notifications.ASCENT_DELETED_TITLE": "", + "notifications.ASCENT_DELETED_MESSAGE": "", + "notifications.ARCHIVED_TITLE": "", + "notifications.ARCHIVED_MESSAGE": "", + "notifications.UNARCHIVED_TITLE": "", + "notifications.UNARCHIVED_MESSAGE": "", + "notifications.ARCHIVED_ERROR_TITLE": "", + "notifications.ARCHIVED_ERROR_MESSAGE": "", + "notifications.UNARCHIVED_ERROR_TITLE": "", + "notifications.UNARCHIVED_ERROR_MESSAGE": "", + "notifications.LOGIN_ERROR_TITLE": "", + "notifications.LOGIN_ERROR_MESSAGE": "", + "notifications.USER_PROMOTED_TITLE": "", + "notifications.USER_PROMOTED_MESSAGE": "", + "notifications.USER_DELETED_TITLE": "", + "notifications.USER_DELETED_MESSAGE": "", + "notifications.CREATE_USER_MAIL_SENT_TITLE": "", + "notifications.CREATE_USER_MAIL_SENT_MESSAGE": "", + "notifications.ACCOUNT_SETTINGS_UPDATED_TITLE": "", + "notifications.ACCOUNT_SETTINGS_UPDATED_MESSAGE": "", + "notifications.INSTANCE_SETTINGS_UPDATED_TITLE": "", + "notifications.INSTANCE_SETTINGS_UPDATED_MESSAGE": "", + "notifications.INSTANCE_SETTINGS_ERROR_MIGRATION_IMPOSSIBLE_TITLE": "", + "notifications.INSTANCE_SETTINGS_ERROR_MIGRATION_IMPOSSIBLE_MESSAGE": "", + "notifications.REGION_UPDATED_TITLE": "", + "notifications.REGION_UPDATED_MESSAGE": "", + "notifications.USER_NOT_ACTIVATED_TITLE": "", + "notifications.USER_NOT_ACTIVATED_MESSAGE": "", + "notifications.LOGIN_SUCCESS_TITLE": "", + "notifications.LOGIN_SUCCESS_MESSAGE": "", + "notifications.CHANGE_PASSWORD_SUCCESS_TITLE": "", + "notifications.CHANGE_PASSWORD_SUCCESS_MESSAGE": "", + "notifications.FORGOT_PASSWORD_SUCCESS_TITLE": "", + "notifications.FORGOT_PASSWORD_SUCCESS_MESSAGE": "", + "notifications.RESET_PASSWORD_SUCCESS_TITLE": "", + "notifications.RESET_PASSWORD_SUCCESS_MESSAGE": "", + "notifications.COOKIES_ALLOWED_TITLE": "", + "notifications.COOKIES_ALLOWED_MESSAGE": "", + "notifications.LOGOUT_SUCCESS_TITLE": "", + "notifications.LOGOUT_SUCCESS_MESSAGE": "", + "notifications.AUTO_LOGOUT_SUCCESS_TITLE": "", + "notifications.AUTO_LOGOUT_SUCCESS_MESSAGE": "", + "notifications.UNKNOWN_AUTHENTICATION_ERROR_TITLE": "", + "notifications.UNKNOWN_AUTHENTICATION_ERROR_MESSAGE": "", + "notifications.UNKNOWN_ERROR_TITLE": "", + "notifications.UNKNOWN_ERROR_MESSAGE": "", + "notifications.LOG_OUT_TO_USE_THIS_FUNCTION_TITLE": "", + "notifications.LOG_OUT_TO_USE_THIS_FUNCTION_MESSAGE": "", + "notifications.CRAG_CREATED_TITLE": "", + "notifications.CRAG_CREATED_MESSAGE": "", + "notifications.CRAG_UPDATED_TITLE": "", + "notifications.CRAG_UPDATED_MESSAGE": "", + "notifications.CRAG_DELETED_TITLE": "", + "notifications.CRAG_DELETED_MESSAGE": "", + "notifications.SECTOR_CREATED_TITLE": "", + "notifications.SECTOR_CREATED_MESSAGE": "", + "notifications.SECTOR_UPDATED_TITLE": "", + "notifications.SECTOR_UPDATED_MESSAGE": "", + "notifications.SECTOR_DELETED_TITLE": "", + "notifications.SECTOR_DELETED_MESSAGE": "", + "notifications.AREA_CREATED_TITLE": "", + "notifications.AREA_CREATED_MESSAGE": "", + "notifications.AREA_UPDATED_TITLE": "", + "notifications.AREA_UPDATED_MESSAGE": "", + "notifications.AREA_DELETED_TITLE": "", + "notifications.AREA_DELETED_MESSAGE": "", + "notifications.LINE_CREATED_TITLE": "", + "notifications.LINE_CREATED_MESSAGE": "", + "notifications.LINE_UPDATED_TITLE": "", + "notifications.LINE_UPDATED_MESSAGE": "", + "notifications.LINE_DELETED_TITLE": "", + "notifications.LINE_DELETED_MESSAGE": "", + "notifications.POST_CREATED_TITLE": "", + "notifications.POST_CREATED_MESSAGE": "", + "notifications.POST_UPDATED_TITLE": "", + "notifications.POST_UPDATED_MESSAGE": "", + "notifications.POST_DELETED_TITLE": "", + "notifications.POST_DELETED_MESSAGE": "", + "notifications.TOPO_IMAGE_ADDED_TITLE": "", + "notifications.TOPO_IMAGE_ADDED_MESSAGE": "", + "notifications.TOPO_IMAGE_UPDATED_TITLE": "", + "notifications.TOPO_IMAGE_UPDATED_MESSAGE": "", + "notifications.TOPO_IMAGE_DELETED_TITLE": "", + "notifications.TOPO_IMAGE_DELETED_MESSAGE": "", + "notifications.LINE_PATH_ADDED_TITLE": "", + "notifications.LINE_PATH_ADDED_MESSAGE": "", + "notifications.LINE_PATH_DELETED_TITLE": "", + "notifications.LINE_PATH_DELETED_MESSAGE": "", + "notifications.MENU_PAGE_DELETED_TITLE": "", + "notifications.MENU_PAGE_DELETED_MESSAGE": "", + "notifications.MENU_PAGE_UPDATED_TITLE": "", + "notifications.MENU_PAGE_UPDATED_MESSAGE": "", + "notifications.MENU_PAGE_CREATED_TITLE": "", + "notifications.MENU_PAGE_CREATED_MESSAGE": "", + "notifications.MENU_ITEM_DELETED_TITLE": "", + "notifications.MENU_ITEM_DELETED_MESSAGE": "", + "notifications.MENU_ITEM_UPDATED_TITLE": "", + "notifications.MENU_ITEM_UPDATED_MESSAGE": "", + "notifications.MENU_ITEM_CREATED_TITLE": "", + "notifications.MENU_ITEM_CREATED_MESSAGE": "", + "notifications.USER_REGISTERED_TITLE": "", + "notifications.USER_REGISTERED_MESSAGE": "", + "notifications.SCALE_CREATED_TITLE": "", + "notifications.SCALE_CREATED_MESSAGE": "", + "notifications.SCALE_CREATED_ERROR_TITLE": "", + "notifications.SCALE_CREATED_ERROR_MESSAGE": "", + "notifications.SCALE_UPDATED_TITLE": "", + "notifications.SCALE_UPDATED_MESSAGE": "", + "notifications.SCALE_UPDATED_ERROR_TITLE": "", + "notifications.SCALE_UPDATED_ERROR_MESSAGE": "", + "notifications.SCALE_DELETED_TITLE": "", + "notifications.SCALE_DELETED_MESSAGE": "", + "notifications.SCALE_DELETED_ERROR_TITLE": "", + "notifications.SCALE_DELETED_ERROR_MESSAGE": "", + "de": "", + "en": "", + "it": "", + "CLOSED_PROJECT": "", + "OPEN_PROJECT": "", + "UNGRADED": "", + "clipboardSuccessToastTitle": "", + "clipboardSuccessToastDescription": "", + "clipboardErrorToastTitle": "", + "clipboardErrorToastDescription": "", + "sortAZ": "", + "sortZA": "", + "usersMenu.promoteToUser": "", + "usersMenu.promoteToMember": "", + "usersMenu.promoteToModerator": "", + "usersMenu.promoteToAdmin": "", + "usersMenu.resendUserCreatedMail": "", + "usersMenu.delete": "", + "users.askReallyWantToDeleteUserTitle": "", + "users.askReallyWantToDeleteUser": "", + "users.yesDelete": "", + "users.noDontDelete": "", + "user.ascents": "", + "user.charts": "", + "user.gallery": "", + "ascents": "", + "ALL": "", + "sortAscending": "", + "sortDescending": "", + "topoImage.askReallyWantToDeleteTopoImage": "", + "topoImage.yesDelete": "", + "topoImage.noDontDelete": "", + "topoImage.askReallyWantToDeleteLinePath": "", + "reorderTopoImagesDialogTitle": "", + "reorderTopoImagesDialogItemsName": "", + "reorderLinePathsDialogTitle": "", + "reorderLinePathsDialogItemsName": "", + "editTopoImageBrowserTitle": "", + "addTopoImageBrowserTitle": "", + "orderByGrade": "", + "orderByTimeCreated": "", + "orderDescending": "", + "orderAscending": "", + "allPriorities": "", + "highPriority": "", + "mediumPriority": "", + "lowPriority": "", + "allCrags": "", + "allSectors": "", + "allAreas": "", + "time.now": "", + "time.minute": "", + "time.hour": "", + "time.day": "", + "time.month": "", + "time.year": "", + "January": "", + "February": "", + "March": "", + "April": "", + "May": "", + "June": "", + "July": "", + "August": "", + "September": "", + "October": "", + "November": "", + "December": "", + "leveledGradeDistributionUntil": "", + "leveledGradeDistributionFrom": "", + "copyCoordinatesToClipboard": "", + "openCoordinatesInGoogleMaps": "", + "reorderSectorsDialogTitle": "", + "reorderSectorsDialogItemsName": "", + "sectorFormBrowserTitle": "", + "sector.askReallyWantToDeleteSector": "", + "sector.yesDelete": "", + "sector.noDontDelete": "", + "sector.infos": "", + "sector.rules": "", + "sector.areas": "", + "sector.lines": "", + "sector.ascents": "", + "sector.ranking": "", + "sector.gallery": "", + "sector.comments": "", + "sector.edit": "", + "FIRST_BAR_CHART_BRACKET": "", + "SECOND_BAR_CHART_BRACKET": "", + "scale.scaleForm.confirmDeleteMessage": "", + "scale.scaleForm.acceptConfirmDelete": "", + "region.infos": "", + "region.rules": "", + "region.crags": "", + "region.lines": "", + "region.ascents": "", + "region.ranking": "", + "region.gallery": "", + "region.comments": "", + "region.edit": "", + "top10Ranking": "", + "top50Ranking": "", + "totalCountRanking": "", + "menuPagesListBrowserTitle": "", + "sortNewToOld": "", + "sortOldToNew": "", + "editMenuPageFormBrowserTitle": "", + "menuPageFormBrowserTitle": "", + "menuPages.askReallyWantToDeleteMenuPage": "", + "menuPages.yesDelete": "", + "menuPages.noDontDelete": "", + "menuItemsListBrowserTitle": "", + "reorderMenuItemsTopDialogItemsName": "", + "reorderMenuItemsBottomDialogItemsName": "", + "reorderMenuItemsDialogTitle": "", + "pi-book": "", + "pi-building": "", + "pi-calendar": "", + "pi-camera": "", + "pi-cloud": "", + "pi-envelope": "", + "pi-flag": "", + "pi-globe": "", + "pi-home": "", + "pi-shield": "", + "pi-wallet": "", + "pi-clock": "", + "pi-shopping-bag": "", + "pi-instagram": "", + "pi-youtube": "", + "editMenuItemFormBrowserTitle": "", + "menuItemFormBrowserTitle": "", + "menuItems.askReallyWantToDeleteMenuItem": "", + "menuItems.yesDelete": "", + "menuItems.noDontDelete": "", + "maps.markerList.header": "", + "maps.markerList.closeDescriptionDialog": "", + "maps.mapMarkerForm.ACCESS_POINT": "", + "maps.mapMarkerForm.PARKING": "", + "addLinePathBrowserTitle": "", + "orderByName": "", + "orderByRating": "", + "reorderLinePathsForLineDialogTitle": "", + "reorderLinePathsForLineDialogItemsName": "", + "lineFormBrowserTitle": "", + "videoTitle": "", + "line.askReallyWantToDeleteLine": "", + "line.yesDelete": "", + "line.noDontDelete": "", + "line.infos": "", + "line.ascents": "", + "line.gallery": "", + "line.comments": "", + "line.edit": "", + "history.created_crag": "", + "history.created_sector": "", + "history.created_area": "", + "history.created_line": "", + "history.grading_changed": "", + "history.project_status_changed": "", + "history.projectClimbed": "", + "history.projectStatusChanged": "", + "history.lineGraded": "", + "history.upgrade": "", + "history.downgrade": "", + "gallery.deleteImage": "", + "gallery.addImage": "", + "gallery.askReallyWantToDeleteGalleryImage": "", + "gallery.yesDelete": "", + "gallery.noDontDelete": "", + "gallery.editImage": "", + "reorderCragsDialogTitle": "", + "reorderCragsDialogItemsName": "", + "cragFormBrowserTitle": "", + "crag.askReallyWantToDeleteCrag": "", + "crag.yesDelete": "", + "crag.noDontDelete": "", + "crag.infos": "", + "crag.rules": "", + "crag.sectors": "", + "crag.lines": "", + "crag.ascents": "", + "crag.ranking": "", + "crag.gallery": "", + "crag.comments": "", + "crag.edit": "", + "resetPasswordBrowserTitle": "", + "registerCheckMailboxBrowserTitle": "", + "registerFormTabTitle": "", + "notFoundPageBrowserTitle": "", + "menu.systemCategory": "", + "menu.menuPages": "", + "menu.menus": "", + "menu.users": "", + "menu.scales": "", + "menu.instanceSettings": "", + "menu.accountCategory": "", + "menu.accountDetail": "", + "menu.todos": "", + "menu.account": "", + "menu.changePassword": "", + "menu.logout": "", + "menu.news": "", + "menu.topo": "", + "menu.ascents": "", + "menu.ranking": "", + "menu.gallery": "", + "menu.history": "", + "instanceSettings.instanceSettingsForm.rankingPastWeeksAll": "", + "instanceSettings.instanceSettingsForm.rankingPastWeeksWeek": "", + "instanceSettings.instanceSettingsForm.rankingPastWeeksWeeks": "", + "forgotPasswordCheckMailboxBrowserTitle": "", + "forgotPasswordBrowserTitle": "", + "changePasswordBrowserTitle": "", + "changeEmailTabTitle": "", + "accountFormTabTitle": "", + "accountForm.deleteAccountDialogTitle": "", + "comments.edit": "", + "comments.delete": "", + "postListBrowserTitle": "", + "editPostFormBrowserTitle": "", + "postFormBrowserTitle": "", + "posts.askReallyWantToDeletePost": "", + "posts.yesDelete": "", + "posts.noDontDelete": "", + "batchUploadTopoImageBrowserTitle": "", + "ascent.editAscent": "", + "ascent.deleteAscent": "", + "orderByAscentDate": "", + "ascent.askReallyWantToDeleteAscent": "", + "ascent.yesDelete": "", + "ascent.noDontDelete": "", + "reorderAreasDialogTitle": "", + "reorderAreasDialogItemsName": "", + "areaFormBrowserTitle": "", + "area.askReallyWantToDeleteArea": "", + "area.yesDelete": "", + "area.noDontDelete": "", + "area.infos": "", + "area.topoImages": "", + "area.lines": "", + "area.ascents": "", + "area.gallery": "", + "area.comments": "", + "area.edit": "", + "archive.askAlsoUnArchiveLines": "", + "archive.askAlsoArchiveLines": "", + "archive.yesWithLines": "", + "archive.noWithoutLines": "", + "SIT": "", + "STAND": "", + "CROUCH": "", + "FRENCH": "", + "CANDLE": "", + "LAYDOWN": "", + "MENU_PAGE": "", + "TOPO": "", + "ASCENTS": "", + "RANKING": "", + "NEWS": "", + "GALLERY": "", + "HISTORY": "", + "URL": "", + "BOTTOM": "", + "TOP": "", + "DATE": "", + "YEAR": "" +} diff --git a/client/src/assets/i18n/it.json b/client/src/assets/i18n/it.json new file mode 100644 index 00000000..dcfa9da0 --- /dev/null +++ b/client/src/assets/i18n/it.json @@ -0,0 +1,816 @@ +{ + "users.list.noUsersFoundEmptyMessage": "", + "users.list.userActivatedAt": "", + "users.list.userNotActivatedYet": "", + "users.list.superadmin": "", + "users.list.admin": "", + "users.list.moderator": "", + "users.list.member": "", + "users.list.userActions": "", + "users.list.userListTitle": "", + "user.charts.gradeDistribution": "", + "user.charts.completion": "", + "user.charts.lineType": "", + "user.charts.noLinesInThisGradeRange": "", + "todos.priorityButton.mediumPriority": "", + "todos.priorityButton.lowPriority": "", + "todos.priorityButton.highPriority": "", + "todos.todoList.noTodosFoundMessage": "", + "todos.todoList.orderByLabel": "", + "todos.todoList.filterLabel": "", + "todos.todoList.lineType": "", + "todos.todoList.loadMore": "", + "todos.todoList.todos": "", + "todoButton.onTodoList": "", + "todoButton.addTodo": "", + "singleImageUploader.browseFilesButtonLabel": "", + "latLabel": "", + "latPlaceholder": "", + "invalidLat": "", + "lngLabel": "", + "lngPlaceholder": "", + "invalidLng": "", + "getCoordinatesPosition": "", + "coordinatesLoadedSuccessfullyWithAccuracyX": "", + "coordinatesLoadedSuccessfullyWithAccuracyXTryingAccuracyImprovement": "", + "coordinatesCouldNotBeLoaded": "", + "advancedColorPicker.globalColor": "", + "advancedColorPicker.customColor": "", + "advancedColorPicker.accept": "", + "advancedColorPicker.cancel": "", + "secretSpotTag.secret": "", + "noDataAvailable": "", + "orderItems.orderItemImageAlt": "", + "orderItems.cancelButtonLabel": "", + "orderItems.saveButtonLabel": "", + "lines": "", + "leveledGradeDistributionUngraded": "", + "leveledGradeDistributionProjects": "", + "totalLines": "", + "plusXUngradedYProjects": "", + "plusXUngraded": "", + "plusXProjects": "", + "coordinatesButton.coordinates": "", + "closedSpotTag.closed": "", + "closedSpotAlert.closedWithReason": "", + "closedSpotAlert.closedWithoutReason": "", + "defaultScalesLabel": "", + "BOULDER": "", + "SPORT": "", + "TRAD": "", + "scale.scaleList.noScalesFoundEmptyMessage": "", + "scale.scaleList.createScale": "", + "scale.scaleList.editScales": "", + "scale.scaleForm.lineTypeLabel": "", + "scale.scaleForm.BOULDER": "", + "scale.scaleForm.SPORT": "", + "scale.scaleForm.TRAD": "", + "scale.scaleForm.nameInputLabel": "", + "scale.scaleForm.required": "", + "scale.scaleForm.gradeNameLabel": "", + "scale.scaleForm.gradeValueLabel": "", + "scale.scaleForm.valuesNotUnique": "", + "scale.scaleForm.namesNotFilled": "", + "scale.scaleForm.addGrade": "", + "scale.scaleForm.reorderGrades": "", + "scale.scaleForm.gradeBracketsLabel": "", + "scale.scaleForm.stackedChartBrackets": "", + "scale.scaleForm.gradeBracketsDescriptionStackedChart": "", + "scale.scaleForm.gradeBracketsInputLabel": "", + "scale.scaleForm.gradeBracketsErrorMsg": "", + "scale.scaleForm.gradeBracketsInvalidLength": "", + "scale.scaleForm.addBracket": "", + "scale.scaleForm.barChartBrackets": "", + "scale.scaleForm.gradeBracketsDescriptionBarChart": "", + "scale.scaleForm.barChartBracketNameLabel": "", + "scale.scaleForm.saveScale": "", + "scale.scaleForm.deleteScale": "", + "scale.scaleForm.cancel": "", + "scale.scaleForm.editScale": "", + "scale.scaleForm.createTitle": "", + "region.region.gradeDistribution": "", + "region.region.description": "", + "region.regionForm.regionNameLabel": "", + "region.regionForm.regionNamePlaceholder": "", + "region.regionForm.required": "", + "region.regionForm.maxlength120": "", + "region.regionForm.regionDescriptionLabel": "", + "region.regionForm.regionDescriptionPlaceholder": "", + "region.regionForm.regionRulesLabel": "", + "region.regionForm.regionRulesPlaceholder": "", + "region.regionForm.editRegionButtonLabel": "", + "region.regionForm.cancelButtonLabel": "", + "region.regionForm.editRegionTitle": "", + "ascents.ascentList.noRankingsFoundMessage": "", + "ascents.ascentList.includeSecretSpots": "", + "ascents.ascentList.aboutRankingsButtonLabel": "", + "ascents.ascentList.aboutRankingsText": "", + "ascents.ascentList.aboutRankingsTimeRangeSuffixSingular": "", + "ascents.ascentList.aboutRankingsTimeRangeSuffix": "", + "ascents.ascentList.aboutRankingsHeader": "", + "menuPages.menuPageList.noMenuPagesFoundEmptyMessage": "", + "menuPages.menuPageList.newMenuPageButtonLabel": "", + "menuPages.menuPageList.visitMenuPageButtonLabel": "", + "menuPages.menuPageList.editMenuPageButtonLabel": "", + "menuPages.menuPageList.menuPagesListTitle": "", + "menuPages.menuPageForm.menuPageTitleLabel": "", + "menuPages.menuPageForm.menuPageTitlePlaceholder": "", + "menuPages.menuPageForm.required": "", + "menuPages.menuPageForm.maxlength120": "", + "menuPages.menuPageForm.menuPageTextLabel": "", + "menuPages.menuPageForm.createMenuPageButtonLabel": "", + "menuPages.menuPageForm.editMenuPageButtonLabel": "", + "menuPages.menuPageForm.cancelButtonLabel": "", + "menuPages.menuPageForm.deleteMenuPageButtonLabel": "", + "menuPages.menuPageForm.editMenuPageTitle": "", + "menuPages.menuPageForm.createMenuPageTitle": "", + "menuPages.menuPageList.noMenuItemsFoundEmptyMessage": "", + "menuPages.menuPageList.topMenu": "", + "menuPages.menuPageList.newMenuItemButtonLabel": "", + "menuPages.menuPageList.reorderMenuItemsButtonLabel": "", + "menuPages.menuPageList.editMenuItemButtonLabel": "", + "menuPages.menuPageList.bottomMenu": "", + "menuPages.menuPageList.menuItemsListTitle": "", + "menuItems.menuItemForm.createMenuItemDescription": "", + "menuItems.menuItemForm.typeLabel": "", + "menuItems.menuItemForm.required": "", + "menuItems.menuItemForm.positionLabel": "", + "menuItems.menuItemForm.menuPageLabel": "", + "menuItems.menuItemForm.iconLabel": "", + "menuItems.menuItemForm.titleLabel": "", + "menuItems.menuItemForm.titlePlaceholder": "", + "menuItems.menuItemForm.maxlength120": "", + "menuItems.menuItemForm.urlLabel": "", + "menuItems.menuItemForm.urlPlaceholder": "", + "menuItems.menuItemForm.invalidHttpUrl": "", + "menuItems.menuItemForm.createMenuItemButtonLabel": "", + "menuItems.menuItemForm.editMenuItemButtonLabel": "", + "menuItems.menuItemForm.cancelButtonLabel": "", + "menuItems.menuItemForm.deleteMenuItemButtonLabel": "", + "menuItems.menuItemForm.editMenuItemTitle": "", + "menuItems.menuItemForm.createMenuItemTitle": "", + "map.map": "", + "linePathEditor.undo": "", + "linePathEditor.restart": "", + "history.openObject": "", + "history.loadMore": "", + "history.noHistory": "", + "history.historyTitle": "", + "gallery.galleryImage.createdBy": "", + "gallery.galleryForm.addGalleryImageDescription": "", + "gallery.galleryForm.editGalleryImageDescription": "", + "gallery.galleryForm.galleryImageLabel": "", + "gallery.galleryForm.required": "", + "gallery.galleryForm.tagsLabel": "", + "gallery.galleryForm.tagsPlaceholder": "", + "gallery.galleryForm.min1TagRequired": "", + "gallery.galleryForm.addGalleryImageButtonLabel": "", + "gallery.galleryForm.editGalleryImageButtonLabel": "", + "gallery.galleryForm.addImage": "", + "gallery.galleryForm.loadMore": "", + "gallery.galleryForm.thisGalleryIsEmpty": "", + "searchDialog.BOULDER": "", + "searchDialog.SPORT": "", + "searchDialog.TRAD": "", + "searchDialog.gradeCircleNotGraded": "", + "searchDialog.gradeCircleProject": "", + "searchDialog.area": "", + "searchDialog.sector": "", + "searchDialog.crag": "", + "searchDialog.user": "", + "searchDialog.searchInputPlaceholder": "", + "resetPassword.title": "", + "resetPassword.description": "", + "resetPassword.newPasswordPlaceholder": "", + "resetPassword.required": "", + "resetPassword.minLength8": "", + "resetPassword.maxlength120": "", + "resetPassword.confirmNewPasswordPlaceholder": "", + "resetPassword.passwordsDontMatchAlertText": "", + "resetPassword.changePasswordButtonLabel": "", + "registerCheckMailbox.title": "", + "registerCheckMailbox.description": "", + "register.registerTitle": "", + "register.description": "", + "register.firstnamePlaceholder": "", + "register.required": "", + "register.maxlength120": "", + "register.lastnamePlaceholder": "", + "register.emailPlaceholder": "", + "register.invalidEmailHint": "", + "register.emailTaken": "", + "register.emailConfirmPlaceholder": "", + "register.emailsDontMatchAlertText": "", + "register.registerButtonLabel": "", + "register.cancelButtonLabel": "", + "refreshLoginModal.passwordPlaceholder": "", + "refreshLoginModal.required": "", + "refreshLoginModal.logoutButtonLabel": "", + "refreshLoginModal.refreshLoginButtonLabel": "", + "refreshLoginModal.title": "", + "notFound.title": "", + "notFound.description": "", + "menu.logoImageAlt": "", + "menu.searchPlaceholder": "Ricerca", + "login.title": "", + "login.description": "", + "login.emailPlaceholder": "", + "login.required": "", + "login.passwordPlaceholder": "", + "login.loginButtonLabel": "", + "login.forgotPasswordButtonLabel": "", + "login.registerButtonLabel": "", + "instanceSettings.instanceSettingsForm.commonSettings": "", + "instanceSettings.instanceSettingsForm.instanceNameLabel": "", + "instanceSettings.instanceSettingsForm.instanceNamePlaceholder": "", + "instanceSettings.instanceSettingsForm.required": "", + "instanceSettings.instanceSettingsForm.maxlength120": "", + "instanceSettings.instanceSettingsForm.copyrightOwnerLabel": "", + "instanceSettings.instanceSettingsForm.copyrightOwnerPlaceholder": "", + "instanceSettings.instanceSettingsForm.gymModeLabel": "", + "instanceSettings.instanceSettingsForm.gymModeLabelFalse": "", + "instanceSettings.instanceSettingsForm.gymModeLabelTrue": "", + "instanceSettings.instanceSettingsForm.displayUserGradesLabel": "", + "instanceSettings.instanceSettingsForm.displayUserGradesFalse": "", + "instanceSettings.instanceSettingsForm.displayUserGradesTrue": "", + "instanceSettings.instanceSettingsForm.displayUserRatingsLabel": "", + "instanceSettings.instanceSettingsForm.displayUserRatingsFalse": "", + "instanceSettings.instanceSettingsForm.displayUserRatingsTrue": "", + "instanceSettings.instanceSettingsForm.disableFAInAscentsLabel": "", + "instanceSettings.instanceSettingsForm.disableFAInAscentsDescription": "", + "instanceSettings.instanceSettingsForm.disableFAInAscentsFalse": "", + "instanceSettings.instanceSettingsForm.disableFAInAscentsTrue": "", + "instanceSettings.instanceSettingsForm.skippedHierarchicalLayersLabel": "", + "instanceSettings.instanceSettingsForm.skippedHierarchicalLayersDescription": "", + "instanceSettings.instanceSettingsForm.skippedHierarchicalLayersItemLabel0": "", + "instanceSettings.instanceSettingsForm.skippedHierarchicalLayersItemLabel1": "", + "instanceSettings.instanceSettingsForm.skippedHierarchicalLayersItemLabel2": "", + "instanceSettings.instanceSettingsForm.faDefaultFormat": "", + "instanceSettings.instanceSettingsForm.faDefaultFormatTooltip": "", + "instanceSettings.instanceSettingsForm.YEAR": "", + "instanceSettings.instanceSettingsForm.DATE": "", + "instanceSettings.instanceSettingsForm.defaultStartingPosition": "", + "instanceSettings.instanceSettingsForm.rankingPastWeeksLabel": "", + "instanceSettings.instanceSettingsForm.rankingPastWeeksTooltip": "", + "instanceSettings.instanceSettingsForm.languageLabel": "", + "instanceSettings.instanceSettingsForm.instanceLanguageTooltip": "", + "instanceSettings.instanceSettingsForm.imagesSettings": "", + "instanceSettings.instanceSettingsForm.logoImageLabel": "", + "instanceSettings.instanceSettingsForm.faviconImageLabel": "", + "instanceSettings.instanceSettingsForm.mainBgImageLabel": "", + "instanceSettings.instanceSettingsForm.authBgImageLabel": "", + "instanceSettings.instanceSettingsForm.arrowsSettings": "", + "instanceSettings.instanceSettingsForm.arrowColorLabel": "", + "instanceSettings.instanceSettingsForm.arrowTextColorLabel": "", + "instanceSettings.instanceSettingsForm.arrowHighlightColorLabel": "", + "instanceSettings.instanceSettingsForm.arrowHighlightTextColorLabel": "", + "instanceSettings.instanceSettingsForm.chartSettings": "", + "instanceSettings.instanceSettingsForm.barChartColorLabel": "", + "instanceSettings.instanceSettingsForm.matomoSettings": "", + "instanceSettings.instanceSettingsForm.matomoTrackerUrlLabel": "", + "instanceSettings.instanceSettingsForm.matomoTrackerUrlPlaceholder": "", + "instanceSettings.instanceSettingsForm.matomoSiteIdLabel": "", + "instanceSettings.instanceSettingsForm.matomoSiteIdPlaceholder": "", + "instanceSettings.instanceSettingsForm.mapSettings": "", + "instanceSettings.instanceSettingsForm.maptilerApiKeyLabel": "", + "instanceSettings.instanceSettingsForm.maptilerApiKeyPlaceholder": "", + "instanceSettings.instanceSettingsForm.editInstanceSettingsButtonLabel": "", + "instanceSettings.instanceSettingsForm.editInstanceSettings": "", + "forgotPasswordCheckMailbox.title": "", + "forgotPasswordCheckMailbox.description": "", + "forgotPassword.title": "", + "forgotPassword.description": "", + "forgotPassword.emailPlaceholder": "", + "forgotPassword.required": "", + "forgotPassword.sendEmailButtonLabel": "", + "footer.copyright": "", + "accountForm.deleteAccountDialogDescription": "", + "accountForm.deleteAccountDialogEmailPlaceholder": "", + "accountForm.deleteAccountDialogCancel": "", + "accountForm.deleteAccountDialogConfirm": "", + "changePassword.changePassword": "", + "changePassword.changeYourPasswordHere": "", + "changePassword.oldPasswordPlaceholder": "", + "changePassword.required": "", + "changePassword.minLength8": "", + "changePassword.newPasswordPlaceholder": "", + "changePassword.confirmNewPasswordPlaceholder": "", + "changePassword.passwordsDontMatch": "", + "changePassword.changePasswordButtonLabel": "", + "changeEmail.changeEmailTitle": "", + "changeEmail.emailChangedSuccessfully": "", + "changeEmail.emailChangeError": "", + "changeEmail.takeMeToTheRock": "", + "appLevelAlerts.yourSessionExpiresInMinutesBefore": "", + "appLevelAlerts.yourSessionExpiresInMinutesAfter": "", + "appLevelAlerts.pleaseLogInAgain": "", + "appLevelAlerts.renewLogin": "", + "appLevelAlerts.thisWebsiteUsesCookies": "", + "appLevelAlerts.readMoreAboutCookies": "", + "appLevelAlerts.AcceptCookiesButton": "", + "activateAccount.activateAccountTitle": "", + "activateAccount.activateAccountDescription": "", + "activateAccount.activateNowButtonLabel": "", + "activateAccount.cancelButtonLabel": "", + "accountSettingsForm.languageLabel": "", + "accountSettingsForm.notificationsLabel": "", + "accountSettingsForm.commentReplyMailsEnabled": "", + "accountSettingsForm.saveAccountSettingsButtonLabel": "", + "accountForm.accountFormTitle": "", + "accountForm.accountFormDescription": "", + "accountForm.firstnamePlaceholder": "", + "accountForm.required": "", + "accountForm.maxlength120": "", + "accountForm.lastnamePlaceholder": "", + "accountForm.emailPlaceholder": "", + "accountForm.invalidEmailHint": "", + "accountForm.emailTaken": "", + "accountForm.emailConfirmPlaceholder": "", + "accountForm.emailsDontMatchAlertText": "", + "accountForm.saveAccountSettingsButtonLabel": "", + "accountForm.emailAddressChangeInfoText": "", + "accountForm.dangerZoneTitle": "", + "accountForm.superadminsCannotDeleteOwnUser": "", + "accountForm.deleteAccountInfoText": "", + "accountForm.deleteAccountButtonLabel": "", + "comments.loadMore": "", + "comments.thisCommentFeedIsEmpty": "", + "comments.editor.writeYourComment": "", + "comments.editor.maxlength2000": "", + "comments.editor.cancel": "", + "comments.editor.postComment": "", + "comments.commentDeleted": "", + "comments.reply": "", + "comments.viewReplies": "", + "comments.loadMoreReplies": "", + "posts.postList.noPostsFoundEmptyMessage": "", + "posts.postList.newPostButtonLabel": "", + "posts.postList.editPostButtonLabel": "", + "posts.postList.byUserAtDate": "", + "posts.postList.deletedUser": "", + "posts.postList.postListTitle": "", + "posts.postForm.postTitleLabel": "", + "posts.postForm.postTitlePlaceholder": "", + "posts.postForm.required": "", + "posts.postForm.maxlength120": "", + "posts.postForm.postTextLabel": "", + "posts.postForm.createPostButtonLabel": "", + "posts.postForm.editPostButtonLabel": "", + "posts.postForm.cancelButtonLabel": "", + "posts.postForm.deletePostButtonLabel": "", + "posts.postForm.editPostTitle": "", + "posts.postForm.createPostTitle": "", + "tickButton.lineClimbed": "", + "tickButton.addAscent": "", + "projectClimbedForm.projectClimbedInfoMessage": "", + "projectClimbedForm.message": "", + "projectClimbedForm.required": "", + "projectClimbedForm.sendProjectClimbedMessageButtonLabel": "", + "ascents.ascents.ascents": "", + "ascents.ascentList.noAscentsFoundMessage": "", + "ascents.ascentList.orderByLabel": "", + "ascents.ascentList.lineType": "", + "ascents.ascentList.soft": "", + "ascents.ascentList.hard": "", + "ascents.ascentList.fa": "", + "ascents.ascentList.flash": "", + "ascents.ascentList.withKneepad": "", + "ascents.ascentList.loadMore": "", + "ascentForm.ascentDate": "", + "ascentForm.yearInFutureValidationError": "", + "ascentForm.required": "", + "ascentForm.dateInFutureValidationError": "", + "ascentForm.onlyYear": "", + "ascentForm.today": "", + "ascentForm.lastSaturday": "", + "ascentForm.lastSunday": "", + "ascentForm.personalGrade": "", + "ascentForm.soft": "", + "ascentForm.gradePlaceholder": "", + "ascentForm.hard": "", + "ascentForm.unusualGradeDifferenceWarning": "", + "ascentForm.rating": "", + "ascentForm.comment": "", + "ascentForm.otherInfo": "", + "ascentForm.flash": "", + "ascentForm.withKneepad": "", + "ascentForm.fa": "", + "ascentForm.firstAscentExplanation": "", + "ascentForm.addAscentButtonLabel": "", + "ascentForm.editAscentButtonLabel": "", + "ascentCount.ascent": "", + "ascentCount.ascents": "", + "archiveButton.archive": "", + "archiveButton.unarchive": "", + "notifications.COMMENT_CREATED_SUCCESS_TITLE": "", + "notifications.COMMENT_CREATED_SUCCESS_MESSAGE": "", + "notifications.COMMENT_UPDATED_SUCCESS_TITLE": "", + "notifications.COMMENT_UPDATED_SUCCESS_MESSAGE": "", + "notifications.COMMENT_DELETE_SUCCESS_TITLE": "", + "notifications.COMMENT_DELETE_SUCCESS_MESSAGE": "", + "notifications.MAP_MARKER_REMOVED_TITLE": "", + "notifications.MAP_MARKER_REMOVED_MESSAGE": "", + "notifications.MAP_MARKER_ADDED_TITLE": "", + "notifications.MAP_MARKER_ADDED_MESSAGE": "", + "notifications.GALLERY_IMAGE_CREATED_TITLE": "", + "notifications.GALLERY_IMAGE_CREATED_MESSAGE": "", + "notifications.GALLERY_IMAGE_UPDATED_TITLE": "", + "notifications.GALLERY_IMAGE_UPDATED_MESSAGE": "", + "notifications.GALLERY_IMAGE_DELETED_TITLE": "", + "notifications.GALLERY_IMAGE_DELETED_MESSAGE": "", + "notifications.PROJECT_CLIMBED_MESSAGE_SENT_TITLE": "", + "notifications.PROJECT_CLIMBED_MESSAGE_SENT_MESSAGE": "", + "notifications.TODO_DELETED_TITLE": "", + "notifications.TODO_DELETED_MESSAGE": "", + "notifications.TODO_PRIORITY_UPDATED_TITLE": "", + "notifications.TODO_PRIORITY_UPDATED_MESSAGE": "", + "notifications.TODO_ADD_ERROR_TITLE": "", + "notifications.TODO_ADD_ERROR_MESSAGE": "", + "notifications.TODO_ADDED_TITLE": "", + "notifications.TODO_ADDED_MESSAGE": "", + "notifications.ASCENT_ADDED_TITLE": "", + "notifications.ASCENT_ADDED_MESSAGE": "", + "notifications.ASCENT_UPDATED_TITLE": "", + "notifications.ASCENT_UPDATED_MESSAGE": "", + "notifications.ASCENT_DELETED_TITLE": "", + "notifications.ASCENT_DELETED_MESSAGE": "", + "notifications.ARCHIVED_TITLE": "", + "notifications.ARCHIVED_MESSAGE": "", + "notifications.UNARCHIVED_TITLE": "", + "notifications.UNARCHIVED_MESSAGE": "", + "notifications.ARCHIVED_ERROR_TITLE": "", + "notifications.ARCHIVED_ERROR_MESSAGE": "", + "notifications.UNARCHIVED_ERROR_TITLE": "", + "notifications.UNARCHIVED_ERROR_MESSAGE": "", + "notifications.LOGIN_ERROR_TITLE": "", + "notifications.LOGIN_ERROR_MESSAGE": "", + "notifications.USER_PROMOTED_TITLE": "", + "notifications.USER_PROMOTED_MESSAGE": "", + "notifications.USER_DELETED_TITLE": "", + "notifications.USER_DELETED_MESSAGE": "", + "notifications.CREATE_USER_MAIL_SENT_TITLE": "", + "notifications.CREATE_USER_MAIL_SENT_MESSAGE": "", + "notifications.ACCOUNT_SETTINGS_UPDATED_TITLE": "", + "notifications.ACCOUNT_SETTINGS_UPDATED_MESSAGE": "", + "notifications.INSTANCE_SETTINGS_UPDATED_TITLE": "", + "notifications.INSTANCE_SETTINGS_UPDATED_MESSAGE": "", + "notifications.INSTANCE_SETTINGS_ERROR_MIGRATION_IMPOSSIBLE_TITLE": "", + "notifications.INSTANCE_SETTINGS_ERROR_MIGRATION_IMPOSSIBLE_MESSAGE": "", + "notifications.REGION_UPDATED_TITLE": "", + "notifications.REGION_UPDATED_MESSAGE": "", + "notifications.USER_NOT_ACTIVATED_TITLE": "", + "notifications.USER_NOT_ACTIVATED_MESSAGE": "", + "notifications.LOGIN_SUCCESS_TITLE": "", + "notifications.LOGIN_SUCCESS_MESSAGE": "", + "notifications.CHANGE_PASSWORD_SUCCESS_TITLE": "", + "notifications.CHANGE_PASSWORD_SUCCESS_MESSAGE": "", + "notifications.FORGOT_PASSWORD_SUCCESS_TITLE": "", + "notifications.FORGOT_PASSWORD_SUCCESS_MESSAGE": "", + "notifications.RESET_PASSWORD_SUCCESS_TITLE": "", + "notifications.RESET_PASSWORD_SUCCESS_MESSAGE": "", + "notifications.COOKIES_ALLOWED_TITLE": "", + "notifications.COOKIES_ALLOWED_MESSAGE": "", + "notifications.LOGOUT_SUCCESS_TITLE": "", + "notifications.LOGOUT_SUCCESS_MESSAGE": "", + "notifications.AUTO_LOGOUT_SUCCESS_TITLE": "", + "notifications.AUTO_LOGOUT_SUCCESS_MESSAGE": "", + "notifications.UNKNOWN_AUTHENTICATION_ERROR_TITLE": "", + "notifications.UNKNOWN_AUTHENTICATION_ERROR_MESSAGE": "", + "notifications.UNKNOWN_ERROR_TITLE": "", + "notifications.UNKNOWN_ERROR_MESSAGE": "", + "notifications.LOG_OUT_TO_USE_THIS_FUNCTION_TITLE": "", + "notifications.LOG_OUT_TO_USE_THIS_FUNCTION_MESSAGE": "", + "notifications.CRAG_CREATED_TITLE": "", + "notifications.CRAG_CREATED_MESSAGE": "", + "notifications.CRAG_UPDATED_TITLE": "", + "notifications.CRAG_UPDATED_MESSAGE": "", + "notifications.CRAG_DELETED_TITLE": "", + "notifications.CRAG_DELETED_MESSAGE": "", + "notifications.SECTOR_CREATED_TITLE": "", + "notifications.SECTOR_CREATED_MESSAGE": "", + "notifications.SECTOR_UPDATED_TITLE": "", + "notifications.SECTOR_UPDATED_MESSAGE": "", + "notifications.SECTOR_DELETED_TITLE": "", + "notifications.SECTOR_DELETED_MESSAGE": "", + "notifications.AREA_CREATED_TITLE": "", + "notifications.AREA_CREATED_MESSAGE": "", + "notifications.AREA_UPDATED_TITLE": "", + "notifications.AREA_UPDATED_MESSAGE": "", + "notifications.AREA_DELETED_TITLE": "", + "notifications.AREA_DELETED_MESSAGE": "", + "notifications.LINE_CREATED_TITLE": "", + "notifications.LINE_CREATED_MESSAGE": "", + "notifications.LINE_UPDATED_TITLE": "", + "notifications.LINE_UPDATED_MESSAGE": "", + "notifications.LINE_DELETED_TITLE": "", + "notifications.LINE_DELETED_MESSAGE": "", + "notifications.POST_CREATED_TITLE": "", + "notifications.POST_CREATED_MESSAGE": "", + "notifications.POST_UPDATED_TITLE": "", + "notifications.POST_UPDATED_MESSAGE": "", + "notifications.POST_DELETED_TITLE": "", + "notifications.POST_DELETED_MESSAGE": "", + "notifications.TOPO_IMAGE_ADDED_TITLE": "", + "notifications.TOPO_IMAGE_ADDED_MESSAGE": "", + "notifications.TOPO_IMAGE_UPDATED_TITLE": "", + "notifications.TOPO_IMAGE_UPDATED_MESSAGE": "", + "notifications.TOPO_IMAGE_DELETED_TITLE": "", + "notifications.TOPO_IMAGE_DELETED_MESSAGE": "", + "notifications.LINE_PATH_ADDED_TITLE": "", + "notifications.LINE_PATH_ADDED_MESSAGE": "", + "notifications.LINE_PATH_DELETED_TITLE": "", + "notifications.LINE_PATH_DELETED_MESSAGE": "", + "notifications.MENU_PAGE_DELETED_TITLE": "", + "notifications.MENU_PAGE_DELETED_MESSAGE": "", + "notifications.MENU_PAGE_UPDATED_TITLE": "", + "notifications.MENU_PAGE_UPDATED_MESSAGE": "", + "notifications.MENU_PAGE_CREATED_TITLE": "", + "notifications.MENU_PAGE_CREATED_MESSAGE": "", + "notifications.MENU_ITEM_DELETED_TITLE": "", + "notifications.MENU_ITEM_DELETED_MESSAGE": "", + "notifications.MENU_ITEM_UPDATED_TITLE": "", + "notifications.MENU_ITEM_UPDATED_MESSAGE": "", + "notifications.MENU_ITEM_CREATED_TITLE": "", + "notifications.MENU_ITEM_CREATED_MESSAGE": "", + "notifications.USER_REGISTERED_TITLE": "", + "notifications.USER_REGISTERED_MESSAGE": "", + "notifications.SCALE_CREATED_TITLE": "", + "notifications.SCALE_CREATED_MESSAGE": "", + "notifications.SCALE_CREATED_ERROR_TITLE": "", + "notifications.SCALE_CREATED_ERROR_MESSAGE": "", + "notifications.SCALE_UPDATED_TITLE": "", + "notifications.SCALE_UPDATED_MESSAGE": "", + "notifications.SCALE_UPDATED_ERROR_TITLE": "", + "notifications.SCALE_UPDATED_ERROR_MESSAGE": "", + "notifications.SCALE_DELETED_TITLE": "", + "notifications.SCALE_DELETED_MESSAGE": "", + "notifications.SCALE_DELETED_ERROR_TITLE": "", + "notifications.SCALE_DELETED_ERROR_MESSAGE": "", + "de": "", + "en": "", + "it": "", + "CLOSED_PROJECT": "", + "OPEN_PROJECT": "", + "UNGRADED": "", + "clipboardSuccessToastTitle": "", + "clipboardSuccessToastDescription": "", + "clipboardErrorToastTitle": "", + "clipboardErrorToastDescription": "", + "sortAZ": "", + "sortZA": "", + "usersMenu.promoteToUser": "", + "usersMenu.promoteToMember": "", + "usersMenu.promoteToModerator": "", + "usersMenu.promoteToAdmin": "", + "usersMenu.resendUserCreatedMail": "", + "usersMenu.delete": "", + "users.askReallyWantToDeleteUserTitle": "", + "users.askReallyWantToDeleteUser": "", + "users.yesDelete": "", + "users.noDontDelete": "", + "user.ascents": "", + "user.charts": "", + "user.gallery": "", + "ascents": "", + "ALL": "", + "sortAscending": "", + "sortDescending": "", + "topoImage.askReallyWantToDeleteTopoImage": "", + "topoImage.yesDelete": "", + "topoImage.noDontDelete": "", + "topoImage.askReallyWantToDeleteLinePath": "", + "reorderTopoImagesDialogTitle": "", + "reorderTopoImagesDialogItemsName": "", + "reorderLinePathsDialogTitle": "", + "reorderLinePathsDialogItemsName": "", + "editTopoImageBrowserTitle": "", + "addTopoImageBrowserTitle": "", + "orderByGrade": "", + "orderByTimeCreated": "", + "orderDescending": "", + "orderAscending": "", + "allPriorities": "", + "highPriority": "", + "mediumPriority": "", + "lowPriority": "", + "allCrags": "", + "allSectors": "", + "allAreas": "", + "time.now": "", + "time.minute": "", + "time.hour": "", + "time.day": "", + "time.month": "", + "time.year": "", + "January": "", + "February": "", + "March": "", + "April": "", + "May": "", + "June": "", + "July": "", + "August": "", + "September": "", + "October": "", + "November": "", + "December": "", + "leveledGradeDistributionUntil": "", + "leveledGradeDistributionFrom": "", + "copyCoordinatesToClipboard": "", + "openCoordinatesInGoogleMaps": "", + "reorderSectorsDialogTitle": "", + "reorderSectorsDialogItemsName": "", + "sectorFormBrowserTitle": "", + "sector.askReallyWantToDeleteSector": "", + "sector.yesDelete": "", + "sector.noDontDelete": "", + "sector.infos": "", + "sector.rules": "", + "sector.areas": "", + "sector.lines": "", + "sector.ascents": "", + "sector.ranking": "", + "sector.gallery": "", + "sector.comments": "", + "sector.edit": "", + "FIRST_BAR_CHART_BRACKET": "", + "SECOND_BAR_CHART_BRACKET": "", + "scale.scaleForm.confirmDeleteMessage": "", + "scale.scaleForm.acceptConfirmDelete": "", + "region.infos": "", + "region.rules": "", + "region.crags": "", + "region.lines": "", + "region.ascents": "", + "region.ranking": "", + "region.gallery": "", + "region.comments": "", + "region.edit": "", + "top10Ranking": "", + "top50Ranking": "", + "totalCountRanking": "", + "menuPagesListBrowserTitle": "", + "sortNewToOld": "", + "sortOldToNew": "", + "editMenuPageFormBrowserTitle": "", + "menuPageFormBrowserTitle": "", + "menuPages.askReallyWantToDeleteMenuPage": "", + "menuPages.yesDelete": "", + "menuPages.noDontDelete": "", + "menuItemsListBrowserTitle": "", + "reorderMenuItemsTopDialogItemsName": "", + "reorderMenuItemsBottomDialogItemsName": "", + "reorderMenuItemsDialogTitle": "", + "pi-book": "", + "pi-building": "", + "pi-calendar": "", + "pi-camera": "", + "pi-cloud": "", + "pi-envelope": "", + "pi-flag": "", + "pi-globe": "", + "pi-home": "", + "pi-shield": "", + "pi-wallet": "", + "pi-clock": "", + "pi-shopping-bag": "", + "pi-instagram": "", + "pi-youtube": "", + "editMenuItemFormBrowserTitle": "", + "menuItemFormBrowserTitle": "", + "menuItems.askReallyWantToDeleteMenuItem": "", + "menuItems.yesDelete": "", + "menuItems.noDontDelete": "", + "maps.markerList.header": "", + "maps.markerList.closeDescriptionDialog": "", + "maps.mapMarkerForm.ACCESS_POINT": "", + "maps.mapMarkerForm.PARKING": "", + "addLinePathBrowserTitle": "", + "orderByName": "", + "orderByRating": "", + "reorderLinePathsForLineDialogTitle": "", + "reorderLinePathsForLineDialogItemsName": "", + "lineFormBrowserTitle": "", + "videoTitle": "", + "line.askReallyWantToDeleteLine": "", + "line.yesDelete": "", + "line.noDontDelete": "", + "line.infos": "", + "line.ascents": "", + "line.gallery": "", + "line.comments": "", + "line.edit": "", + "history.created_crag": "", + "history.created_sector": "", + "history.created_area": "", + "history.created_line": "", + "history.grading_changed": "", + "history.project_status_changed": "", + "history.projectClimbed": "", + "history.projectStatusChanged": "", + "history.lineGraded": "", + "history.upgrade": "", + "history.downgrade": "", + "gallery.deleteImage": "", + "gallery.addImage": "", + "gallery.askReallyWantToDeleteGalleryImage": "", + "gallery.yesDelete": "", + "gallery.noDontDelete": "", + "gallery.editImage": "", + "reorderCragsDialogTitle": "", + "reorderCragsDialogItemsName": "", + "cragFormBrowserTitle": "", + "crag.askReallyWantToDeleteCrag": "", + "crag.yesDelete": "", + "crag.noDontDelete": "", + "crag.infos": "", + "crag.rules": "", + "crag.sectors": "", + "crag.lines": "", + "crag.ascents": "", + "crag.ranking": "", + "crag.gallery": "", + "crag.comments": "", + "crag.edit": "", + "resetPasswordBrowserTitle": "", + "registerCheckMailboxBrowserTitle": "", + "registerFormTabTitle": "", + "notFoundPageBrowserTitle": "", + "menu.systemCategory": "", + "menu.menuPages": "", + "menu.menus": "", + "menu.users": "", + "menu.scales": "", + "menu.instanceSettings": "", + "menu.accountCategory": "", + "menu.accountDetail": "", + "menu.todos": "", + "menu.account": "", + "menu.changePassword": "", + "menu.logout": "", + "menu.news": "", + "menu.topo": "", + "menu.ascents": "", + "menu.ranking": "", + "menu.gallery": "", + "menu.history": "", + "instanceSettings.instanceSettingsForm.rankingPastWeeksAll": "", + "instanceSettings.instanceSettingsForm.rankingPastWeeksWeek": "", + "instanceSettings.instanceSettingsForm.rankingPastWeeksWeeks": "", + "forgotPasswordCheckMailboxBrowserTitle": "", + "forgotPasswordBrowserTitle": "", + "changePasswordBrowserTitle": "", + "changeEmailTabTitle": "", + "accountFormTabTitle": "", + "accountForm.deleteAccountDialogTitle": "", + "comments.edit": "", + "comments.delete": "", + "postListBrowserTitle": "", + "editPostFormBrowserTitle": "", + "postFormBrowserTitle": "", + "posts.askReallyWantToDeletePost": "", + "posts.yesDelete": "", + "posts.noDontDelete": "", + "batchUploadTopoImageBrowserTitle": "", + "ascent.editAscent": "", + "ascent.deleteAscent": "", + "orderByAscentDate": "", + "ascent.askReallyWantToDeleteAscent": "", + "ascent.yesDelete": "", + "ascent.noDontDelete": "", + "reorderAreasDialogTitle": "", + "reorderAreasDialogItemsName": "", + "areaFormBrowserTitle": "", + "area.askReallyWantToDeleteArea": "", + "area.yesDelete": "", + "area.noDontDelete": "", + "area.infos": "", + "area.topoImages": "", + "area.lines": "", + "area.ascents": "", + "area.gallery": "", + "area.comments": "", + "area.edit": "", + "archive.askAlsoUnArchiveLines": "", + "archive.askAlsoArchiveLines": "", + "archive.yesWithLines": "", + "archive.noWithoutLines": "", + "SIT": "", + "STAND": "", + "CROUCH": "", + "FRENCH": "", + "CANDLE": "", + "LAYDOWN": "", + "MENU_PAGE": "", + "TOPO": "", + "ASCENTS": "", + "RANKING": "", + "NEWS": "", + "GALLERY": "", + "HISTORY": "", + "URL": "", + "BOTTOM": "", + "TOP": "", + "DATE": "", + "YEAR": "" +} diff --git a/client/src/assets/i18n/line/en.json b/client/src/assets/i18n/line/en.json new file mode 100644 index 00000000..7d758b32 --- /dev/null +++ b/client/src/assets/i18n/line/en.json @@ -0,0 +1,120 @@ +{ + "lineList.noLinesFoundEmptyMessage": "", + "lineList.newLineButtonLabel": "", + "lineList.orderByLabel": "", + "lineList.lineType": "", + "lineList.hideArchive": "", + "lineList.showArchive": "", + "lineList.loadMore": "", + "line.reorderLinePathsForLineButtonLabel": "", + "line.ascentCount": "", + "line.userGrade": "", + "line.videoBeta": "", + "line.rating": "", + "line.FA": "", + "lineForm.createLineDescription": "", + "lineForm.lineNameLabel": "", + "lineForm.lineNamePlaceholder": "", + "lineForm.required": "", + "lineForm.maxlength120": "", + "lineForm.lineDescriptionLabel": "", + "lineForm.lineDescriptionPlaceholder": "", + "lineForm.colorLabel": "", + "lineForm.lineTypeLabel": "", + "lineForm.lineScaleLabel": "", + "lineForm.lineGradeLabel": "", + "lineForm.gradePlaceholder": "", + "lineForm.displayUserGradesActive": "", + "lineForm.startingPositionLabel": "", + "lineForm.lineRatingLabel": "", + "lineForm.displayUserRatingsActive": "", + "lineForm.lineVideosLabel": "", + "lineForm.lineVideoTitlePlaceholder": "", + "lineForm.requiredValidationError": "", + "lineForm.lineVideoUrlPlaceholder": "", + "lineForm.invalidHttpUrlValidationError": "", + "lineForm.addVideoButtonLabel": "", + "lineForm.lineFANameLabel": "", + "lineForm.lineFANamePlaceholder": "", + "lineForm.lineFAYearLabel": "", + "lineForm.yearInFutureValidationError": "", + "lineForm.lineFADateLabel": "", + "lineForm.dateInFutureValidationError": "", + "lineForm.showFaDateInsteadOfYear": "", + "lineForm.showFaYearInsteadOfDate": "", + "lineForm.linePropertiesLabel": "", + "lineForm.lineEliminateLabel": "", + "lineForm.lineHighballLabel": "", + "lineForm.lineLowballLabel": "", + "lineForm.lineMorphoLabel": "", + "lineForm.lineNoTopoutLabel": "", + "lineForm.lineBadDropzoneLabel": "", + "lineForm.lineChildFriendlyLabel": "", + "lineForm.lineRoofLabel": "", + "lineForm.lineOverhangLabel": "", + "lineForm.lineVerticalLabel": "", + "lineForm.lineSlabLabel": "", + "lineForm.lineTraverseLabel": "", + "lineForm.lineDihedralLabel": "", + "lineForm.lineCompressionLabel": "", + "lineForm.lineAreteLabel": "", + "lineForm.lineCrackLabel": "", + "lineForm.lineDynoLabel": "", + "lineForm.lineMantleLabel": "", + "lineForm.lineKeyAspectsLabel": "", + "lineForm.lineAthleticLabel": "", + "lineForm.lineTechnicalLabel": "", + "lineForm.lineEnduranceLabel": "", + "lineForm.lineCruxyLabel": "", + "lineForm.linePrimaryHoldTypesLabel": "", + "lineForm.lineJugsLabel": "", + "lineForm.lineSloperLabel": "", + "lineForm.lineCrimpsLabel": "", + "lineForm.linePocketsLabel": "", + "lineForm.linePinchesLabel": "", + "lineForm.lineOptionsLabel": "", + "lineForm.secretLabel": "", + "lineForm.closedLabel": "", + "lineForm.aPublicLineWillSetParentsToPublic": "", + "lineForm.openingAClosedLineWillSetParentsToOpen": "", + "lineForm.closedReasonLabel": "", + "lineForm.closedReasonPlaceholder": "", + "lineForm.createLineButtonLabel": "", + "lineForm.editLineButtonLabel": "", + "lineForm.cancelButtonLabel": "", + "lineForm.deleteLineButtonLabel": "", + "lineForm.editLineTitle": "", + "lineForm.createLineTitle": "", + "lineBoolPropList.eliminate": "", + "lineBoolPropList.traverse": "", + "lineBoolPropList.highball": "", + "lineBoolPropList.lowball": "", + "lineBoolPropList.morpho": "", + "lineBoolPropList.noTopout": "", + "lineBoolPropList.badDropzone": "", + "lineBoolPropList.childFriendly": "", + "lineBoolPropList.roof": "", + "lineBoolPropList.overhang": "", + "lineBoolPropList.vertical": "", + "lineBoolPropList.slab": "", + "lineBoolPropList.athletic": "", + "lineBoolPropList.technical": "", + "lineBoolPropList.endurance": "", + "lineBoolPropList.cruxy": "", + "lineBoolPropList.dyno": "", + "lineBoolPropList.crack": "", + "lineBoolPropList.dihedral": "", + "lineBoolPropList.compression": "", + "lineBoolPropList.arete": "", + "lineBoolPropList.mantle": "", + "lineBoolPropList.jugs": "", + "lineBoolPropList.sloper": "", + "lineBoolPropList.crimps": "", + "lineBoolPropList.pockets": "", + "lineBoolPropList.pinches": "", + "batchLineForm.lineNamePlaceholder": "", + "batchLineForm.required": "", + "batchLineForm.maxlength120": "", + "batchLineForm.gradePlaceholder": "", + "batchLineForm.lineFANamePlaceholder": "" +} diff --git a/client/src/assets/i18n/line/it.json b/client/src/assets/i18n/line/it.json new file mode 100644 index 00000000..7d758b32 --- /dev/null +++ b/client/src/assets/i18n/line/it.json @@ -0,0 +1,120 @@ +{ + "lineList.noLinesFoundEmptyMessage": "", + "lineList.newLineButtonLabel": "", + "lineList.orderByLabel": "", + "lineList.lineType": "", + "lineList.hideArchive": "", + "lineList.showArchive": "", + "lineList.loadMore": "", + "line.reorderLinePathsForLineButtonLabel": "", + "line.ascentCount": "", + "line.userGrade": "", + "line.videoBeta": "", + "line.rating": "", + "line.FA": "", + "lineForm.createLineDescription": "", + "lineForm.lineNameLabel": "", + "lineForm.lineNamePlaceholder": "", + "lineForm.required": "", + "lineForm.maxlength120": "", + "lineForm.lineDescriptionLabel": "", + "lineForm.lineDescriptionPlaceholder": "", + "lineForm.colorLabel": "", + "lineForm.lineTypeLabel": "", + "lineForm.lineScaleLabel": "", + "lineForm.lineGradeLabel": "", + "lineForm.gradePlaceholder": "", + "lineForm.displayUserGradesActive": "", + "lineForm.startingPositionLabel": "", + "lineForm.lineRatingLabel": "", + "lineForm.displayUserRatingsActive": "", + "lineForm.lineVideosLabel": "", + "lineForm.lineVideoTitlePlaceholder": "", + "lineForm.requiredValidationError": "", + "lineForm.lineVideoUrlPlaceholder": "", + "lineForm.invalidHttpUrlValidationError": "", + "lineForm.addVideoButtonLabel": "", + "lineForm.lineFANameLabel": "", + "lineForm.lineFANamePlaceholder": "", + "lineForm.lineFAYearLabel": "", + "lineForm.yearInFutureValidationError": "", + "lineForm.lineFADateLabel": "", + "lineForm.dateInFutureValidationError": "", + "lineForm.showFaDateInsteadOfYear": "", + "lineForm.showFaYearInsteadOfDate": "", + "lineForm.linePropertiesLabel": "", + "lineForm.lineEliminateLabel": "", + "lineForm.lineHighballLabel": "", + "lineForm.lineLowballLabel": "", + "lineForm.lineMorphoLabel": "", + "lineForm.lineNoTopoutLabel": "", + "lineForm.lineBadDropzoneLabel": "", + "lineForm.lineChildFriendlyLabel": "", + "lineForm.lineRoofLabel": "", + "lineForm.lineOverhangLabel": "", + "lineForm.lineVerticalLabel": "", + "lineForm.lineSlabLabel": "", + "lineForm.lineTraverseLabel": "", + "lineForm.lineDihedralLabel": "", + "lineForm.lineCompressionLabel": "", + "lineForm.lineAreteLabel": "", + "lineForm.lineCrackLabel": "", + "lineForm.lineDynoLabel": "", + "lineForm.lineMantleLabel": "", + "lineForm.lineKeyAspectsLabel": "", + "lineForm.lineAthleticLabel": "", + "lineForm.lineTechnicalLabel": "", + "lineForm.lineEnduranceLabel": "", + "lineForm.lineCruxyLabel": "", + "lineForm.linePrimaryHoldTypesLabel": "", + "lineForm.lineJugsLabel": "", + "lineForm.lineSloperLabel": "", + "lineForm.lineCrimpsLabel": "", + "lineForm.linePocketsLabel": "", + "lineForm.linePinchesLabel": "", + "lineForm.lineOptionsLabel": "", + "lineForm.secretLabel": "", + "lineForm.closedLabel": "", + "lineForm.aPublicLineWillSetParentsToPublic": "", + "lineForm.openingAClosedLineWillSetParentsToOpen": "", + "lineForm.closedReasonLabel": "", + "lineForm.closedReasonPlaceholder": "", + "lineForm.createLineButtonLabel": "", + "lineForm.editLineButtonLabel": "", + "lineForm.cancelButtonLabel": "", + "lineForm.deleteLineButtonLabel": "", + "lineForm.editLineTitle": "", + "lineForm.createLineTitle": "", + "lineBoolPropList.eliminate": "", + "lineBoolPropList.traverse": "", + "lineBoolPropList.highball": "", + "lineBoolPropList.lowball": "", + "lineBoolPropList.morpho": "", + "lineBoolPropList.noTopout": "", + "lineBoolPropList.badDropzone": "", + "lineBoolPropList.childFriendly": "", + "lineBoolPropList.roof": "", + "lineBoolPropList.overhang": "", + "lineBoolPropList.vertical": "", + "lineBoolPropList.slab": "", + "lineBoolPropList.athletic": "", + "lineBoolPropList.technical": "", + "lineBoolPropList.endurance": "", + "lineBoolPropList.cruxy": "", + "lineBoolPropList.dyno": "", + "lineBoolPropList.crack": "", + "lineBoolPropList.dihedral": "", + "lineBoolPropList.compression": "", + "lineBoolPropList.arete": "", + "lineBoolPropList.mantle": "", + "lineBoolPropList.jugs": "", + "lineBoolPropList.sloper": "", + "lineBoolPropList.crimps": "", + "lineBoolPropList.pockets": "", + "lineBoolPropList.pinches": "", + "batchLineForm.lineNamePlaceholder": "", + "batchLineForm.required": "", + "batchLineForm.maxlength120": "", + "batchLineForm.gradePlaceholder": "", + "batchLineForm.lineFANamePlaceholder": "" +} diff --git a/client/src/assets/i18n/linePath/en.json b/client/src/assets/i18n/linePath/en.json new file mode 100644 index 00000000..41cc929d --- /dev/null +++ b/client/src/assets/i18n/linePath/en.json @@ -0,0 +1,11 @@ +{ + "linePathForm.addLinePathDescription": "", + "linePathForm.addLinePathTitle": "", + "linePathForm.lineLabel": "", + "linePathForm.linePlaceholder": "", + "linePathForm.required": "", + "linePathForm.linePathLabel": "", + "linePathForm.drawALineWithAtLeastTwoAnchorPoints": "", + "linePathForm.addLinePathButtonLabel": "", + "linePathForm.leaveEditorButtonLabel": "" +} diff --git a/client/src/assets/i18n/linePath/it.json b/client/src/assets/i18n/linePath/it.json new file mode 100644 index 00000000..41cc929d --- /dev/null +++ b/client/src/assets/i18n/linePath/it.json @@ -0,0 +1,11 @@ +{ + "linePathForm.addLinePathDescription": "", + "linePathForm.addLinePathTitle": "", + "linePathForm.lineLabel": "", + "linePathForm.linePlaceholder": "", + "linePathForm.required": "", + "linePathForm.linePathLabel": "", + "linePathForm.drawALineWithAtLeastTwoAnchorPoints": "", + "linePathForm.addLinePathButtonLabel": "", + "linePathForm.leaveEditorButtonLabel": "" +} diff --git a/client/src/assets/i18n/maps/en.json b/client/src/assets/i18n/maps/en.json new file mode 100644 index 00000000..55d7bbcb --- /dev/null +++ b/client/src/assets/i18n/maps/en.json @@ -0,0 +1,26 @@ +{ + "markerList.addMarker": "", + "mapMarkerForm.typeLabel": "", + "mapMarkerForm.typePlaceholder": "", + "mapMarkerForm.required": "", + "mapMarkerForm.coordinatesLabel": "", + "mapMarkerForm.markerNameLabel": "", + "mapMarkerForm.markerNamePlaceholder": "", + "mapMarkerForm.maxlength120": "", + "mapMarkerForm.markerDescriptionLabel": "", + "mapMarkerForm.markerDescriptionPlaceholder": "", + "mapMarkerForm.cancel": "", + "mapMarkerForm.save": "", + "mapMarkerForm.editMarker": "", + "mapMarkerForm.createMarker": "", + "mapItemInfoDialog.navigateToItem": "", + "mapItemInfoDialog.openItem": "", + "mapItemInfoDialog.block": "", + "markerList.TOPO_IMAGE": "", + "markerList.AREA": "", + "markerList.SECTOR": "", + "markerList.CRAG": "", + "markerList.PARKING": "", + "markerList.ACCESS_POINT": "", + "markerList.OTHER": "" +} diff --git a/client/src/assets/i18n/maps/it.json b/client/src/assets/i18n/maps/it.json new file mode 100644 index 00000000..55d7bbcb --- /dev/null +++ b/client/src/assets/i18n/maps/it.json @@ -0,0 +1,26 @@ +{ + "markerList.addMarker": "", + "mapMarkerForm.typeLabel": "", + "mapMarkerForm.typePlaceholder": "", + "mapMarkerForm.required": "", + "mapMarkerForm.coordinatesLabel": "", + "mapMarkerForm.markerNameLabel": "", + "mapMarkerForm.markerNamePlaceholder": "", + "mapMarkerForm.maxlength120": "", + "mapMarkerForm.markerDescriptionLabel": "", + "mapMarkerForm.markerDescriptionPlaceholder": "", + "mapMarkerForm.cancel": "", + "mapMarkerForm.save": "", + "mapMarkerForm.editMarker": "", + "mapMarkerForm.createMarker": "", + "mapItemInfoDialog.navigateToItem": "", + "mapItemInfoDialog.openItem": "", + "mapItemInfoDialog.block": "", + "markerList.TOPO_IMAGE": "", + "markerList.AREA": "", + "markerList.SECTOR": "", + "markerList.CRAG": "", + "markerList.PARKING": "", + "markerList.ACCESS_POINT": "", + "markerList.OTHER": "" +} diff --git a/client/src/assets/i18n/sector/en.json b/client/src/assets/i18n/sector/en.json new file mode 100644 index 00000000..d74bd5e9 --- /dev/null +++ b/client/src/assets/i18n/sector/en.json @@ -0,0 +1,33 @@ +{ + "sectorList.noSectorsFoundEmptyMessage": "", + "sectorList.newSectorButtonLabel": "", + "sectorList.reorderSectorsButtonLabel": "", + "sector.gradeDistribution": "", + "sector.description": "", + "sectorForm.createSectorDescription": "", + "sectorForm.sectorNameLabel": "", + "sectorForm.sectorNamePlaceholder": "", + "sectorForm.required": "", + "sectorForm.maxlength120": "", + "sectorForm.sectorShortDescriptionLabel": "", + "sectorForm.sectorShortDescriptionPlaceholder": "", + "sectorForm.sectorDescriptionLabel": "", + "sectorForm.sectorDescriptionPlaceholder": "", + "sectorForm.sectorRulesLabel": "", + "sectorForm.sectorRulesPlaceholder": "", + "sectorForm.sectorPortraitImageLabel": "", + "sectorForm.mapMarkersLabel": "", + "sectorForm.sectorOptionsLabel": "", + "sectorForm.secretLabel": "", + "sectorForm.closedLabel": "", + "sectorForm.aPublicSectorWillSetParentsToPublic": "", + "sectorForm.openingAClosedSectorWillSetParentsToOpen": "", + "sectorForm.closedReasonLabel": "", + "sectorForm.closedReasonPlaceholder": "", + "sectorForm.createSectorButtonLabel": "", + "sectorForm.editSectorButtonLabel": "", + "sectorForm.cancelButtonLabel": "", + "sectorForm.deleteSectorButtonLabel": "", + "sectorForm.editSectorTitle": "", + "sectorForm.createSectorTitle": "" +} diff --git a/client/src/assets/i18n/sector/it.json b/client/src/assets/i18n/sector/it.json new file mode 100644 index 00000000..d74bd5e9 --- /dev/null +++ b/client/src/assets/i18n/sector/it.json @@ -0,0 +1,33 @@ +{ + "sectorList.noSectorsFoundEmptyMessage": "", + "sectorList.newSectorButtonLabel": "", + "sectorList.reorderSectorsButtonLabel": "", + "sector.gradeDistribution": "", + "sector.description": "", + "sectorForm.createSectorDescription": "", + "sectorForm.sectorNameLabel": "", + "sectorForm.sectorNamePlaceholder": "", + "sectorForm.required": "", + "sectorForm.maxlength120": "", + "sectorForm.sectorShortDescriptionLabel": "", + "sectorForm.sectorShortDescriptionPlaceholder": "", + "sectorForm.sectorDescriptionLabel": "", + "sectorForm.sectorDescriptionPlaceholder": "", + "sectorForm.sectorRulesLabel": "", + "sectorForm.sectorRulesPlaceholder": "", + "sectorForm.sectorPortraitImageLabel": "", + "sectorForm.mapMarkersLabel": "", + "sectorForm.sectorOptionsLabel": "", + "sectorForm.secretLabel": "", + "sectorForm.closedLabel": "", + "sectorForm.aPublicSectorWillSetParentsToPublic": "", + "sectorForm.openingAClosedSectorWillSetParentsToOpen": "", + "sectorForm.closedReasonLabel": "", + "sectorForm.closedReasonPlaceholder": "", + "sectorForm.createSectorButtonLabel": "", + "sectorForm.editSectorButtonLabel": "", + "sectorForm.cancelButtonLabel": "", + "sectorForm.deleteSectorButtonLabel": "", + "sectorForm.editSectorTitle": "", + "sectorForm.createSectorTitle": "" +} diff --git a/client/src/assets/i18n/topoImage/en.json b/client/src/assets/i18n/topoImage/en.json new file mode 100644 index 00000000..977d4af2 --- /dev/null +++ b/client/src/assets/i18n/topoImage/en.json @@ -0,0 +1,45 @@ +{ + "topoImageList.hideArchive": "", + "topoImageList.showArchive": "", + "topoImageList.newTopoImageButtonLabel": "", + "topoImageList.topoImageBatchUploadButtonLabel": "", + "topoImageList.reorderTopoImagesButtonLabel": "", + "topoImageList.noTopoImagesFoundEmptyMessage": "", + "topoImageList.noLineAssignedToTopoImage": "", + "topoImageList.lines": "", + "topoImageList.addLineToImageButtonLabel": "", + "topoImageList.reorderLinePathsButtonLabel": "", + "topoImageList.editTopoImageButtonLabel": "", + "topoImageForm.addTopoImageDescription": "", + "topoImageForm.editTopoImageDescription": "", + "topoImageForm.required": "", + "topoImageForm.topoImageTitleLabel": "", + "topoImageForm.topoImageTitlePlaceholder": "", + "topoImageForm.maxlength120": "", + "topoImageForm.topoImageDescriptionLabel": "", + "topoImageForm.topoImageDescriptionPlaceholder": "", + "topoImageForm.coordinatesLabel": "", + "topoImageForm.addTopoImageButtonLabel": "", + "topoImageForm.cancelButtonLabel": "", + "topoImageForm.addTopoImageTitle": "", + "topoImageForm.editTopoImageTitle": "", + "batchUploadForm.batchImageUploadDescription": "", + "batchUploadForm.images": "", + "batchUploadForm.lines": "", + "batchUploadForm.linePaths": "", + "batchUploadForm.min-length-1-image-validation-error": "", + "batchUploadForm.cancel": "", + "batchUploadForm.Next": "", + "batchUploadForm.lineTypeLabel": "", + "batchUploadForm.lineScaleLabel": "", + "batchUploadForm.lineFADateLabel": "", + "batchUploadForm.dateInFutureValidationError": "", + "batchUploadForm.linesLabel": "", + "batchUploadForm.addLine": "", + "batchUploadForm.displayUserGradesActive": "", + "batchUploadForm.Back": "", + "batchUploadForm.saveLinesAndTopoImagesAndNext": "", + "batchUploadForm.topoImageLabel": "", + "batchUploadForm.topoImagePlaceholder": "", + "batchUploadForm.batchImageUploadTitle": "" +} diff --git a/client/src/assets/i18n/topoImage/it.json b/client/src/assets/i18n/topoImage/it.json new file mode 100644 index 00000000..977d4af2 --- /dev/null +++ b/client/src/assets/i18n/topoImage/it.json @@ -0,0 +1,45 @@ +{ + "topoImageList.hideArchive": "", + "topoImageList.showArchive": "", + "topoImageList.newTopoImageButtonLabel": "", + "topoImageList.topoImageBatchUploadButtonLabel": "", + "topoImageList.reorderTopoImagesButtonLabel": "", + "topoImageList.noTopoImagesFoundEmptyMessage": "", + "topoImageList.noLineAssignedToTopoImage": "", + "topoImageList.lines": "", + "topoImageList.addLineToImageButtonLabel": "", + "topoImageList.reorderLinePathsButtonLabel": "", + "topoImageList.editTopoImageButtonLabel": "", + "topoImageForm.addTopoImageDescription": "", + "topoImageForm.editTopoImageDescription": "", + "topoImageForm.required": "", + "topoImageForm.topoImageTitleLabel": "", + "topoImageForm.topoImageTitlePlaceholder": "", + "topoImageForm.maxlength120": "", + "topoImageForm.topoImageDescriptionLabel": "", + "topoImageForm.topoImageDescriptionPlaceholder": "", + "topoImageForm.coordinatesLabel": "", + "topoImageForm.addTopoImageButtonLabel": "", + "topoImageForm.cancelButtonLabel": "", + "topoImageForm.addTopoImageTitle": "", + "topoImageForm.editTopoImageTitle": "", + "batchUploadForm.batchImageUploadDescription": "", + "batchUploadForm.images": "", + "batchUploadForm.lines": "", + "batchUploadForm.linePaths": "", + "batchUploadForm.min-length-1-image-validation-error": "", + "batchUploadForm.cancel": "", + "batchUploadForm.Next": "", + "batchUploadForm.lineTypeLabel": "", + "batchUploadForm.lineScaleLabel": "", + "batchUploadForm.lineFADateLabel": "", + "batchUploadForm.dateInFutureValidationError": "", + "batchUploadForm.linesLabel": "", + "batchUploadForm.addLine": "", + "batchUploadForm.displayUserGradesActive": "", + "batchUploadForm.Back": "", + "batchUploadForm.saveLinesAndTopoImagesAndNext": "", + "batchUploadForm.topoImageLabel": "", + "batchUploadForm.topoImagePlaceholder": "", + "batchUploadForm.batchImageUploadTitle": "" +} diff --git a/server/src/helpers/user_helpers.py b/server/src/helpers/user_helpers.py index 062331ef..c2122205 100644 --- a/server/src/helpers/user_helpers.py +++ b/server/src/helpers/user_helpers.py @@ -1,11 +1,12 @@ from extensions import db from models.account_settings import AccountSettings +from models.instance_settings import InstanceSettings from models.user import User from util.email import send_create_user_email from util.password_util import generate_password -def create_user(user_data, created_by=None, skip_account_settings=False) -> User: +def create_user(user_data, created_by=None) -> User: """ Creates a new user. @param user_data: User data as parsed from request. @@ -20,17 +21,16 @@ def create_user(user_data, created_by=None, skip_account_settings=False) -> User new_user.firstname = user_data["firstname"] new_user.lastname = user_data["lastname"] new_user.email = user_data["email"].lower() - new_user.language = "de" new_user.password = User.generate_hash(password) if created_by: new_user.created_by_id = created_by.id db.session.add(new_user) db.session.flush() # ensure ID available - if not skip_account_settings: - account_settings = AccountSettings() - account_settings.user_id = new_user.id - db.session.add(account_settings) + account_settings = AccountSettings() + account_settings.user_id = new_user.id + account_settings.language = InstanceSettings.return_it().language + db.session.add(account_settings) db.session.commit() send_create_user_email(password, new_user) diff --git a/server/src/i18n/change_email_address_mail.py b/server/src/i18n/change_email_address_mail.py index 80f9b29a..2a60fa96 100644 --- a/server/src/i18n/change_email_address_mail.py +++ b/server/src/i18n/change_email_address_mail.py @@ -32,4 +32,18 @@ "greetings": "Your LocalCrag team", "subject": "Change of your LocalCrag e-mail address", }, + "it": { + "title": "", + "intro_text": "", + "LOCALCRAG": "", + "hello": "", + "text_1": "", + "text_2": "", + "change_email": "", + "copyright": "", + "hint": "", + "thanks": "", + "greetings": "", + "subject": "", + }, } diff --git a/server/src/i18n/comment_created_mail.py b/server/src/i18n/comment_created_mail.py index 34935239..2a68b467 100644 --- a/server/src/i18n/comment_created_mail.py +++ b/server/src/i18n/comment_created_mail.py @@ -24,4 +24,16 @@ "subject": "New comment", "copyright": "© LocalCrag. All rights reserved.", }, + "it": { + "title": "", + "LOCALCRAG": "", + "hello": "", + "new_comment_text": "", + "view": "", + "thanks": "", + "greetings": "", + "hint": "", + "subject": "", + "copyright": "", + }, } diff --git a/server/src/i18n/comment_reply_mail.py b/server/src/i18n/comment_reply_mail.py index 2b023d95..f75a4511 100644 --- a/server/src/i18n/comment_reply_mail.py +++ b/server/src/i18n/comment_reply_mail.py @@ -24,4 +24,16 @@ "subject": "New reply to your comment", "copyright": "© LocalCrag. All rights reserved.", }, + "it": { + "title": "", + "LOCALCRAG": "", + "hello": "", + "comment_reply_text": "", + "view": "", + "thanks": "", + "greetings": "", + "hint": "", + "subject": "", + "copyright": "", + }, } diff --git a/server/src/i18n/create_user_mail.py b/server/src/i18n/create_user_mail.py index 0949145a..b23746d7 100644 --- a/server/src/i18n/create_user_mail.py +++ b/server/src/i18n/create_user_mail.py @@ -35,4 +35,20 @@ "greetings": "Your LocalCrag team", "subject": "Your LocalCrag account", }, + "it": { + "title": "", + "intro_text": "", + "LOCALCRAG": "", + "hello": "", + "text_1": "", + "text_2": "", + "email_title": "", + "password_title": "", + "activate_account": "", + "copyright": "", + "hint": "", + "thanks": "", + "greetings": "", + "subject": "", + }, } diff --git a/server/src/i18n/project_climbed_mail.py b/server/src/i18n/project_climbed_mail.py index ee717d6e..4c7d4b8f 100644 --- a/server/src/i18n/project_climbed_mail.py +++ b/server/src/i18n/project_climbed_mail.py @@ -30,4 +30,19 @@ "subject": "A project has been climbed!", "reply_to": "To reply, mail", }, + "it": { + "title": "", + "intro": "", + "LOCALCRAG": "", + "hello": "", + "message": "", + "view_user": "", + "view_project": "", + "copyright": "", + "hint": "", + "thanks": "", + "greetings": "", + "subject": "", + "reply_to": "", + }, } diff --git a/server/src/i18n/reset_password_mail.py b/server/src/i18n/reset_password_mail.py index 090d2e0f..224cd381 100644 --- a/server/src/i18n/reset_password_mail.py +++ b/server/src/i18n/reset_password_mail.py @@ -31,4 +31,18 @@ "greetings": "Your LocalCrag team", "subject": "Reset LocalCrag password", }, + "it": { + "title": "", + "intro_text": "", + "LOCALCRAG": "", + "hello": "", + "text_1": "", + "text_2": "", + "reset_password": "", + "copyright": "", + "hint": "", + "thanks": "", + "greetings": "", + "subject": "", + }, } diff --git a/server/src/i18n/user_registered_mail.py b/server/src/i18n/user_registered_mail.py index af542966..219d6cea 100644 --- a/server/src/i18n/user_registered_mail.py +++ b/server/src/i18n/user_registered_mail.py @@ -27,4 +27,17 @@ "greetings": "Your LocalCrag team", "subject": "New user", }, + "it": { + "title": "", + "new_user_text": "", + "LOCALCRAG": "", + "user_count_text": "", + "hello": "", + "view_user": "", + "copyright": "", + "hint": "", + "thanks": "", + "greetings": "", + "subject": "", + }, } diff --git a/server/src/marshmallow_schemas/account_settings_schema.py b/server/src/marshmallow_schemas/account_settings_schema.py index 3a0e1374..7cbea15c 100644 --- a/server/src/marshmallow_schemas/account_settings_schema.py +++ b/server/src/marshmallow_schemas/account_settings_schema.py @@ -5,6 +5,7 @@ class AccountSettingsSchema(ma.SQLAlchemySchema): commentReplyMailsEnabled = fields.Boolean(attribute="comment_reply_mails_enabled") + language = fields.String(attribute="language") account_settings_schema = AccountSettingsSchema() diff --git a/server/src/marshmallow_schemas/instance_settings_schema.py b/server/src/marshmallow_schemas/instance_settings_schema.py index d548f677..a7e87848 100644 --- a/server/src/marshmallow_schemas/instance_settings_schema.py +++ b/server/src/marshmallow_schemas/instance_settings_schema.py @@ -19,6 +19,7 @@ class InstanceSettingsSchema(ma.SQLAlchemySchema): arrowHighlightColor = fields.String(attribute="arrow_highlight_color") arrowHighlightTextColor = fields.String(attribute="arrow_highlight_text_color") barChartColor = fields.String(attribute="bar_chart_color") + language = fields.String(attribute="language") matomoTrackerUrl = fields.String(attribute="matomo_tracker_url") matomoSiteId = fields.String(attribute="matomo_site_id") maptilerApiKey = fields.String(attribute="maptiler_api_key") diff --git a/server/src/marshmallow_schemas/user_schema.py b/server/src/marshmallow_schemas/user_schema.py index 6d4dc574..ccb1561e 100644 --- a/server/src/marshmallow_schemas/user_schema.py +++ b/server/src/marshmallow_schemas/user_schema.py @@ -18,7 +18,7 @@ class UserSchema(BaseEntitySchema): firstname = fields.String() lastname = fields.String() slug = fields.String() - language = fields.String() + accountLanguage = fields.String(attribute="account_settings.language") superadmin = fields.Boolean() admin = fields.Boolean() moderator = fields.Boolean() diff --git a/server/src/migrations/env.py b/server/src/migrations/env.py index 7c500a09..786cdfa1 100644 --- a/server/src/migrations/env.py +++ b/server/src/migrations/env.py @@ -6,6 +6,8 @@ from alembic import context from sqlalchemy import engine_from_config, pool +from migrations.util_scripts.database_setup import database_setup + # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config @@ -92,3 +94,4 @@ def process_revision_directives(_context, _revision, directives): run_migrations_offline() else: run_migrations_online() +database_setup() diff --git a/server/src/migrations/util_scripts/add_superadmin.py b/server/src/migrations/util_scripts/add_superadmin.py index c70ec773..fe25a9ef 100644 --- a/server/src/migrations/util_scripts/add_superadmin.py +++ b/server/src/migrations/util_scripts/add_superadmin.py @@ -25,7 +25,7 @@ def add_superadmin(): "lastname": current_app.config["SUPERADMIN_LASTNAME"], "email": current_app.config["SUPERADMIN_EMAIL"], } - user = create_user(user_data, skip_account_settings=True) + user = create_user(user_data) user.superadmin = True user.admin = True user.moderator = True diff --git a/server/src/migrations/util_scripts/database_setup.py b/server/src/migrations/util_scripts/database_setup.py new file mode 100644 index 00000000..fcb98fd9 --- /dev/null +++ b/server/src/migrations/util_scripts/database_setup.py @@ -0,0 +1,18 @@ +from migrations.util_scripts.add_backup_user import add_backup_user +from migrations.util_scripts.add_initial_data import add_initial_data +from migrations.util_scripts.add_initial_instance_settings import ( + add_initial_instance_settings, +) +from migrations.util_scripts.add_scales import add_scales +from migrations.util_scripts.add_superadmin import add_superadmin + + +def database_setup(): + """ + Add initial data to the database. + """ + add_initial_instance_settings() + add_superadmin() + add_initial_data() + add_scales() + add_backup_user() diff --git a/server/src/migrations/versions/28f64bea4755_database_setup.py b/server/src/migrations/versions/28f64bea4755_database_setup.py index 406e0b52..365d6aa3 100644 --- a/server/src/migrations/versions/28f64bea4755_database_setup.py +++ b/server/src/migrations/versions/28f64bea4755_database_setup.py @@ -6,14 +6,6 @@ """ -from migrations.util_scripts.add_backup_user import add_backup_user -from migrations.util_scripts.add_initial_data import add_initial_data -from migrations.util_scripts.add_initial_instance_settings import ( - add_initial_instance_settings, -) -from migrations.util_scripts.add_scales import add_scales -from migrations.util_scripts.add_superadmin import add_superadmin - # revision identifiers, used by Alembic. revision = "28f64bea4755" down_revision = "43fac44d1505" @@ -22,11 +14,13 @@ def upgrade(): - add_superadmin() - add_initial_data() - add_scales() - add_initial_instance_settings() - add_backup_user() + pass + # Setup scripts have been moved to env.py to ensure they run only after all migrations + # add_superadmin() + # add_initial_data() + # add_scales() + # add_initial_instance_settings() + # add_backup_user() def downgrade(): diff --git a/server/src/migrations/versions/b318163f4e81_add_language_columns.py b/server/src/migrations/versions/b318163f4e81_add_language_columns.py new file mode 100644 index 00000000..67f62282 --- /dev/null +++ b/server/src/migrations/versions/b318163f4e81_add_language_columns.py @@ -0,0 +1,32 @@ +"""add language to instance_settings and account_settings, backfill existing to 'de' + +Revision ID: aa_add_language_columns +Revises: +Create Date: 2025-12-07 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "b318163f4e81" +down_revision = "14dc45b4214e" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column("instance_settings", sa.Column("language", sa.String(length=10), nullable=False, server_default="en")) + op.add_column("account_settings", sa.Column("language", sa.String(length=10), nullable=False, server_default="en")) + + # set existing rows to 'de' to keep current behavior + op.execute("UPDATE instance_settings SET language='de' WHERE language IS NULL OR language='' ") + op.execute("UPDATE account_settings SET language='de' WHERE language IS NULL OR language='' ") + + op.drop_column("users", "language") + + +def downgrade(): + op.drop_column("account_settings", "language") + op.drop_column("instance_settings", "language") diff --git a/server/src/models/account_settings.py b/server/src/models/account_settings.py index 9eeccd82..b7c85f2a 100644 --- a/server/src/models/account_settings.py +++ b/server/src/models/account_settings.py @@ -14,5 +14,7 @@ class AccountSettings(db.Model): ) # Whether the user wants to receive emails when someone replies to their comment comment_reply_mails_enabled = db.Column(db.Boolean, nullable=False, default=True, server_default="true") + # Preferred language for the account + language = db.Column(db.String(10), nullable=False, default="en", server_default="en") user = db.relationship("User", back_populates="account_settings") diff --git a/server/src/models/instance_settings.py b/server/src/models/instance_settings.py index 4fb42a7c..716b9249 100644 --- a/server/src/models/instance_settings.py +++ b/server/src/models/instance_settings.py @@ -32,6 +32,7 @@ class InstanceSettings(db.Model): arrow_highlight_color = db.Column(db.String(7), nullable=False, server_default="#FF0000") arrow_highlight_text_color = db.Column(db.String(7), nullable=False, server_default="#FFFFFF") bar_chart_color = db.Column(db.String(30), nullable=False, server_default="rgb(213, 30, 38)") + language = db.Column(db.String(10), nullable=False, default="en", server_default="en") matomo_tracker_url = db.Column(db.String(120), nullable=True) matomo_site_id = db.Column(db.String(120), nullable=True) maptiler_api_key = db.Column(db.String(120), nullable=True) diff --git a/server/src/models/user.py b/server/src/models/user.py index 79454f94..8873f518 100644 --- a/server/src/models/user.py +++ b/server/src/models/user.py @@ -1,7 +1,7 @@ import secrets from base64 import b64decode, b64encode from hashlib import pbkdf2_hmac -from typing import Self +from typing import Optional, Self from sqlalchemy.dialects.postgresql import UUID @@ -33,7 +33,6 @@ class User(HasSlug, IsSearchable, BaseEntity): activated_at = db.Column(db.DateTime(), nullable=True) reset_password_hash = db.Column(db.String(120), nullable=True, default=None) reset_password_hash_created = db.Column(db.DateTime(timezone=True), default=None, nullable=True) - language = db.Column(db.String, nullable=False, server_default="de") avatar_id = db.Column(UUID(), db.ForeignKey("files.id", name="fk_user_avatar_id"), nullable=True) avatar = db.relationship("File", foreign_keys=avatar_id) superadmin = db.Column(db.Boolean, nullable=False, default=False, server_default="0") @@ -75,15 +74,15 @@ def verify_hash(password, password_hash): return secrets.compare_digest(new_hash, known_hash) @classmethod - def find_by_reset_password_hash(cls, password_hash) -> Self | None: + def find_by_reset_password_hash(cls, password_hash) -> Optional[Self]: return cls.query.filter_by(reset_password_hash=password_hash).first() @classmethod - def find_by_new_email_hash(cls, new_email_hash) -> Self | None: + def find_by_new_email_hash(cls, new_email_hash) -> Optional[Self]: return cls.query.filter_by(new_email_hash=new_email_hash).first() @classmethod - def find_by_email(cls, email) -> Self | None: + def find_by_email(cls, email) -> Optional[Self]: user = cls.query.filter_by(email=email).first() return user diff --git a/server/src/resources/account_resources.py b/server/src/resources/account_resources.py index fefc9fe8..131a1f3d 100644 --- a/server/src/resources/account_resources.py +++ b/server/src/resources/account_resources.py @@ -43,6 +43,7 @@ def put(self): data = parser.parse(account_settings_args) settings = user.account_settings settings.comment_reply_mails_enabled = data["commentReplyMailsEnabled"] + settings.language = data["language"] db.session.add(settings) db.session.commit() return account_settings_schema.dump(settings), 200 diff --git a/server/src/resources/instance_settings_resources.py b/server/src/resources/instance_settings_resources.py index 618ad61d..037ce85d 100644 --- a/server/src/resources/instance_settings_resources.py +++ b/server/src/resources/instance_settings_resources.py @@ -50,6 +50,7 @@ def put(self): instance_settings.arrow_highlight_color = instance_settings_data["arrowHighlightColor"] instance_settings.arrow_highlight_text_color = instance_settings_data["arrowHighlightTextColor"] instance_settings.bar_chart_color = instance_settings_data["barChartColor"] + instance_settings.language = instance_settings_data["language"] instance_settings.matomo_tracker_url = instance_settings_data["matomoTrackerUrl"] instance_settings.matomo_site_id = instance_settings_data["matomoSiteId"] instance_settings.maptiler_api_key = instance_settings_data["maptilerApiKey"] diff --git a/server/src/util/email.py b/server/src/util/email.py index 117fefd1..a79dbfea 100644 --- a/server/src/util/email.py +++ b/server/src/util/email.py @@ -77,7 +77,7 @@ def prepare_message(user: User, i18n_dict_source): :param i18n_dict_source: Translation source dict. :return: Tuple of message object and translation dict. """ - i18n_keyword_arg_dict = build_i18n_keyword_arg_dict(user.language, i18n_dict_source) + i18n_keyword_arg_dict = build_i18n_keyword_arg_dict(user.account_settings.language, i18n_dict_source) msg = MIMEMultipart("alternative") msg["Subject"] = i18n_keyword_arg_dict["i18n_subject"] msg["From"] = current_app.config["SYSTEM_EMAIL"] diff --git a/server/src/util/validators.py b/server/src/util/validators.py index 688bbb40..d87e9654 100644 --- a/server/src/util/validators.py +++ b/server/src/util/validators.py @@ -3,6 +3,16 @@ from models.enums.line_type_enum import LineTypeEnum from models.scale import Scale +ALLOWED_LANGUAGES = {"de", "en", "it"} + + +def validate_language(value: str): + """ + Raises a ValidationError if the language is not one of the allowed values. + """ + if value not in ALLOWED_LANGUAGES: + raise ValidationError(f"Invalid language '{value}'. Allowed: {', '.join(sorted(ALLOWED_LANGUAGES))}") + def cross_validate_grade(grade_value, grade_scale, line_type): """ diff --git a/server/src/webargs_schemas/account_settings_args.py b/server/src/webargs_schemas/account_settings_args.py index ccfa3c8b..6227d1da 100644 --- a/server/src/webargs_schemas/account_settings_args.py +++ b/server/src/webargs_schemas/account_settings_args.py @@ -1,5 +1,8 @@ from webargs import fields +from util.validators import validate_language + account_settings_args = { "commentReplyMailsEnabled": fields.Boolean(required=True), + "language": fields.Str(required=True, validate=validate_language), } diff --git a/server/src/webargs_schemas/instance_settings_args.py b/server/src/webargs_schemas/instance_settings_args.py index 82647616..ec4d550a 100644 --- a/server/src/webargs_schemas/instance_settings_args.py +++ b/server/src/webargs_schemas/instance_settings_args.py @@ -3,6 +3,7 @@ from models.enums.fa_default_format_enum import FaDefaultFormatEnum from models.enums.starting_position_enum import StartingPositionEnum +from util.validators import validate_language instance_settings_args = { "instanceName": fields.Str(required=True, validate=validate.Length(max=120)), @@ -17,6 +18,7 @@ "arrowHighlightColor": fields.Str(required=True), "arrowHighlightTextColor": fields.Str(required=True), "barChartColor": fields.Str(required=True), + "language": fields.Str(required=True, validate=validate_language), "matomoTrackerUrl": fields.Str(required=True, allow_none=True), "matomoSiteId": fields.Str(required=True, allow_none=True), "maptilerApiKey": fields.Str(required=True, allow_none=True), diff --git a/server/tests/test_account_settings_resources.py b/server/tests/test_account_settings_resources.py index 0150c21c..1487b4bf 100644 --- a/server/tests/test_account_settings_resources.py +++ b/server/tests/test_account_settings_resources.py @@ -5,19 +5,22 @@ def test_get_account_settings(client, member_token): rv = client.get("/api/users/account/settings", token=member_token) assert rv.status_code == 200, rv.text assert rv.json["commentReplyMailsEnabled"] is True + assert rv.json["language"] in ("de", "en", "it") def test_update_account_settings(client, member_token): rv = client.put( "/api/users/account/settings", token=member_token, - json={"commentReplyMailsEnabled": False}, + json={"commentReplyMailsEnabled": False, "language": "it"}, ) assert rv.status_code == 200, rv.text assert rv.json["commentReplyMailsEnabled"] is False + assert rv.json["language"] == "it" # Read again rv = client.get("/api/users/account/settings", token=member_token) assert rv.json["commentReplyMailsEnabled"] is False + assert rv.json["language"] == "it" def test_comment_reply_email_sent_when_enabled(client, admin_token, member_token, smtp_mock): @@ -51,7 +54,7 @@ def test_comment_reply_email_not_sent_when_disabled(client, admin_token, member_ rv = client.put( "/api/users/account/settings", token=member_token, - json={"commentReplyMailsEnabled": False}, + json={"commentReplyMailsEnabled": False, "language": "de"}, ) assert rv.status_code == 200, rv.text line_id = Line.get_id_by_slug("treppe") @@ -76,3 +79,12 @@ def test_comment_reply_email_not_sent_when_disabled(client, admin_token, member_ ) assert rv.status_code == 201, rv.text assert smtp_mock.return_value.__enter__.return_value.sendmail.call_count == 1 # Only admin notification + + +def test_update_account_settings_invalid_language(client, member_token): + rv = client.put( + "/api/users/account/settings", + token=member_token, + json={"commentReplyMailsEnabled": True, "language": "fr"}, + ) + assert rv.status_code == 400, rv.text diff --git a/server/tests/test_auth_resources.py b/server/tests/test_auth_resources.py index 85a33889..3d3b82f2 100644 --- a/server/tests/test_auth_resources.py +++ b/server/tests/test_auth_resources.py @@ -23,7 +23,7 @@ def test_successful_login(client): assert res["user"]["firstname"] == "admin" assert res["user"]["lastname"] == "admin" assert isinstance(res["user"]["id"], str) - assert res["user"]["language"] == "de" + assert res["user"]["accountLanguage"] == "en" assert res["user"]["timeCreated"] is not None assert res["user"]["avatar"] is None @@ -172,7 +172,7 @@ def test_reset_password_success(client): assert res["accessToken"] != res["refreshToken"] assert res["user"]["email"] == user.email assert res["user"]["id"] == str(user.id) - assert res["user"]["language"] == user.language + assert res["user"]["accountLanguage"] == user.account_settings.language assert res["user"]["timeCreated"] is not None assert res["user"]["timeUpdated"] is not None assert res["user"]["avatar"] is None diff --git a/server/tests/test_database_setup.py b/server/tests/test_database_setup.py index 1fa8e558..b37c80de 100644 --- a/server/tests/test_database_setup.py +++ b/server/tests/test_database_setup.py @@ -1,9 +1,8 @@ -from importlib import import_module - import pytest from flask import current_app from extensions import db +from migrations.util_scripts.database_setup import database_setup from models.enums.menu_item_position_enum import MenuItemPositionEnum from models.enums.menu_item_type_enum import MenuItemTypeEnum from models.instance_settings import InstanceSettings @@ -12,16 +11,9 @@ from models.region import Region from models.user import User -# We test the utils that are called by the migration script directly -# Previously the database setup was done by a dedicated script run after migrations -_migration = import_module( - "migrations.versions.28f64bea4755_database_setup" -) # Needed for importing module starting with a number -upgrade = _migration.upgrade - def test_database_setup(client, clean_db, smtp_mock): - upgrade() + database_setup() assert smtp_mock.return_value.__enter__.return_value.login.call_count == 1 assert smtp_mock.return_value.__enter__.return_value.sendmail.call_count == 1 assert smtp_mock.return_value.__enter__.return_value.quit.call_count == 1 @@ -85,4 +77,4 @@ def test_database_setup_with_missing_env_vars(client, clean_db): current_app.config["SUPERADMIN_LASTNAME"] = None current_app.config["SUPERADMIN_EMAIL"] = None with pytest.raises(ValueError): - upgrade() + database_setup() diff --git a/server/tests/test_instance_settings_resources.py b/server/tests/test_instance_settings_resources.py index d4a17c29..5143c143 100644 --- a/server/tests/test_instance_settings_resources.py +++ b/server/tests/test_instance_settings_resources.py @@ -66,6 +66,7 @@ def test_successful_edit_instance_settings(client, moderator_token): "defaultStartingPosition": StartingPositionEnum.SIT.value, "rankingPastWeeks": 12, "disableFAInAscents": True, + "language": "de", } rv = client.put("/api/instance-settings", token=moderator_token, json=post_data) assert rv.status_code == 200 @@ -94,6 +95,7 @@ def test_successful_edit_instance_settings(client, moderator_token): assert res["defaultStartingPosition"] == StartingPositionEnum.SIT.value assert res["rankingPastWeeks"] == 12 assert res["disableFAInAscents"] is True + assert res["language"] == "de" def test_successful_change_skipped_hierarchical_layers(client, moderator_token): @@ -127,6 +129,7 @@ def test_successful_change_skipped_hierarchical_layers(client, moderator_token): "defaultStartingPosition": StartingPositionEnum.STAND.value, "rankingPastWeeks": None, "disableFAInAscents": False, + "language": "en", } rv = client.put("/api/instance-settings", token=moderator_token, json=post_data) assert rv.status_code == 200 @@ -163,6 +166,7 @@ def test_error_conflict_skipped_hierarchical_layers(client, moderator_token): "defaultStartingPosition": StartingPositionEnum.STAND.value, "rankingPastWeeks": 4, "disableFAInAscents": True, + "language": "en", } rv = client.put("/api/instance-settings", token=moderator_token, json=post_data) assert rv.status_code == 409, rv.json diff --git a/server/tests/test_user_resources.py b/server/tests/test_user_resources.py index 7f9c2694..d54ffd91 100644 --- a/server/tests/test_user_resources.py +++ b/server/tests/test_user_resources.py @@ -19,7 +19,7 @@ def test_successful_get_user(client, member_token): assert res["firstname"] == member.firstname assert res["lastname"] == member.lastname assert res["email"] == member.email - assert res["language"] == member.language + assert res["accountLanguage"] == member.account_settings.language assert res["activated"] == member.activated assert res["admin"] == member.admin assert res["moderator"] == member.moderator @@ -40,7 +40,7 @@ def test_successful_get_users(client, admin_token): assert isinstance(user["firstname"], str) assert isinstance(user["lastname"], str) assert isinstance(user["email"], str) - assert isinstance(user["language"], str) + assert isinstance(user["accountLanguage"], str) assert isinstance(user["activated"], bool) assert isinstance(user["admin"], bool) assert isinstance(user["moderator"], bool) @@ -84,7 +84,7 @@ def test_successful_register_user(client, member_token, smtp_mock): assert res["lastname"] == "Test" assert res["email"] == "felix.engelmann@fengelmann.de" # expect it to be changed to lowercase assert not res["activated"] - assert res["language"] == "de" + assert res["accountLanguage"] == "en" assert res["avatar"] is None assert smtp_mock.return_value.__enter__.return_value.login.call_count == 2 assert smtp_mock.return_value.__enter__.return_value.sendmail.call_count == 2 @@ -136,7 +136,7 @@ def test_update_user(client, admin_token): rv = client.put("/api/users/account", token=admin_token, json=data) assert rv.status_code == 200, rv.text res = rv.json - assert res["language"] == "de" + assert res["accountLanguage"] == "en" assert res["firstname"] == "Thorsten" assert res["lastname"] == "Test" assert res["avatar"]["id"] == str(any_file.id) @@ -156,7 +156,7 @@ def test_update_user_different_email(client, mocker, member_token): rv = client.put("/api/users/account", token=member_token, json=data) assert rv.status_code == 200 res = rv.json - assert res["language"] == "de" + assert res["accountLanguage"] == "en" assert res["firstname"] == "Thorsten" assert res["lastname"] == "Test" assert res["avatar"] is None @@ -187,7 +187,7 @@ def test_change_email(client): assert res["accessToken"] != res["refreshToken"] assert res["user"]["email"] == "masteradmin@localcrag.invalid.org" assert isinstance(res["user"]["id"], str) - assert res["user"]["language"] == "de" + assert res["user"]["accountLanguage"] == "en" assert res["user"]["timeCreated"] is not None assert res["user"]["timeUpdated"] is not None assert res["user"]["avatar"] is None