Skip to content

Commit

Permalink
refactor: move translation logic into public NPM package
Browse files Browse the repository at this point in the history
  • Loading branch information
MaSch0212 committed Jul 8, 2024
1 parent 235614b commit bfae9ae
Show file tree
Hide file tree
Showing 15 changed files with 45 additions and 138 deletions.
1 change: 1 addition & 0 deletions src/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@angular/service-worker": "^18.0.2",
"@microsoft/signalr": "^8.0.0",
"@ngneers/easy-ngrx-distinct-selector": "^0.1.1",
"@ngneers/signal-translate": "^0.1.0",
"@ngrx/effects": "18.0.0-rc.0",
"@ngrx/entity": "18.0.0-rc.0",
"@ngrx/operators": "18.0.0-rc.0",
Expand Down
15 changes: 15 additions & 0 deletions src/client/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CommonModule, formatDate } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { InterpolatePipe, interpolate } from '@ngneers/signal-translate';
import { Actions, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { AccordionModule } from 'primeng/accordion';
Expand All @@ -26,7 +27,6 @@ import { keepEventLoaded } from '../../../+state/events/events.utils';
import { mapSelectors } from '../../../+state/maps';
import { keepMapsLoaded } from '../../../+state/maps/maps.utils';
import { keepUsersLoaded, userSelectors } from '../../../+state/users';
import { InterpolatePipe, interpolate } from '../../../directives/interpolate.pipe';
import { TranslateService } from '../../../services/translate.service';
import { ifTruthy } from '../../../utils/common.utils';
import { compareTimes } from '../../../utils/date.utils';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/c
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { InterpolatePipe, interpolate } from '@ngneers/signal-translate';
import { Actions, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { AccordionModule } from 'primeng/accordion';
Expand Down Expand Up @@ -35,7 +36,6 @@ import {
selectUsersActionState,
userSelectors,
} from '../../../+state/users';
import { InterpolatePipe, interpolate } from '../../../directives/interpolate.pipe';
import { EventInstancePreconfiguration, User } from '../../../models/parsed-models';
import { TranslateService } from '../../../services/translate.service';
import { ifTruthy, isNullish } from '../../../utils/common.utils';
Expand Down
2 changes: 1 addition & 1 deletion src/client/src/app/components/login/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { InterpolatePipe } from '@ngneers/signal-translate';
import { ButtonModule } from 'primeng/button';
import { CardModule } from 'primeng/card';
import { InputTextModule } from 'primeng/inputtext';
Expand All @@ -19,7 +20,6 @@ import { TooltipModule } from 'primeng/tooltip';
import { distinctUntilChanged, map, startWith } from 'rxjs';

import { ErrorTextDirective } from '../../directives/error-text.directive';
import { InterpolatePipe } from '../../directives/interpolate.pipe';
import { OnEnterDirective } from '../../directives/on-enter.directive';
import { AuthService, SignInResult } from '../../services/auth.service';
import { Logger } from '../../services/logger.service';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { InterpolatePipe } from '@ngneers/signal-translate';
import { Actions, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { ButtonModule } from 'primeng/button';
Expand All @@ -18,7 +19,6 @@ import { MessagesModule } from 'primeng/messages';

import { isActionBusy } from '../../../+state/action-state';
import { addMapAction, selectMapsActionState, updateMapAction } from '../../../+state/maps';
import { InterpolatePipe } from '../../../directives/interpolate.pipe';
import { MinigolfMap } from '../../../models/parsed-models';
import { TranslateService } from '../../../services/translate.service';
import { selectSignal } from '../../../utils/ngrx.utils';
Expand Down
2 changes: 1 addition & 1 deletion src/client/src/app/components/maps/maps.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { interpolate } from '@ngneers/signal-translate';
import { Actions, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { ConfirmationService, MessageService } from 'primeng/api';
Expand All @@ -15,7 +16,6 @@ import { MapItemComponent } from './map-item/map-item.component';
import { hasActionFailed, isActionBusy } from '../../+state/action-state';
import { mapSelectors, removeMapAction, selectMapsActionState } from '../../+state/maps';
import { keepMapsLoaded } from '../../+state/maps/maps.utils';
import { interpolate } from '../../directives/interpolate.pipe';
import { MinigolfMap } from '../../models/parsed-models';
import { TranslateService } from '../../services/translate.service';
import { notNullish } from '../../utils/common.utils';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { RouterLink } from '@angular/router';
import { InterpolatePipe } from '@ngneers/signal-translate';
import { MessagesModule } from 'primeng/messages';
import { ProgressSpinnerModule } from 'primeng/progressspinner';
import { map, timer } from 'rxjs';

import { hasActionFailed, isActionBusy } from '../../+state/action-state';
import { playerEventSelectors, selectPlayerEventsActionState } from '../../+state/player-events';
import { keepPlayerEventsLoaded } from '../../+state/player-events/player-events.utils';
import { InterpolatePipe } from '../../directives/interpolate.pipe';
import { PlayerEvent } from '../../models/parsed-models';
import { AuthService } from '../../services/auth.service';
import { TranslateService } from '../../services/translate.service';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { interpolate, InterpolatePipe } from '@ngneers/signal-translate';
import copyToClipboard from 'copy-to-clipboard';
import { MessageService } from 'primeng/api';
import { ButtonModule } from 'primeng/button';
import { DialogModule } from 'primeng/dialog';

import { interpolate, InterpolatePipe } from '../../../directives/interpolate.pipe';
import { User } from '../../../models/parsed-models';
import { TranslateService } from '../../../services/translate.service';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { InterpolatePipe } from '@ngneers/signal-translate';
import { Actions, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import copyToClipboard from 'copy-to-clipboard';
Expand All @@ -35,7 +36,6 @@ import {
updateUserAction,
userSelectors,
} from '../../../+state/users';
import { InterpolatePipe } from '../../../directives/interpolate.pipe';
import { User } from '../../../models/parsed-models';
import { TranslateService } from '../../../services/translate.service';
import { areArraysEqual } from '../../../utils/array.utils';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { InterpolatePipe } from '@ngneers/signal-translate';
import { AutoFocusModule } from 'primeng/autofocus';
import { ButtonModule } from 'primeng/button';
import { DialogModule } from 'primeng/dialog';
import { InputTextModule } from 'primeng/inputtext';
import { InputTextareaModule } from 'primeng/inputtextarea';

import { NotificationsService } from '../../../api/services';
import { InterpolatePipe } from '../../../directives/interpolate.pipe';
import { User } from '../../../models/parsed-models';
import { TranslateService } from '../../../services/translate.service';

Expand Down
2 changes: 1 addition & 1 deletion src/client/src/app/components/users/users.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { interpolate } from '@ngneers/signal-translate';
import { Store } from '@ngrx/store';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ButtonModule } from 'primeng/button';
Expand All @@ -18,7 +19,6 @@ import {
selectUsersActionState,
userSelectors,
} from '../../+state/users';
import { interpolate } from '../../directives/interpolate.pipe';
import { User } from '../../models/parsed-models';
import { TranslateService } from '../../services/translate.service';
import { notNullish } from '../../utils/common.utils';
Expand Down
2 changes: 1 addition & 1 deletion src/client/src/app/directives/error-text.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
inject,
input,
} from '@angular/core';
import { interpolate } from '@ngneers/signal-translate';

import { interpolate } from './interpolate.pipe';
import { TranslateService } from '../services/translate.service';

@Directive({
Expand Down
37 changes: 0 additions & 37 deletions src/client/src/app/directives/interpolate.pipe.ts

This file was deleted.

108 changes: 18 additions & 90 deletions src/client/src/app/services/translate.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { registerLocaleData } from '@angular/common';
import { Injectable, Signal, computed, effect, inject, signal } from '@angular/core';
import { effect, inject, Injectable } from '@angular/core';
import { BaseTranslateService, TranslateKeys } from '@ngneers/signal-translate';
import { PrimeNGConfig } from 'primeng/api';

import { getLocalStorage, setLocalStorage } from '../utils/local-storage.utils';
Expand All @@ -18,76 +19,28 @@ const langs: Record<string, () => Promise<LangType>> = {
de: () => import('../i18n/de').then(x => x.default),
};

type TranslateKeys<T> = T extends object
? T extends unknown[]
? never
: T extends Set<unknown>
? never
: T extends Map<unknown, unknown>
? never
: T extends Function
? never
: {
[K in keyof T]: K extends string
? T[K] extends string
? K
: `${K}_${TranslateKeys<T[K]>}`
: never;
}[keyof T]
: never;
export type TranslationKey = TranslateKeys<typeof en>;

const langLocalStorageKey = 'lang';

export type TranslationKey = TranslateKeys<typeof en, '_'>;

@Injectable({ providedIn: 'root' })
export class TranslateService {
export class TranslateService extends BaseTranslateService<typeof en> {
private readonly _primengConfig = inject(PrimeNGConfig);

private readonly _translations = signal<typeof en | undefined>(undefined);
private readonly _language = signal(getLocalStorage(langLocalStorageKey));

public readonly translations = toTranslationsSignal(this._translations);
public readonly browserLanguage = computed(() => navigator.language ?? 'en');
public readonly language = computed(() => this._language() ?? this.browserLanguage());

constructor() {
effect(() => {
const lang = this.language();

document.documentElement.lang = lang;
super(['en', 'de'], getLocalStorage(langLocalStorageKey));

let getTranslations = langs[lang];
if (!getTranslations && lang.includes('-')) {
getTranslations = langs[lang.split('-')[0]];
}
if (!getTranslations) {
getTranslations = langs['en'];
effect(() => {
if (this.isLanguage(null)) {
setLocalStorage(langLocalStorageKey, null);
} else {
setLocalStorage(langLocalStorageKey, this.language());
}

getTranslations().then(({ translations, locale, localeExtra, primengTranslations }) => {
this._translations.set(JSON.parse(JSON.stringify(translations)));
registerLocaleData(locale, lang, localeExtra);
this._primengConfig.setTranslation(primengTranslations);
});
});
}

public translate(key: string) {
const path = key.split('_');
return (getDeepValue(this.translations(), path) as string) ?? key;
}

public setLanguage(language: string | null) {
setLocalStorage(langLocalStorageKey, language);
this._language.set(language);
}

public isLanguage(language: string | null) {
return this._language() === language;
}

public getLangDisplay(lang: string | null) {
lang ??= this.browserLanguage();
lang ??= this.browserLanguage;
switch (lang) {
case 'en':
return 'English';
Expand All @@ -97,36 +50,11 @@ export class TranslateService {
return lang;
}
}
}

export function isFunctionKey(key: string): key is Extract<keyof Function, string> {
return (key in Function.prototype && key !== 'constructor') || key === 'prototype';
}
export type TranslationsSignal<T> = Signal<T> &
Readonly<{
[K in TranslateKeys<T>]: Signal<string> & { key: K };
}>;
function toTranslationsSignal<T extends Record<string, unknown>>(
signal: Signal<T | undefined>
): TranslationsSignal<T> {
return new Proxy(signal, {
get(target: Signal<T | undefined> & Record<string | symbol, unknown>, prop) {
if (typeof prop !== 'string' || isFunctionKey(prop)) {
return target[prop];
}
const sig = computed(() => getDeepValue(target(), prop.split('_')));
Object.defineProperty(sig, 'key', { value: prop });
return sig;
},
}) as TranslationsSignal<T>;
}

function getDeepValue<T extends Record<string, unknown> | undefined>(
obj: T,
path: string[]
): unknown {
return path.reduce<Record<string, unknown> | null | undefined>(
(xs, x) => (xs && xs[x] ? (xs[x] as Record<string, unknown>) : null),
obj
);
protected override async loadTranslations(lang: string): Promise<typeof en> {
const { translations, locale, localeExtra, primengTranslations } = await langs[lang]();
registerLocaleData(locale, lang, localeExtra);
this._primengConfig.setTranslation(primengTranslations);
return translations;
}
}

0 comments on commit bfae9ae

Please sign in to comment.