From 890257c77ec13df050c97425400f817edc1c107d Mon Sep 17 00:00:00 2001 From: Nico Haider Date: Wed, 27 Aug 2025 17:05:51 +0200 Subject: [PATCH] feat(core): implement translation --- package-lock.json | 28 ++++ package.json | 2 + public/i18n/de.json | 6 + public/i18n/en.json | 6 + src/app/app.config.ts | 17 ++- src/app/app.ts | 6 +- .../core/components/background/background.ts | 8 +- .../navigation-bar/navigation-bar.html | 8 +- .../navigation-bar/navigation-bar.ts | 18 ++- src/app/core/config/translation-init.ts | 22 +++ src/app/core/config/translations.ts | 130 +++++++++++++++++- src/index.html | 2 +- 12 files changed, 234 insertions(+), 19 deletions(-) create mode 100644 public/i18n/de.json create mode 100644 public/i18n/en.json create mode 100644 src/app/core/config/translation-init.ts diff --git a/package-lock.json b/package-lock.json index 3c3f665..12869dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,8 @@ "@fortawesome/free-brands-svg-icons": "^7.0.0", "@fortawesome/free-regular-svg-icons": "^7.0.0", "@fortawesome/free-solid-svg-icons": "^7.0.0", + "@ngx-translate/core": "^17.0.0", + "@ngx-translate/http-loader": "^17.0.0", "@primeuix/themes": "^1.2.3", "@tailwindcss/postcss": "^4.1.12", "postcss": "^8.5.6", @@ -2600,6 +2602,32 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@ngx-translate/core": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-17.0.0.tgz", + "integrity": "sha512-Rft2D5ns2pq4orLZjEtx1uhNuEBerUdpFUG1IcqtGuipj6SavgB8SkxtNQALNDA+EVlvsNCCjC2ewZVtUeN6rg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16", + "@angular/core": ">=16" + } + }, + "node_modules/@ngx-translate/http-loader": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-17.0.0.tgz", + "integrity": "sha512-hgS8sa0ARjH9ll3PhkLTufeVXNI2DNR2uFKDhBgq13siUXzzVr/a31M6zgecrtwbA34iaBV01hsTMbMS8V7iIw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16", + "@angular/core": ">=16" + } + }, "node_modules/@npmcli/agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", diff --git a/package.json b/package.json index 7e73efd..7fc5fbf 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "@fortawesome/free-brands-svg-icons": "^7.0.0", "@fortawesome/free-regular-svg-icons": "^7.0.0", "@fortawesome/free-solid-svg-icons": "^7.0.0", + "@ngx-translate/core": "^17.0.0", + "@ngx-translate/http-loader": "^17.0.0", "@primeuix/themes": "^1.2.3", "@tailwindcss/postcss": "^4.1.12", "postcss": "^8.5.6", diff --git a/public/i18n/de.json b/public/i18n/de.json new file mode 100644 index 0000000..b1a1e64 --- /dev/null +++ b/public/i18n/de.json @@ -0,0 +1,6 @@ +{ + "common": { + "home": "Startseite", + "about": "Über mich" + } +} \ No newline at end of file diff --git a/public/i18n/en.json b/public/i18n/en.json new file mode 100644 index 0000000..ca0d65b --- /dev/null +++ b/public/i18n/en.json @@ -0,0 +1,6 @@ +{ + "common": { + "home": "Home", + "about": "About" + } +} \ No newline at end of file diff --git a/src/app/app.config.ts b/src/app/app.config.ts index beaa65c..e8e08b2 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,4 +1,4 @@ -import { ApplicationConfig, LOCALE_ID, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core'; +import { ApplicationConfig, LOCALE_ID, provideAppInitializer, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core'; import { provideRouter, withComponentInputBinding } from '@angular/router'; import { providePrimeNG } from 'primeng/config'; @@ -6,9 +6,14 @@ import { Theme } from '../../public/theme/theme'; import { routes } from './app.routes'; import { registerLocaleData } from '@angular/common'; -import localeDeAt from '@angular/common/locales/de-AT'; +import localeDeAt from '@angular/common/locales/de'; import { germanTranslation } from './core/config/translations'; +import {provideTranslateService} from "@ngx-translate/core"; +import {provideTranslateHttpLoader} from "@ngx-translate/http-loader"; +import { provideHttpClient } from '@angular/common/http'; +import { initializeTranslations } from './core/config/translation-init'; + registerLocaleData(localeDeAt); export const appConfig: ApplicationConfig = { @@ -16,6 +21,7 @@ export const appConfig: ApplicationConfig = { provideBrowserGlobalErrorListeners(), provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes, withComponentInputBinding()), + provideHttpClient(), { provide: LOCALE_ID, useValue: 'de-AT' }, providePrimeNG({ theme: { @@ -31,5 +37,12 @@ export const appConfig: ApplicationConfig = { }, translation: germanTranslation, // TODO: dynamic - selected language }), + provideTranslateService({ + loader: provideTranslateHttpLoader({ + prefix: 'i18n/', + suffix: '.json' + }), + }), + provideAppInitializer(initializeTranslations), ], }; diff --git a/src/app/app.ts b/src/app/app.ts index 80224b9..febb400 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { ThemeSwitchService } from './core/service/theme-switch.service'; import { Background } from "./core/components/background/background"; @@ -11,7 +11,9 @@ import { NavigationBar } from "./core/components/navigation-bar/navigation-bar"; }) export class App { - constructor(private themeSwitchService: ThemeSwitchService) { + private themeSwitchService = inject(ThemeSwitchService); + + constructor() { this.themeSwitchService.initialize(); } diff --git a/src/app/core/components/background/background.ts b/src/app/core/components/background/background.ts index ed2fed3..2fcf49c 100644 --- a/src/app/core/components/background/background.ts +++ b/src/app/core/components/background/background.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, HostListener, ViewChild } from '@angular/core'; +import { Component, ElementRef, HostListener, inject, ViewChild } from '@angular/core'; import { ThemeSwitchService } from '../../service/theme-switch.service'; @Component({ @@ -9,9 +9,9 @@ import { ThemeSwitchService } from '../../service/theme-switch.service'; }) export class Background { - constructor( - private themeSwitchService: ThemeSwitchService, - ) {} + private themeSwitchService = inject(ThemeSwitchService); + + constructor() {} @ViewChild('canvas', { static: true }) canvasRef!: ElementRef; diff --git a/src/app/core/components/navigation-bar/navigation-bar.html b/src/app/core/components/navigation-bar/navigation-bar.html index 95d2f43..99d7549 100644 --- a/src/app/core/components/navigation-bar/navigation-bar.html +++ b/src/app/core/components/navigation-bar/navigation-bar.html @@ -7,10 +7,10 @@ @@ -26,7 +26,9 @@ } -
EN
+
+ {{ translateService.getCurrentLang().toUpperCase() }} +
diff --git a/src/app/core/components/navigation-bar/navigation-bar.ts b/src/app/core/components/navigation-bar/navigation-bar.ts index 0857dfa..b933472 100644 --- a/src/app/core/components/navigation-bar/navigation-bar.ts +++ b/src/app/core/components/navigation-bar/navigation-bar.ts @@ -1,28 +1,34 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { Router, RouterLink, RouterLinkActive } from '@angular/router'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { faMoon } from '@fortawesome/free-solid-svg-icons'; import { faSun } from '@fortawesome/free-solid-svg-icons'; import { ThemeSwitchService } from '../../service/theme-switch.service'; import { Card } from 'primeng/card'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; @Component({ selector: 'app-navigation-bar', - imports: [FontAwesomeModule, RouterLink, RouterLinkActive, Card ], + imports: [FontAwesomeModule, RouterLink, RouterLinkActive, Card, TranslatePipe], templateUrl: './navigation-bar.html', styleUrl: './navigation-bar.scss' }) export class NavigationBar { - constructor( - private router: Router, - protected themeSwitchService: ThemeSwitchService, - ) {} + private router = inject(Router); + protected themeSwitchService = inject(ThemeSwitchService); + protected translateService = inject(TranslateService); + + constructor() {} isActive(route: string): boolean { return this.router.url === route; } + toggleLanguage(): void { + this.translateService.use(this.translateService.getCurrentLang() === 'de' ? 'en' : 'de'); + } + faMoon = faMoon; faSun = faSun; diff --git a/src/app/core/config/translation-init.ts b/src/app/core/config/translation-init.ts new file mode 100644 index 0000000..535747b --- /dev/null +++ b/src/app/core/config/translation-init.ts @@ -0,0 +1,22 @@ +import { inject } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +export function initializeTranslations() { + const translate = inject(TranslateService); + + const availableLangs = ['de', 'at', 'en']; + translate.addLangs(availableLangs); + + const defaultLang = 'de'; + let langToUse = translate.getBrowserLang() ?? defaultLang; + langToUse = availableLangs.includes(langToUse) ? langToUse : defaultLang; + + translate.setFallbackLang(defaultLang); + translate.use(langToUse); + + document.documentElement.lang = langToUse; + + translate.onLangChange.subscribe((event) => { + document.documentElement.lang = event.lang; + }); +} diff --git a/src/app/core/config/translations.ts b/src/app/core/config/translations.ts index 4393cb1..27edb79 100644 --- a/src/app/core/config/translations.ts +++ b/src/app/core/config/translations.ts @@ -69,7 +69,6 @@ export const germanTranslation: Translation = { emptySelectionMessage: 'Kein Element ausgewählt', emptySearchMessage: 'Keine Ergebnisse gefunden', emptyMessage: 'Keine Optionen verfügbar', - aria: { trueLabel: 'Wahr', falseLabel: 'Falsch', @@ -121,3 +120,132 @@ export const germanTranslation: Translation = { rotateLeft: 'Nach links drehen' } }; + +export const austrianTranslation: Translation = { + ...germanTranslation, + monthNames: [ + 'Jänner','Februar','März','April','Mai','Juni', + 'Juli','August','September','Oktober','November','Dezember' + ], +}; + +export const englishTranslation: Translation = { + startsWith: 'Starts with', + contains: 'Contains', + notContains: 'Does not contain', + endsWith: 'Ends with', + equals: 'Equals', + notEquals: 'Not equals', + noFilter: 'No Filter', + lt: 'Less than', + lte: 'Less than or equal to', + gt: 'Greater than', + gte: 'Greater than or equal to', + dateIs: 'Date is', + dateIsNot: 'Date is not', + dateBefore: 'Date is before', + dateAfter: 'Date is after', + clear: 'Clear', + apply: 'Apply', + matchAll: 'Match All', + matchAny: 'Match Any', + addRule: 'Add Rule', + removeRule: 'Remove Rule', + accept: 'Yes', + reject: 'No', + choose: 'Choose', + upload: 'Upload', + cancel: 'Cancel', + completed: 'Completed', + pending: 'Pending', + fileSizeTypes: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], + dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + dayNamesMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], + monthNames: [ + 'January','February','March','April','May','June', + 'July','August','September','October','November','December' + ], + monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'], + chooseYear: 'Choose Year', + chooseMonth: 'Choose Month', + chooseDate: 'Choose Date', + prevDecade: 'Previous Decade', + nextDecade: 'Next Decade', + prevYear: 'Previous Year', + nextYear: 'Next Year', + prevMonth: 'Previous Month', + nextMonth: 'Next Month', + prevHour: 'Previous Hour', + nextHour: 'Next Hour', + prevMinute: 'Previous Minute', + nextMinute: 'Next Minute', + prevSecond: 'Previous Second', + nextSecond: 'Next Second', + am: 'AM', + pm: 'PM', + today: 'Today', + weekHeader: 'Wk', + firstDayOfWeek: 0, + dateFormat: 'mm/dd/yy', + weak: 'Weak', + medium: 'Medium', + strong: 'Strong', + passwordPrompt: 'Enter a password', + emptyFilterMessage: 'No results found', + searchMessage: '{0} results available', + selectionMessage: '{0} items selected', + emptySelectionMessage: 'No item selected', + emptySearchMessage: 'No results found', + emptyMessage: 'No options available', + aria: { + trueLabel: 'True', + falseLabel: 'False', + nullLabel: 'Not selected', + star: '1 Star', + stars: '{star} Stars', + selectAll: 'Select all items', + unselectAll: 'Unselect all items', + close: 'Close', + previous: 'Previous', + next: 'Next', + navigation: 'Navigation', + scrollTop: 'Scroll to top', + moveTop: 'Move to top', + moveUp: 'Move up', + moveDown: 'Move down', + moveBottom: 'Move to bottom', + moveToTarget: 'Move to target', + moveToSource: 'Move to source', + moveAllToTarget: 'Move all to target', + moveAllToSource: 'Move all to source', + pageLabel: '{page}', + firstPageLabel: 'First Page', + lastPageLabel: 'Last Page', + nextPageLabel: 'Next Page', + prevPageLabel: 'Previous Page', + rowsPerPageLabel: 'Rows per page', + jumpToPageDropdownLabel: 'Select page', + jumpToPageInputLabel: 'Enter page', + selectRow: 'Row selected', + unselectRow: 'Row unselected', + expandRow: 'Row expanded', + collapseRow: 'Row collapsed', + showFilterMenu: 'Show filter menu', + hideFilterMenu: 'Hide filter menu', + filterOperator: 'Filter operator', + filterConstraint: 'Filter constraint', + editRow: 'Edit row', + saveEdit: 'Save edit', + cancelEdit: 'Cancel edit', + listView: 'List view', + gridView: 'Grid view', + slide: 'Slide', + slideNumber: '{slideNumber}', + zoomImage: 'Zoom image', + zoomIn: 'Zoom in', + zoomOut: 'Zoom out', + rotateRight: 'Rotate right', + rotateLeft: 'Rotate left' + } +}; diff --git a/src/index.html b/src/index.html index 37d641a..570439b 100644 --- a/src/index.html +++ b/src/index.html @@ -1,5 +1,5 @@ - + byHaider