diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index f95c1c71..a013923e 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -11,10 +11,10 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: 16 + node-version: 20 - run: npm install - run: npm run bootstrap - run: npm run lint @@ -24,10 +24,10 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: 16 + node-version: 20 registry-url: https://registry.npmjs.org/ - run: npm install - run: npm run bootstrap diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index a0cc770c..a1807427 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -14,12 +14,12 @@ jobs: strategy: matrix: - node-version: [16.x, 18.x, 20.x] + node-version: [18.x, 20.x, 22.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm install diff --git a/packages/angular/angular.json b/packages/angular/angular.json index 83f47bdb..2badb315 100644 --- a/packages/angular/angular.json +++ b/packages/angular/angular.json @@ -29,7 +29,8 @@ "polyfills": ["zone.js", "zone.js/testing"], "tsConfig": "projects/tx-native-angular-sdk/tsconfig.spec.json", "karmaConfig": "projects/tx-native-angular-sdk/karma.conf.js", - "codeCoverage": true + "codeCoverage": true, + "watch": false } }, "lint": { @@ -53,5 +54,8 @@ "@angular-eslint/schematics:library": { "setParserOptionsProject": true } + }, + "cli": { + "analytics": false } } diff --git a/packages/angular/package.json b/packages/angular/package.json index ceea263d..5db84098 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -31,36 +31,39 @@ "test": "ng test" }, "private": true, + "engines": { + "node": ">=18.19.0" + }, "dependencies": { - "@angular/animations": "~16.2.7", - "@angular/common": "~16.2.7", - "@angular/compiler": "~16.2.7", - "@angular/core": "~16.2.7", - "@angular/forms": "~16.2.7", - "@angular/platform-browser": "^16.2.7", - "@angular/platform-browser-dynamic": "^16.2.7", - "@angular/router": "~16.2.7", - "@transifex/native": "^7.1.6", - "rxjs": "~6.6.0", + "@angular/animations": "~19.2.19", + "@angular/common": "~19.2.19", + "@angular/compiler": "~19.2.19", + "@angular/core": "~19.2.19", + "@angular/forms": "~19.2.19", + "@angular/platform-browser": "~19.2.19", + "@angular/platform-browser-dynamic": "~19.2.19", + "@angular/router": "~19.2.19", + "@transifex/native": "^7.1.5", + "rxjs": "~7.8.0", "tslib": "^2.6.2", - "zone.js": "^0.13.3" + "zone.js": "~0.15.0" }, "devDependencies": { - "@angular-devkit/architect": "^0.1602.4", - "@angular-devkit/build-angular": "^16.2.4", - "@angular-eslint/builder": "^16.2.0", - "@angular-eslint/eslint-plugin": "^16.2.0", - "@angular-eslint/eslint-plugin-template": "^16.2.0", - "@angular-eslint/schematics": "16.2.0", - "@angular-eslint/template-parser": "^16.2.0", - "@angular/cli": "^16.2.4", - "@angular/compiler-cli": "^16.2.7", - "@types/jasmine": "~4.0.0", - "@types/node": "^16.18.70", - "@typescript-eslint/eslint-plugin": "^5.62.0", - "@typescript-eslint/parser": "^5.62.0", + "@angular-devkit/architect": "0.1902.19", + "@angular-devkit/build-angular": "~19.2.19", + "@angular-eslint/builder": "~19.2.0", + "@angular-eslint/eslint-plugin": "~19.2.0", + "@angular-eslint/eslint-plugin-template": "~19.2.0", + "@angular-eslint/schematics": "~19.2.0", + "@angular-eslint/template-parser": "~19.2.0", + "@angular/cli": "~19.2.0", + "@angular/compiler-cli": "~19.2.19", + "@types/jasmine": "~5.1.0", + "@types/node": "^20.0.0", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", "eslint": "^8.56.0", - "eslint-config-airbnb-typescript": "^16.2.0", + "eslint-config-airbnb-typescript": "^18.0.0", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-prefer-arrow": "^1.2.3", @@ -69,15 +72,15 @@ "eslint-plugin-rxjs-angular": "^2.0.1", "eslint-plugin-tsdoc": "^0.2.17", "eslint-plugin-unicorn": "^46.0.1", - "jasmine-core": "~4.3.0", + "jasmine-core": "~5.1.0", "jasmine-spec-reporter": "^7.0.0", "karma": "~6.4.1", "karma-chrome-launcher": "~3.1.1", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "^2.1.0", - "ng-packagr": "^16.2.3", - "ts-node": "~8.3.0", - "typescript": "~4.9.5" + "ng-packagr": "~19.2.0", + "ts-node": "~10.9.0", + "typescript": "~5.5.0" } } diff --git a/packages/angular/projects/tx-native-angular-sdk/README.md b/packages/angular/projects/tx-native-angular-sdk/README.md index 0cbed314..756160e8 100644 --- a/packages/angular/projects/tx-native-angular-sdk/README.md +++ b/packages/angular/projects/tx-native-angular-sdk/README.md @@ -61,16 +61,18 @@ If you are upgrading from the `1.x.x` version, please read this [migration guide * [translate Pipe](#translate-pipe) * [Language Picker Component](#language-picker-component) * [TX Instance Component](#tx-instance-component) + * [TxInstanceContext](#txinstancecontext) * [txLoadTranslations Directive](#txloadtranslations-directive) * [License](#license) # Requirements -Angular 16 is required. If you are using Angular 14 or 15, please use the `6.x.x` version of -Transifex Native related packages. If you are using Angular 12 or 13, please use the `5.x.x` version of -Transifex Native related packages. If you are using Angular 11, please use the `1.x.x` version of -Transifex Native related packages. Other Angular versions are not officially supported at the moment. +Angular 19 and Node.js >= 18.19 are required. If you are using Angular 16, please use the `7.1.x` version of +Transifex Native related packages. If you are using Angular 14 or 15, please use the `6.x.x` version. +If you are using Angular 12 or 13, please use the `5.x.x` version. +If you are using Angular 11, please use the `1.x.x` version. +Other Angular versions are not officially supported at the moment. # Installation @@ -85,9 +87,58 @@ npm install @transifex/native @transifex/angular --save ## Initialization In order to use the TX Native object globally, it is necessary to initialize -the library in the angular application bootstrap, in two locations: +the library in the Angular application bootstrap. -- NgModule initialization +All components, directives, and pipes in this SDK are **standalone**, so they can be imported directly where needed. A backward-compatible `TxNativeModule` is still provided for NgModule-based applications. + +**`TranslationService` and the `@T` decorator:** `@T` uses the same root `TranslationService` instance as Angular DI (including any extra instances registered for ``). The property is translated **when it is read** (lazy), not when the class is loaded. `TxNativeModule.forRoot()` registers `provideTxNativeEagerTranslationService()` so that service exists during app startup. If you use **`bootstrapApplication`** (no `forRoot()`), add `provideTxNativeEagerTranslationService()` to your root `providers` whenever you use `@T`, so the root service is created before the first `@T` access—especially if your first screen might not inject `TranslationService` yet. + +### Standalone applications + +Import the components you need directly in your standalone components or in the `imports` array of your app configuration. + +If you use the **`@T` property decorator**, register the eager provider in your application root (typically `main.ts`): + +```typescript +import { bootstrapApplication } from '@angular/platform-browser'; +import { provideTxNativeEagerTranslationService } from '@transifex/angular'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, { + providers: [provideTxNativeEagerTranslationService()], +}).catch((err) => console.error(err)); +``` + +Example root component: + +```typescript +import { Component } from '@angular/core'; +import { TranslationService, TComponent, LanguagePickerComponent } from '@transifex/angular'; + +@Component({ + standalone: true, + selector: 'app-root', + imports: [TComponent, LanguagePickerComponent], + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent { + constructor(private translationService: TranslationService) { + translationService.init({ + token: '----- here your TX Native token ------', + }); + } + + async ngOnInit() { + await this.translationService.getLanguages(); + await this.translationService.setCurrentLocale('el'); + } +} +``` + +### NgModule + +If your application uses NgModules, import `TxNativeModule.forRoot()` in your root module: ```typescript @NgModule({ @@ -105,13 +156,12 @@ the library in the angular application bootstrap, in two locations: // TX Native module declaration TxNativeModule.forRoot(), ], - providers: [, - ], + providers: [], bootstrap: [AppComponent] }) ``` -- Application Boostrap +Then initialize the SDK in your root component: ```typescript import { Component } from '@angular/core'; @@ -124,7 +174,6 @@ import { TranslationService } from '@transifex/angular'; }) export class AppComponent { constructor(private translationService: TranslationService) { - // TX Native library intialization translationService.init({ token: '----- here your TX Native token ------', }); @@ -340,6 +389,8 @@ export interface ITranslateParams { This is a decorator for using inside classes and components in order to have properties with the translation and used them in code and templates. +Use **`TxNativeModule.forRoot()`** or **`provideTxNativeEagerTranslationService()`** in standalone apps (see [Initialization](#initialization)) so the root `TranslationService` is available the first time an `@T` property is read. You do not need a separate `TranslationService` instance for the decorator; it shares the one from DI. + An example of use is the following: ```typescript @@ -477,12 +528,13 @@ such as `getLanguages`. ## TX Instance Component -Creates a new TX Native instance with the given configuration and adds it to the TX Native main instance. All the nested components will use the new instance in order to fetch the translations. This apply to components: +Creates a new TX Native instance with the given configuration and adds it to the TX Native main instance. All the nested components will use the new instance in order to fetch the translations. This applies to components: - T/UT - translate pipe +- Language Picker -Uses `Translation Service` internally to add the instance. +Uses `Translation Service` internally to add the instance and provides a scoped `TxInstanceContext` so that nested components automatically resolve the correct instance. See the [TxInstanceContext](#txinstancecontext) section for details on how this works. The html selector is `tx-instance`. @@ -511,7 +563,7 @@ Accepts properties: - `token`: The token for the new instance. -- `alias`: A string indetifier of the instance, should be unique. If the identifier already exists, the existing instance with the given alias is used, and no new instance is created. +- `alias`: A string identifier of the instance, should be unique. If the identifier already exists, the existing instance with the given alias is used, and no new instance is created. - `controlled`: If the new instance is controlled (locale) by the main TX Native instance. @@ -523,6 +575,63 @@ Exposes: - `instanceIsReady`: observable for listening the readiness of the new instance. +## `TxInstanceContext` + +`TxInstanceContext` is an injectable service that acts as a lightweight handle for the active TX Native instance alias. It is the mechanism that allows standalone components (`T`, `UT`, `LanguagePickerComponent`, `TranslatePipe`) to know which instance they should use for translations, without requiring a direct reference to the `TXInstanceComponent`. + +### How it works + +A root-level `TxInstanceContext` is provided application-wide (via `providedIn: 'root'`). Its `alias` defaults to an empty string, which tells components to use the main TX Native instance. + +When you wrap components inside a `` element, that component provides its own `TxInstanceContext` at the component level. Angular's dependency injection will resolve the closest `TxInstanceContext` in the injector hierarchy, so nested components automatically see the alias of the wrapping instance. + +``` +AppModule / root injector + └─ TxInstanceContext (alias = '', main instance) + │ + ├─ + │ + └─ + └─ TxInstanceContext (alias = 'homepage', scoped) + │ + ├─ + └─ {{ 'A string' | translate }} +``` + +### Using TxInstanceContext directly + +In most cases you do not need to interact with `TxInstanceContext` yourself — wrapping components in `` handles everything automatically. However, if you need to read or react to instance readiness programmatically, you can inject it: + +```typescript +import { Component } from '@angular/core'; +import { TxInstanceContext, TranslationService } from '@transifex/angular'; + +@Component({ + standalone: true, + selector: 'my-custom-component', + template: `...` +}) +export class MyCustomComponent { + constructor( + private translationService: TranslationService, + private txContext: TxInstanceContext, + ) {} + + getTranslation(str: string): string { + const instance = this.translationService.getInstance(this.txContext.alias); + return instance.translate(str); + } +} +``` + +### API + +| Property / Method | Type | Description | +|-------------------------|--------------------------|-------------------------------------------------------------------------------------------------------| +| `alias` | `string` | The alias of the active TX Native instance. Empty string means the main instance. | +| `instanceIsReady` | `Observable` | Emits when the associated instance has finished initializing and fetching translations. | +| `notifyInstanceReady()` | `(ready: boolean) => void` | Called internally by `TXInstanceComponent` to signal readiness. Not typically called by application code. | + ## `txLoadTranslations` Directive This directive can be used within any html or angular tag in order to force a group of translations to be fetched, using a list of tags to retrieve the translations that match. diff --git a/packages/angular/projects/tx-native-angular-sdk/karma.conf.js b/packages/angular/projects/tx-native-angular-sdk/karma.conf.js index 6a1db930..9d150439 100644 --- a/packages/angular/projects/tx-native-angular-sdk/karma.conf.js +++ b/packages/angular/projects/tx-native-angular-sdk/karma.conf.js @@ -16,7 +16,7 @@ module.exports = function (config) { jasmine: { failSpecWithNoExpectations: true, }, - clearContext: false + clearContext: true, // Suppresses "full page reload" false positive in Chrome 128+ }, jasmineHtmlReporter: { suppressAll: true @@ -30,7 +30,7 @@ module.exports = function (config) { { type: 'text-summary' } ] }, - reporters: ['progress', 'kjhtml'], + reporters: ['progress', 'coverage'], port: 9876, colors: true, logLevel: config.LOG_INFO, @@ -44,10 +44,11 @@ module.exports = function (config) { }, ChromeHeadlessNoSandbox: { base: 'ChromeHeadless', - flags: ['--no-sandbox'], + flags: ['--no-sandbox', '--headless=old'], }, }, - singleRun: true, // Moved this property outside the customLaunchers object - restartOnFileChange: true, // Moved this property outside the customLaunchers object + singleRun: true, + restartOnFileChange: false, + browserDisconnectTolerance: 2, }); }; diff --git a/packages/angular/projects/tx-native-angular-sdk/package.json b/packages/angular/projects/tx-native-angular-sdk/package.json index 6c9a1b25..f55fafe0 100644 --- a/packages/angular/projects/tx-native-angular-sdk/package.json +++ b/packages/angular/projects/tx-native-angular-sdk/package.json @@ -17,8 +17,8 @@ "repository": "git://github.com/transifex/transifex-javascript.git", "license": "Apache-2.0", "peerDependencies": { - "@angular/common": "^16.0.0", - "@angular/core": " ^16.0.0", + "@angular/common": "^19.0.0", + "@angular/core": "^19.0.0", "@transifex/native": "^7.0.1" } } diff --git a/packages/angular/projects/tx-native-angular-sdk/src/lib/T.component.ts b/packages/angular/projects/tx-native-angular-sdk/src/lib/T.component.ts index c98b3f2c..8bd78a2a 100644 --- a/packages/angular/projects/tx-native-angular-sdk/src/lib/T.component.ts +++ b/packages/angular/projects/tx-native-angular-sdk/src/lib/T.component.ts @@ -1,56 +1,45 @@ import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { Observable, Subscription } from 'rxjs'; -import { TXInstanceComponent } from './instance.component'; +import { TxInstanceContext } from './tx-instance-context'; import { ITranslateParams } from './interfaces'; import { TranslationService } from './translation.service'; /** - * A translation component - **/ + * A translation component. + * + * Uses DomSanitizer directly (injected via constructor) instead of SafeHtmlPipe. + * SafeHtmlPipe uses Angular DI internally; when the pipe is resolved as part of a + * template it can be called in an invalid injection context on Angular 19+, triggering + * NG0203. Inlining the sanitizer call here avoids that entirely. + */ @Component({ + standalone: true, selector: 'T', + imports: [CommonModule], template: ` {{ translatedStr }} - + `, styles: [], }) - - export class TComponent implements OnInit, OnDestroy, OnChanges { - @Input() - str = ''; - - @Input() - key?: string = ''; - - @Input() - context?: string = ''; - - @Input() - comment?: string = ''; - - @Input() - charlimit?: number = 0; - - @Input() - tags?: string = ''; - - @Input() - escapeVars?: boolean = false; - - @Input() - inline?: boolean = false; - - @Input() - sanitize?: boolean = false; + @Input() str = ''; + @Input() key?: string = ''; + @Input() context?: string = ''; + @Input() comment?: string = ''; + @Input() charlimit?: number = 0; + @Input() tags?: string = ''; + @Input() escapeVars?: boolean = false; + @Input() inline?: boolean = false; + @Input() sanitize?: boolean = false; @Input() get vars(): Record { return this.actualVars; } - set vars(v: Record) { this.actualVars = { ...v }; } @@ -70,66 +59,52 @@ export class TComponent implements OnInit, OnDestroy, OnChanges { translatedStr = ''; - // Observable for detecting locale changes + get sanitizedStr(): SafeHtml { + return this.sanitizer.bypassSecurityTrustHtml(this.translatedStr); + } + get localeChanged(): Observable { return this.translationService.localeChanged; } onLocaleChange: Subscription | undefined; - onTranslationsFetch: Subscription | undefined; private actualVars: Record = {}; - constructor(protected translationService: TranslationService, - protected instance: TXInstanceComponent) { - this.onLocaleChange = this.localeChanged.subscribe( - () => { - this.translate(); - }, - ); - this.onTranslationsFetch = - this.translationService.translationsFetched.subscribe( - () => { - this.translate(); - }, - ); + constructor( + protected translationService: TranslationService, + protected instance: TxInstanceContext, + protected sanitizer: DomSanitizer, + ) { + this.onLocaleChange = this.localeChanged.subscribe(() => { + this.translate(); + }); + this.onTranslationsFetch = this.translationService.translationsFetched.subscribe(() => { + this.translate(); + }); } - /** - * Component initialization - */ ngOnInit() { this.translate(); } - /** - * Component destruction - */ ngOnDestroy() { - if (this.onLocaleChange !== undefined) { - this.onLocaleChange.unsubscribe(); - this.onLocaleChange = undefined; - } - - if (this.onTranslationsFetch !== undefined) { - this.onTranslationsFetch.unsubscribe(); - this.onTranslationsFetch = undefined; - } + this.onLocaleChange?.unsubscribe(); + this.onLocaleChange = undefined; + this.onTranslationsFetch?.unsubscribe(); + this.onTranslationsFetch = undefined; } - /** - * Input parameters change detector - */ ngOnChanges() { this.translate(); } - /** - * Translate a string using the translation service - */ translate() { - this.translatedStr = this.translationService.translate(this.str, - { ...this.translateParams, ...this.vars }, this.instance.alias || ''); + this.translatedStr = this.translationService.translate( + this.str, + { ...this.translateParams, ...this.vars }, + this.instance.alias || '', + ); } } diff --git a/packages/angular/projects/tx-native-angular-sdk/src/lib/T.decorator.ts b/packages/angular/projects/tx-native-angular-sdk/src/lib/T.decorator.ts index 444764de..412d3518 100644 --- a/packages/angular/projects/tx-native-angular-sdk/src/lib/T.decorator.ts +++ b/packages/angular/projects/tx-native-angular-sdk/src/lib/T.decorator.ts @@ -1,31 +1,38 @@ -import { Injector } from '@angular/core'; - -import { TranslationService } from './translation.service'; -import {ITXInstanceConfiguration} from "./interfaces"; +import { getTranslationServiceForTDecorator } from './t-decorator-translation-bridge'; +import { ITXInstanceConfiguration } from './interfaces'; /** - * Decorator for using transparently the translation service as a property + * Resolves the app’s root {@link TranslationService} on each @T property read (lazy). + * + * Resolving here (not when the decorator runs on the class) avoids requiring a service + * before `TestBed.inject` / bootstrap. Cannot use `Injector.create().get(TranslationService)` + * in decorator setup (NG0203 on Angular 19+). The root service self-registers when Angular + * constructs it (`useFactory`) and again in {@link TranslationService.init}. */ +function getTranslationService() { + return getTranslationServiceForTDecorator(); +} -export const T = (str: string, params?: Record, - instanceConfig?: ITXInstanceConfiguration) => (target: any, key: string) => { - const injector = Injector.create( - { - providers: [ - { provide: TranslationService, useClass: TranslationService }, - ], - }, - ); - const translationService = injector.get(TranslationService); - +/** + * Decorator for transparently using the translation service as a component property. + */ +export const T = ( + str: string, + params?: Record, + instanceConfig?: ITXInstanceConfiguration, +) => (target: object, key: string) => { Object.defineProperty(target, key, { configurable: false, get: () => { + const translationService = getTranslationService(); if (instanceConfig) { translationService.addInstance(instanceConfig); } - return translationService.translate(str, { ...params }, - instanceConfig && instanceConfig.alias || ''); + return translationService.translate( + str, + { ...params }, + (instanceConfig && instanceConfig.alias) || '', + ); }, }); }; diff --git a/packages/angular/projects/tx-native-angular-sdk/src/lib/TXNative.module.ts b/packages/angular/projects/tx-native-angular-sdk/src/lib/TXNative.module.ts index 7461d52a..ec07b236 100644 --- a/packages/angular/projects/tx-native-angular-sdk/src/lib/TXNative.module.ts +++ b/packages/angular/projects/tx-native-angular-sdk/src/lib/TXNative.module.ts @@ -5,14 +5,15 @@ import { LanguagePickerComponent } from './language-picker.component'; import { TComponent } from './T.component'; import { UTComponent } from './UT.component'; import { SafeHtmlPipe } from './safe-html.pipe'; -import { TranslationService } from './translation.service'; import { TranslatePipe } from './translate.pipe'; import { TXInstanceComponent } from './instance.component'; import { LoadTranslationsDirective } from './load-translations.directive'; +import { provideTxNativeEagerTranslationService } from './tx-native.providers'; @NgModule({ - declarations: [ + imports: [ + CommonModule, TComponent, UTComponent, LanguagePickerComponent, @@ -21,7 +22,6 @@ import { LoadTranslationsDirective } from './load-translations.directive'; TXInstanceComponent, LoadTranslationsDirective, ], - imports: [CommonModule], exports: [ TComponent, UTComponent, @@ -31,20 +31,16 @@ import { LoadTranslationsDirective } from './load-translations.directive'; TXInstanceComponent, LoadTranslationsDirective, ], - providers: [ - TXInstanceComponent, - ], + // TxInstanceContext is providedIn: 'root' — no registration needed here. + // TranslationService is providedIn: 'root'. forRoot() adds provideAppInitializer + // (see provideTxNativeEagerTranslationService) so @T works before any injection. + providers: [], }) export class TxNativeModule { - /** - * Use this method in your root module to provide the TranslationService - */ static forRoot(): ModuleWithProviders { return { ngModule: TxNativeModule, - providers: [ - TranslationService, - ], + providers: [provideTxNativeEagerTranslationService()], }; } } diff --git a/packages/angular/projects/tx-native-angular-sdk/src/lib/UT.component.ts b/packages/angular/projects/tx-native-angular-sdk/src/lib/UT.component.ts index b1697a52..fc06d0c7 100644 --- a/packages/angular/projects/tx-native-angular-sdk/src/lib/UT.component.ts +++ b/packages/angular/projects/tx-native-angular-sdk/src/lib/UT.component.ts @@ -1,25 +1,29 @@ import { Component, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DomSanitizer } from '@angular/platform-browser'; -import { TXInstanceComponent } from './instance.component'; +import { TxInstanceContext } from './tx-instance-context'; import { TComponent } from './T.component'; import { TranslationService } from './translation.service'; @Component({ + standalone: true, selector: 'UT', + imports: [CommonModule], template: ` -
- +
+ `, styles: [], encapsulation: ViewEncapsulation.None, }) - export class UTComponent extends TComponent { constructor( - translationService: TranslationService, - protected override instance: TXInstanceComponent, + translationService: TranslationService, + protected override instance: TxInstanceContext, + sanitizer: DomSanitizer, ) { - super(translationService, instance); + super(translationService, instance, sanitizer); this.escapeVars = true; } } diff --git a/packages/angular/projects/tx-native-angular-sdk/src/lib/instance.component.ts b/packages/angular/projects/tx-native-angular-sdk/src/lib/instance.component.ts index 59d302bb..3a870e95 100644 --- a/packages/angular/projects/tx-native-angular-sdk/src/lib/instance.component.ts +++ b/packages/angular/projects/tx-native-angular-sdk/src/lib/instance.component.ts @@ -1,54 +1,63 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { Observable, ReplaySubject } from 'rxjs'; +import { Observable } from 'rxjs'; import { TranslationService } from './translation.service'; -import {TxNative} from "@transifex/native"; - +import { TxInstanceContext } from './tx-instance-context'; +import { TxNative } from '@transifex/native'; + +/** + * Component that sets up an alternative TX Native instance. + * + * Provides its own {@link TxInstanceContext} so components nested inside a + * `` element (T, UT, tx-language-picker) see this instance's alias + * rather than the root-level one. + */ @Component({ + standalone: true, selector: 'tx-instance', - template: ` - - `, + template: ``, + providers: [TxInstanceContext], }) - export class TXInstanceComponent implements OnInit { @Input() alias = ''; - @Input() token = ''; - @Input() controlled = true; @Output() instanceReady: EventEmitter = new EventEmitter(); - // Observables for detecting instance readiness get instanceIsReady(): Observable { - return this.instanceReadySubject; + return this.txContext.instanceIsReady; } - private instanceReadySubject = new ReplaySubject(0); + private nativeInstance?: TxNative; - // The instance - private instance?: TxNative; + constructor( + private translationService: TranslationService, + private txContext: TxInstanceContext, + ) {} - constructor(private translationService: TranslationService) {} + async ngOnInit(): Promise { + this.txContext.alias = this.alias; - async ngOnInit(): Promise { if (!this.token || !this.alias) { this.instanceReady.emit(false); - this.instanceReadySubject.next(false); + this.txContext.notifyInstanceReady(false); + return; } + const instanceCreated = await this.translationService.addInstance({ token: this.token, alias: this.alias, controlled: this.controlled, }); - this.instance = this.translationService.getInstance(this.alias); - if (instanceCreated && this.instance) { + this.nativeInstance = this.translationService.getInstance(this.alias); + + if (instanceCreated && this.nativeInstance) { this.instanceReady.emit(true); - this.instanceReadySubject.next(true); + this.txContext.notifyInstanceReady(true); } else { this.instanceReady.emit(false); - this.instanceReadySubject.next(false); + this.txContext.notifyInstanceReady(false); } } } diff --git a/packages/angular/projects/tx-native-angular-sdk/src/lib/language-picker.component.ts b/packages/angular/projects/tx-native-angular-sdk/src/lib/language-picker.component.ts index 2a6148b0..cf314607 100644 --- a/packages/angular/projects/tx-native-angular-sdk/src/lib/language-picker.component.ts +++ b/packages/angular/projects/tx-native-angular-sdk/src/lib/language-picker.component.ts @@ -1,15 +1,18 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; import { Observable, Subscription } from 'rxjs'; -import { TXInstanceComponent } from './instance.component'; +import { TxInstanceContext } from './tx-instance-context'; import { ILanguage } from './interfaces'; import { TranslationService } from './translation.service'; /** - * A language picker component with the available languages already populated + * A language picker component with the available languages already populated. */ @Component({ + standalone: true, selector: 'tx-language-picker', + imports: [CommonModule], template: `