Skip to content

Commit a8c92f9

Browse files
committed
feat(core): make getRoot() public, add getParent + getRoot to ITranslateService
- getRoot() flipped from protected to public with TSDoc explaining isolated-subtree termination (parent === null at the boundary). - ITranslateService gains abstract getParent() and getRoot() — the interface now represents the full public API of the concrete class (getParent was previously public-on-class-but-missing-from-interface). - chain-walk-regression.spec adds two cases: getRoot() walks the chain to the root, and stops at an isolated subtree boundary. - README mock recipe removed — it belongs on the docs site, not in the README. Webpage docs cover isLoading + getParent/getRoot semantics.
1 parent bfd5943 commit a8c92f9

4 files changed

Lines changed: 61 additions & 20 deletions

File tree

README.md

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,6 @@ interfaces and explains how to develop custom plugins.
1717
In addition to that, a getting started tutorial is available here:
1818
[How to Translate Your Angular App with NGX-Translate](https://www.codeandweb.com/babeledit/tutorials/how-to-translate-your-angular-app-with-ngx-translate)
1919

20-
## Testing components that use TranslateService
21-
22-
When mocking `TranslateService` by structurally implementing `ITranslateService`,
23-
all abstract members must be provided. As of v18, that includes `isLoading`:
24-
25-
```ts
26-
import { signal } from "@angular/core";
27-
import { ITranslateService } from "@ngx-translate/core";
28-
29-
class MockTranslateService implements Partial<ITranslateService> {
30-
// ...your existing mocked methods...
31-
readonly isLoading = signal(false).asReadonly();
32-
}
33-
```
34-
35-
Apps that subclass `TranslateService` directly are unaffected — the concrete
36-
`isLoading` implementation lives on the class. Only mocks that implement the
37-
interface structurally need this addition.
38-
3920
## Support for older versions
4021

4122
We only support the current version with new updates and fixes.

projects/ngx-translate/src/lib/translate.service.interface.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,22 @@ export abstract class ITranslateService {
172172
* Returns the loaded translations for the given language.
173173
*/
174174
public abstract getTranslations(language: Language): InterpolatableTranslationObject;
175+
176+
/**
177+
* Returns the service this one inherits translations from, or `null` if
178+
* this is a root (a top-level service or an isolated subtree root).
179+
*
180+
* A `null` return means the service is the terminus of its translation
181+
* fallback chain — equivalent to "is this a root?".
182+
*/
183+
public abstract getParent(): ITranslateService | null;
184+
185+
/**
186+
* Returns the root of this service's hierarchy — the topmost service in
187+
* the `getParent()` chain. For an isolated subtree, returns the subtree's
188+
* root (since `getParent()` returns `null` at the isolation boundary).
189+
*
190+
* A root service returns itself.
191+
*/
192+
public abstract getRoot(): ITranslateService;
175193
}

projects/ngx-translate/src/lib/translate.service.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,15 @@ export class TranslateService implements ITranslateService {
114114
() => this.loadingTranslations.hasAny() || (this.parent?.isLoading() ?? false),
115115
);
116116

117-
protected getRoot(): TranslateService {
117+
/**
118+
* Returns the root of this service's hierarchy — the topmost service in
119+
* the `getParent()` chain. For an isolated subtree, returns the subtree's
120+
* root (since `parent === null` at the isolation boundary).
121+
*
122+
* A root service returns itself. Equivalent to walking `getParent()` until
123+
* it returns `null`, but provided as a convenience.
124+
*/
125+
public getRoot(): TranslateService {
118126
// eslint-disable-next-line @typescript-eslint/no-this-alias
119127
let svc: TranslateService = this;
120128
while (svc.parent) svc = svc.parent;

projects/ngx-translate/src/tests/chain-walk-regression.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,38 @@ describe("Chain walk regression (root + non-isolated child + grandchild)", () =>
8686
expect(childService.getParent()).toBe(rootService);
8787
expect(rootService.getParent()).toBeNull();
8888
});
89+
90+
it("getRoot() returns the topmost service in the chain", () => {
91+
const { rootService, childService, grandchildService } = setup();
92+
expect(rootService.getRoot()).toBe(rootService);
93+
expect(childService.getRoot()).toBe(rootService);
94+
expect(grandchildService.getRoot()).toBe(rootService);
95+
});
96+
97+
it("getRoot() stops at an isolated subtree boundary", () => {
98+
const rootInjector = Injector.create({
99+
providers: [
100+
provideTranslateService({
101+
loader: { provide: TranslateLoader, useValue: new FakeLoader({}) },
102+
}),
103+
],
104+
});
105+
const rootService = rootInjector.get(TranslateService);
106+
107+
// Nested provideTranslateService() creates an isolated subtree — its
108+
// root's getParent() returns null, so getRoot() returns itself.
109+
const isolatedInjector = Injector.create({
110+
providers: [
111+
provideTranslateService({
112+
loader: { provide: TranslateLoader, useValue: new FakeLoader({}) },
113+
}),
114+
],
115+
parent: rootInjector,
116+
});
117+
const isolatedService = isolatedInjector.get(TranslateService);
118+
119+
expect(isolatedService.getParent()).toBeNull();
120+
expect(isolatedService.getRoot()).toBe(isolatedService);
121+
expect(rootService.getRoot()).toBe(rootService);
122+
});
89123
});

0 commit comments

Comments
 (0)