Skip to content

The translate pipes

@flyze/lib-core-angular 1.0.0-alpha.34· latest

Two pipes render translations in a template:

PipeInputOptions
fyzTranslatea FyzTranslationConfig (or string){ params }
fyzTranslocoa 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.

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.

Each pipe stores its last input and returns the cached result unless the input actually changed:

// fyz-translate.pipe.ts:62
if (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:

  • fyzTranslate compares both the value and the options structurally — its value is an object.
  • fyzTransloco compares the key by reference (value === this._input, it is a string) and the options structurally (fyz-transloco.pipe.ts:46).
  1. The new input is stored.
  2. The Unsubscriber is reset() — torn down and re-armed — so the old translation stream must not keep writing into #result.
  3. selectTranslate() is subscribed, piped through unsubscriber.takeUntil().
  4. transform() returns the previous #result synchronously.

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.

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.

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.

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.