From ddf8fb34e6ba43a73d5b36a54f9cb19686ed0a4e Mon Sep 17 00:00:00 2001 From: Andrii Rodzyk Date: Wed, 31 Jan 2024 14:05:24 +0200 Subject: [PATCH] fead: Multi lang UI --- angular.json | 6 +- src/app/app.component.ts | 21 ++- src/app/app.module.ts | 8 +- .../imgur-shell/imgur-shell.component.html | 4 +- .../imgur-shell/imgur-shell.component.ts | 5 +- .../link-parser/link-parser.component.html | 162 ++++++++++++++++-- .../link-parser/link-parser.component.scss | 4 + .../link-parser/link-parser.component.ts | 19 +- src/app/shared/data-access/lang.service.ts | 65 +++++++ src/app/shared/data-access/locale.provider.ts | 8 + src/app/shared/data-access/viewer.service.ts | 8 +- src/app/shared/shared.module.ts | 2 +- .../nsfw-warning/nsfw-warning.component.html | 4 +- .../ui/nsfw-warning/nsfw-warning.component.ts | 6 +- .../view-mode-bar.component.html | 2 +- .../view-mode-bar/view-mode-bar.component.ts | 4 + .../shared/ui/viewer/viewer.component.html | 11 +- src/app/shared/ui/viewer/viewer.component.ts | 3 +- .../warm-control/warm-control.component.html | 2 +- .../ui/warm-control/warm-control.component.ts | 3 + src/app/shared/utils/phrases.ts | 30 ++++ src/assets/langs/uk.json | 20 +++ src/manifest-uk.webmanifest | 90 ++++++++++ 23 files changed, 440 insertions(+), 47 deletions(-) create mode 100644 src/app/shared/data-access/lang.service.ts create mode 100644 src/app/shared/data-access/locale.provider.ts create mode 100644 src/app/shared/utils/phrases.ts create mode 100644 src/assets/langs/uk.json create mode 100644 src/manifest-uk.webmanifest diff --git a/angular.json b/angular.json index 17170a9..3d697de 100644 --- a/angular.json +++ b/angular.json @@ -53,7 +53,8 @@ "assets": [ "src/favicon.ico", "src/assets", - "src/manifest.webmanifest" + "src/manifest.webmanifest", + "src/manifest-uk.webmanifest" ], "styles": [ "src/styles.scss" @@ -126,7 +127,8 @@ "assets": [ "src/favicon.ico", "src/assets", - "src/manifest.webmanifest" + "src/manifest.webmanifest", + "src/manifest-uk.webmanifest" ], "styles": [ "src/styles.scss" diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 403b209..b29e45e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,7 +1,24 @@ -import { Component } from '@angular/core'; +import { Component, WritableSignal, signal } from '@angular/core'; +import { LangService } from './shared/data-access/lang.service'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-root', template: `` }) -export class AppComponent {} +export class AppComponent { + constructor(public lang: LangService, private route: ActivatedRoute) { + this.lang.updateManifest() + this.lang.updateTranslate() + } + + ngOnInit() { + this.route.pathFromRoot[0].queryParams.subscribe(q => { + const l = q['lang'] + if (l && this.lang.manifests.has(l)) { + this.lang.setLang(l) + this.lang.updateManifest() + } + }) + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d911f73..1f2ce64 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,4 +1,4 @@ -import { NgModule, isDevMode } from '@angular/core'; +import { LOCALE_ID, NgModule, isDevMode } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; @@ -8,6 +8,12 @@ import { ServiceWorkerModule } from '@angular/service-worker'; import { PageNotFoundComponent } from './page-not-found.component'; import { SharedModule } from './shared/shared.module'; +import { LocaleProvider } from './shared/data-access/locale.provider'; +import { registerLocaleData } from '@angular/common'; +import localeUk from "@angular/common/locales/uk"; + +registerLocaleData(localeUk) + @NgModule({ declarations: [ AppComponent, diff --git a/src/app/imgur/imgur-shell/imgur-shell.component.html b/src/app/imgur/imgur-shell/imgur-shell.component.html index 52e42c9..fe2f220 100644 --- a/src/app/imgur/imgur-shell/imgur-shell.component.html +++ b/src/app/imgur/imgur-shell/imgur-shell.component.html @@ -5,8 +5,8 @@ Imgur logo -

Images via Imgur API. - Thanks!
Details on their site. Respect copyrights.

+

{{lang.phrases.imagesVia}}Imgur API. + {{lang.phrases.thanks}}
{{lang.phrases.detalisCopy}}

diff --git a/src/app/imgur/imgur-shell/imgur-shell.component.ts b/src/app/imgur/imgur-shell/imgur-shell.component.ts index 046a915..92ce756 100644 --- a/src/app/imgur/imgur-shell/imgur-shell.component.ts +++ b/src/app/imgur/imgur-shell/imgur-shell.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router'; import { ImgurService } from '../data-access/imgur.service'; import { CompositionEpisode } from '../../shared/utils'; import { BehaviorSubject, Subject, catchError, finalize, forkJoin, of, switchMap, takeUntil } from 'rxjs'; +import { LangService } from '../../shared/data-access/lang.service'; @Component({ selector: 'app-imgur-shell', @@ -23,7 +24,7 @@ export class ImgurShellComponent { return (this.imgur.getComposition(id)).pipe( catchError(() => { - this.error$.next('Data loading error. Please try again later.'); + this.error$.next(this.lang.phrases.dataLoadErr); return of(null); }), @@ -32,5 +33,5 @@ export class ImgurShellComponent { }) ); - constructor(private route: ActivatedRoute, public imgur: ImgurService) { } + constructor(private route: ActivatedRoute, public imgur: ImgurService, public lang: LangService) { } } diff --git a/src/app/link-parser/link-parser/link-parser.component.html b/src/app/link-parser/link-parser/link-parser.component.html index 4c524b7..6919882 100644 --- a/src/app/link-parser/link-parser/link-parser.component.html +++ b/src/app/link-parser/link-parser/link-parser.component.html @@ -1,21 +1,19 @@
- - +
- - +
- and read it - easily and comfortably! + {{lang.phrases.slogan}} - + - Let's go + {{lang.phrases.letsgo}} {{linkParams()?.site}} {{'/'+linkParams()?.id | truncate}} @@ -25,14 +23,142 @@
- - - - + + + + - - - - + + + + -
\ No newline at end of file +
+ + + + + + + {{lang.phrases.shortTitle}}, © 2024 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/link-parser/link-parser/link-parser.component.scss b/src/app/link-parser/link-parser/link-parser.component.scss index c82468d..c7c2f1b 100644 --- a/src/app/link-parser/link-parser/link-parser.component.scss +++ b/src/app/link-parser/link-parser/link-parser.component.scss @@ -100,4 +100,8 @@ svg:hover { scale: 1.16; } +} + +app-overlay { + position: absolute; } \ No newline at end of file diff --git a/src/app/link-parser/link-parser/link-parser.component.ts b/src/app/link-parser/link-parser/link-parser.component.ts index 48b5c06..d262f10 100644 --- a/src/app/link-parser/link-parser/link-parser.component.ts +++ b/src/app/link-parser/link-parser/link-parser.component.ts @@ -2,7 +2,13 @@ import { Component, Signal, WritableSignal, computed, signal } from '@angular/co import { LinkParserService } from '../data-access/link-parser.service'; import { ImgurLinkParser, MangadexLinkParser } from '../utils'; import { ActivatedRoute , Router} from '@angular/router'; +import { LangService } from '../../shared/data-access/lang.service'; +import { ViewModeOption } from '../../shared/data-access'; +const LANG_OPTIONS: ViewModeOption[] = [ + { dir: "rtl", mode: "pages", hintPhraceKey: "english", code: "en", emoji: "🇬🇧" }, + { dir: "ltr", mode: "pages", hintPhraceKey: "ukrainian", code: "uk", emoji: "🇺🇦" } +] @Component({ selector: 'app-link-parser', templateUrl: './link-parser.component.html', @@ -16,9 +22,14 @@ export class LinkParserComponent { link: WritableSignal = signal(''); linkParams: Signal = computed(() => this.parser.parse(this.link())); + langOpt = LANG_OPTIONS - constructor(private route: ActivatedRoute, private router: Router, public parser: LinkParserService) { + optLangValue = () => this.langOpt.filter((opt: any)=>opt.code == this.lang.lang())[0] + + constructor(private route: ActivatedRoute, private router: Router, public parser: LinkParserService, public lang: LangService) { this.initParser(); + // console.log( this.); + } initParser() { @@ -33,10 +44,10 @@ export class LinkParserComponent { } ngOnInit() { - const q: string | null = this.route.snapshot.queryParamMap.get('q'); + const url: string | null = this.route.snapshot.queryParamMap.get('url'); - if (q) { - this.link.set(q ?? '') + if (url) { + this.link.set(url ?? '') } else { this.initFromclipboard(); } diff --git a/src/app/shared/data-access/lang.service.ts b/src/app/shared/data-access/lang.service.ts new file mode 100644 index 0000000..c7ea0fc --- /dev/null +++ b/src/app/shared/data-access/lang.service.ts @@ -0,0 +1,65 @@ +import { Injectable, WritableSignal, signal } from '@angular/core'; +import { Phrases } from '../utils/phrases'; +import { Observable, map, of } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; + +type Langs = { en: string, uk: string } + +const DEFAULT_LANG = 'en' +const LANG_STORAGE_NAME = 'lang' + + +@Injectable({ + providedIn: 'root' +}) +export class LangService { + public readonly manifests = new Map([ + ['en', "manifest.webmanifest"], + ['uk', "manifest-uk.webmanifest"] + ]); + + lang: WritableSignal = signal(localStorage.getItem(LANG_STORAGE_NAME) ?? DEFAULT_LANG); + linkManifestElement: WritableSignal = signal(document.querySelector('link[rel="manifest"]')) + + phrases: Phrases = new Phrases(); + + constructor(private http: HttpClient) { } + + setLang(lang: string) { + this.lang.set(lang) + document.documentElement.lang = lang + localStorage.setItem(LANG_STORAGE_NAME, lang) + this.updateTranslate() + } + + updateManifest() { + this.linkManifestElement()?.setAttribute('href', this.manifests.get(this.lang()) ?? 'manifest.webmanifest'); + } + + updateTranslate() { + if (this.lang() == 'en') { this.phrases = new Phrases(); return; } + + this.getTranslate(this.lang()).subscribe(data =>{ + this.phrases = data + }) + } + + getTranslate(lang: string): Observable { + return this.http.get(`assets/langs/${lang}.json`).pipe( + map(data => { + const ph = new Phrases(); + const keys = Object.keys(ph); + + keys.forEach((key: string) => { + const translatedValue: string = data[key as keyof Phrases] as string + + if (translatedValue) { + (ph[key as keyof Phrases] as any) = data[key as keyof Phrases] + } + }) + + return ph; + }) + ) + } +} diff --git a/src/app/shared/data-access/locale.provider.ts b/src/app/shared/data-access/locale.provider.ts new file mode 100644 index 0000000..f1e7b7a --- /dev/null +++ b/src/app/shared/data-access/locale.provider.ts @@ -0,0 +1,8 @@ +import { LOCALE_ID, Provider } from '@angular/core'; +import { LangService } from './lang.service'; + +export let LocaleProvider: Provider = { + provide: LOCALE_ID, + deps: [LangService], + useFactory: (ls: LangService) => ls.lang(), +}; \ No newline at end of file diff --git a/src/app/shared/data-access/viewer.service.ts b/src/app/shared/data-access/viewer.service.ts index 8cce4eb..48fece3 100644 --- a/src/app/shared/data-access/viewer.service.ts +++ b/src/app/shared/data-access/viewer.service.ts @@ -3,15 +3,15 @@ import { Injectable } from '@angular/core'; export interface ViewModeOption { dir: "rtl" | "ltr"; mode: "pages" | "long" - title: string; code: string; emoji: string; + hintPhraceKey: string; } export const VIEV_MODE_OPTIONS: ViewModeOption[] = [ - { dir: "rtl", mode: "pages", title: "Scroll left", code: "", emoji: "⬅️" }, - { dir: "ltr", mode: "pages", title: "Scroll right", code: "", emoji: "➡️" }, - { dir: "ltr", mode: "long", title: "Scroll down", code: "", emoji: "⬇️" }, + { dir: "rtl", mode: "pages", hintPhraceKey: "scrollLeft", code: "", emoji: "⬅️" }, + { dir: "ltr", mode: "pages", hintPhraceKey: "scrollRight", code: "", emoji: "➡️" }, + { dir: "ltr", mode: "long", hintPhraceKey: "scrollDown", code: "", emoji: "⬇️" }, ] @Injectable({ diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 259e0ef..9927238 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -31,6 +31,6 @@ import { NsfwWarningComponent } from './ui/nsfw-warning/nsfw-warning.component'; FormsModule, RouterModule ], - exports: [TruncatePipe, TextEmbracerComponent, ViewerComponent] + exports: [TruncatePipe, TextEmbracerComponent, ViewerComponent, OverlayComponent, ViewModeBarComponent] }) export class SharedModule { } diff --git a/src/app/shared/ui/nsfw-warning/nsfw-warning.component.html b/src/app/shared/ui/nsfw-warning/nsfw-warning.component.html index 44d471d..5b5ccfc 100644 --- a/src/app/shared/ui/nsfw-warning/nsfw-warning.component.html +++ b/src/app/shared/ui/nsfw-warning/nsfw-warning.component.html @@ -2,6 +2,6 @@
{{title}}
- - + +
\ No newline at end of file diff --git a/src/app/shared/ui/nsfw-warning/nsfw-warning.component.ts b/src/app/shared/ui/nsfw-warning/nsfw-warning.component.ts index 6e1a20c..096d152 100644 --- a/src/app/shared/ui/nsfw-warning/nsfw-warning.component.ts +++ b/src/app/shared/ui/nsfw-warning/nsfw-warning.component.ts @@ -6,8 +6,12 @@ import { Component, EventEmitter, Output, Input } from '@angular/core'; styleUrl: './nsfw-warning.component.scss' }) export class NsfwWarningComponent { - title: string = `⚠️🔞 NSFW Content`; + @Input() title: string = `⚠️🔞 NSFW Content`; @Input() text = "The following content may be Not Safe For Work. Viewer discretion is advised."; + + @Input() labelAgree: string = "Ready for the wild side!" + @Input() labelDisagree: string = "I'll pass, let's keep it safe." + @Output() agree: EventEmitter = new EventEmitter(); @Output() disagree: EventEmitter = new EventEmitter(); diff --git a/src/app/shared/ui/view-mode-bar/view-mode-bar.component.html b/src/app/shared/ui/view-mode-bar/view-mode-bar.component.html index fa99a49..c958c1e 100644 --- a/src/app/shared/ui/view-mode-bar/view-mode-bar.component.html +++ b/src/app/shared/ui/view-mode-bar/view-mode-bar.component.html @@ -2,7 +2,7 @@ -