Skip to content

Instantly share code, notes, and snippets.

@SalathielGenese
Created June 22, 2023 16:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SalathielGenese/bc866020687776779d55acc74a5834cb to your computer and use it in GitHub Desktop.
Save SalathielGenese/bc866020687776779d55acc74a5834cb to your computer and use it in GitHub Desktop.
Override Angular's ApplicationRef.isStable
import {ApplicationRef, DestroyRef, Injectable} from "@angular/core";
import {SanityClient} from "@sanity/client";
import {BehaviorSubject, debounceTime, filter, map, mergeMap, Observable, of, ReplaySubject, skip, tap} from "rxjs";
import {LocaleService} from "./locale.service";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {fromPromise} from "rxjs/internal/observable/innerFrom";
@Injectable()
export class I18nService {
readonly #keys$ = new BehaviorSubject([] as string[]);
readonly #pendingCount$: ReplaySubject<number> = new ReplaySubject<number>();
readonly #cache$ = new BehaviorSubject({} as TranslationMap);
constructor(destroyRef: DestroyRef,
sanityClient: SanityClient,
localeService: LocaleService,
applicationRef: ApplicationRef) {
const isStable$ = applicationRef.isStable;
Reflect.defineProperty(applicationRef, 'isStable', {
...Reflect.getOwnPropertyDescriptor(applicationRef, 'isStable'),
value: isStable$.pipe(mergeMap(_ => {
let current: undefined | number;
this.#pendingCount$.subscribe(_ => current = _).unsubscribe();
return 0 === current ? of(_).pipe(debounceTime(1)) : of(false);
})),
});
localeService.localeCode$
.pipe(takeUntilDestroyed(destroyRef))
.subscribe(() => this.#keys$.next([
...this.#current(this.#keys$)!,
...Object.keys(this.#current(this.#cache$)!),
]));
localeService.localeCode$
.pipe(mergeMap(localeCode =>
this.#keys$.pipe(map(keys => [localeCode, keys] as const))))
.pipe(takeUntilDestroyed(destroyRef))
.pipe(filter(_ => !!_[1].length))
.pipe(debounceTime(50))
.pipe(mergeMap(([localeCode, keys]) => {
for (const key of keys) this.#cache$.value[key] = undefined;
return fromPromise(sanityClient
.fetch<I18nTranslation[]>(`
*[_type == "i18nTranslations" && !(_id match "drafts.") && locale->code == $localeCode && key->key in $keys] {
"key": key->key,
value
}`, {keys: keys.splice(0), localeCode}));
}))
.pipe(map(_ => _.reduce((__, _) => ({
...__,
[_.key]: _.value,
}), this.#cache$.value)))
.pipe(tap(this.#cache$.next.bind(this.#cache$)))
.subscribe(() => setTimeout(() =>
this.#pendingCount$.next(this.#current(this.#pendingCount$)! - 1)));
}
fetch(...keys: (string | undefined | null)[]): Observable<TranslationMap> {
const cache = this.#cache$.value;
const pruned = keys.filter(_ => _) as string[];
const missing = pruned.filter(_ => !(_ in cache));
missing.length && !this.#keys$.value.length
&& this.#pendingCount$.next((this.#current(this.#pendingCount$) ?? 0) + 1);
missing.length && this.#keys$.next([...missing, ...this.#keys$.value]);
const pending = pruned.filter(_ => undefined === cache[_] && _ in cache);
return this.#cache$
.pipe(skip((missing.length ? 1 : 0) + (pending.length ? 1 : 0)))
.pipe(map(() => pruned.reduce((__, _) => ({
...__,
[_]: this.#cache$.value[_],
}), {} as TranslationMap)));
}
#current<T>(observable: Observable<T>): T | undefined {
let current: T | undefined;
observable.subscribe(value => current = value).unsubscribe();
return current;
}
}
export type TranslationMap = Record<string, undefined | string | null | never>;
export interface I18nTranslation {
value: string | null;
key: string;
}
@SalathielGenese
Copy link
Author

For the context, I use Sanity.io for I81n.

But you should check for the free tier quota. Here

So I tought it would be great to throtle my queries to it and run them in batches.

Fortunately, it does work on the backend (SSR) where my content is rendered without translation (not good for SEO).

Moreover, it cause a glitch on the browser between SSR response and when the browser gets its own i18n translations.

So the idea is to increment the batches count issued and decrement when they complete and mork the application (well, this seervice) as stable only when that count is 0 (zero).

But Angular SSR is even faster than that so I didn't use a BehaviourSubject but a ReplaySubject to avoid initial values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment