The translate pipes
@flyze/lib-core-angular 1.0.0-alpha.34· latestTwo pipes render translations in a template:
| Pipe | Input | Options |
|---|---|---|
fyzTranslate | a FyzTranslationConfig (or string) | { params } |
fyzTransloco | a translation key | { scope, params } |
Both are declared and exported by FyzTranslateModule
(fyz-translate.module.ts).
What they render is explained in translation-config; this doc is about
how they behave during change detection, which is the part that has bitten this library twice.
Both pipes are impure
Section titled “Both pipes are impure”pure: false
(fyz-translate.pipe.ts:17,
fyz-transloco.pipe.ts:15).
They have to be: a translation is an Observable, so there is no synchronous value to return on the
first call, and a pure pipe would never be re-run when the stream emits. Reference comparison would
not help either — { params: { name } } written inline in a template is a fresh object on every
cycle, so a pure pipe re-runs regardless.
The consequence is that transform() runs on every change detection cycle of the whole
application. Everything below exists to make that cheap.
The memoization gate
Section titled “The memoization gate”Each pipe stores its last input and returns the cached result unless the input actually changed:
// fyz-translate.pipe.ts:62if (this.#hasInput && deepEqual(value, this.#input) && deepEqual(opts, this.#opts)) { return this.result;}The comparison is structural (deepEqual from @flyze/lib-core), not by reference. Template
expressions such as {{ config | fyzTranslate: { params: buildParams() } }}, getter-backed configs
and objects arriving from async / map all hand out a new-but-equal object every cycle. Comparing
by reference would resubscribe forever; ignoring the options entirely — which fyzTransloco used to
do — makes a changing param never update. Rationale:
ADR 0001.
The #hasInput flag is separate from #input being non-null because null is a legal input: it
resolves to the literal NULL (see translation-config) and must be
cached like any other value.
In-place mutation is never detected. The previous input is stored by reference, so
deepEqual(value, this.#input) compares a mutated object against itself and reports it unchanged —
exactly the blind spot a pure pipe has. Structural comparison buys the opposite case: replacing the
object with a fresh, equal one is free. Callers must replace rather than mutate, which the
transform() JSDoc states as part of the contract
(fyz-translate.pipe.ts:49).
The two pipes gate on slightly different things:
fyzTranslatecompares both the value and the options structurally — its value is an object.fyzTranslococompares the key by reference (value === this._input, it is a string) and the options structurally (fyz-transloco.pipe.ts:46).
What happens when the input does change
Section titled “What happens when the input does change”- The new input is stored.
- The
Unsubscriberisreset()— torn down and re-armed — so the old translation stream must not keep writing into#result. selectTranslate()is subscribed, piped throughunsubscriber.takeUntil().transform()returns the previous#resultsynchronously.
Step 4 matters: for anything asynchronous the first rendered value is the stale one (or null on
the first ever call, which Angular renders as an empty string). The subscription writes #result
and calls cdr.markForCheck()
(fyz-translate.pipe.ts:81), which
schedules the host view for the next cycle — that is when the real text appears. For static
configs the observable emits synchronously, so #result is already correct before transform()
returns.
markForCheck() on the injected ChangeDetectorRef is what makes these pipes work inside OnPush
components: the pipe’s ChangeDetectorRef refers to the host view, so marking it dirty is enough.
Teardown
Section titled “Teardown”ngOnDestroy unsubscribes and nulls every field including #hasInput
(fyz-translate.pipe.ts:88). The
Unsubscriber itself is set to null, so a transform() call after destruction would throw rather
than silently resubscribe — pipes are not reused after destruction.
The fyzTansloco alias
Section titled “The fyzTansloco alias”FyzTanslocoPipe (note the missing r) subclasses FyzTranslocoPipe and registers under the
misspelled name fyzTansloco
(fyz-transloco.pipe.ts:90). It is
deprecated but kept, because template strings are resolved at runtime and deleting it would break
consumers without a compile error. Rationale:
ADR 0006.
Verified by
Section titled “Verified by”fyz-translate.pipe.spec.ts
is the executable version of this document. It asserts that five consecutive detectChanges()
cycles over deliberately churning inputs produce zero additional selectTranslate calls, and
that a real param change re-translates every pipe. Any change to the gate should keep both
assertions.
The /transloco-test route of the test app is the manual counterpart — it renders a churning config
next to a change-detection tick counter, so the counter climbing while the text stays put is the
memoization working.