├── src ├── assets │ ├── .gitkeep │ └── localization │ │ ├── zh-CN │ │ └── translations.json │ │ └── en │ │ └── translations.json ├── favicon.ico ├── app │ ├── feature2 │ │ ├── feature2.component.css │ │ ├── feature2.component.html │ │ └── feature2.component.ts │ ├── feature1 │ │ ├── widget │ │ │ ├── widget.component.scss │ │ │ ├── widget.component.html │ │ │ └── widget.component.ts │ │ ├── feature1.component.scss │ │ ├── feature1.routing.module.ts │ │ ├── feature1.module.ts │ │ ├── feature1.component.html │ │ └── feature1.component.ts │ ├── app.component.scss │ ├── app.service.ts │ ├── localization │ │ ├── zh-CN │ │ │ └── translations.ts │ │ └── en │ │ │ └── translations.ts │ ├── app.routes.ts │ ├── app.component.spec.ts │ ├── g-tag.service.ts │ ├── app.component.html │ ├── app.component.ts │ └── app.config.ts ├── styles.scss ├── main.ts └── index.html ├── docs ├── styles-5INURTSO.css ├── prerendered-routes.json ├── favicon.ico ├── assets │ └── localization │ │ ├── zh-CN │ │ └── translations.json │ │ └── en │ │ └── translations.json ├── 404.html ├── index.html ├── chunk-FQRN3WBV.js ├── main-HLG3KWLN.js └── chunk-AT77VTST.js ├── projects └── nb-trans │ ├── src │ ├── lib │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── test │ │ │ │ ├── nb-trans-tools.service.spec.ts │ │ │ │ └── nb-trans.service.spec.ts │ │ │ ├── nb-trans-tools.service.ts │ │ │ └── nb-trans.service.ts │ │ ├── models │ │ │ ├── nb-translation.interface.ts │ │ │ ├── nb-trans-change-lang.interface.ts │ │ │ ├── nb-trans-params.interface.ts │ │ │ ├── nb-trans-loader.interface.ts │ │ │ ├── index.ts │ │ │ ├── nb-trans-options.interface.ts │ │ │ └── nb-trans-sentence-part.interface.ts │ │ ├── testing │ │ │ ├── index.ts │ │ │ ├── data │ │ │ │ ├── index.ts │ │ │ │ ├── translationSync.test-data.ts │ │ │ │ └── handleSentenceWithParams.test-data.ts │ │ │ ├── localization │ │ │ │ ├── zh-CN │ │ │ │ │ └── translations.ts │ │ │ │ └── en │ │ │ │ │ └── translations.ts │ │ │ ├── nb-trans-testing.module.ts │ │ │ └── trans-loader.ts │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── nb-trans-subcontent │ │ │ │ ├── nb-trans-subcontent.component.ts │ │ │ │ └── test │ │ │ │ │ └── nb-trans-subcontent.component.spec.ts │ │ │ └── nb-trans │ │ │ │ ├── nb-trans.component.html │ │ │ │ ├── nb-trans.component.ts │ │ │ │ └── test │ │ │ │ ├── nb-trans2.component.spec.ts │ │ │ │ └── nb-trans.component.spec.ts │ │ ├── pipes │ │ │ ├── index.ts │ │ │ ├── nb-trans-content.pipe.ts │ │ │ ├── nb-sentence-item-type.pipe.ts │ │ │ ├── nb-trans.pipe.ts │ │ │ └── test │ │ │ │ ├── nb-trans-content.pipe.spec.ts │ │ │ │ ├── nb-sentence-item-type.pipe.spec.ts │ │ │ │ └── nb-trans.pipe.spec.ts │ │ ├── constants │ │ │ ├── nb-trans-default-lang.token.ts │ │ │ ├── nb-trans-loader.token.ts │ │ │ ├── nb-trans-sentence-item.enum.ts │ │ │ ├── nb-trans-param-key-invalid-warning.token.ts │ │ │ ├── nb-trans-max-retry.token.ts │ │ │ ├── index.ts │ │ │ ├── nb-param-key-regexp.ts │ │ │ └── nb-trans-lang.enum.ts │ │ └── nb-trans.module.ts │ ├── public-api.ts │ └── test.ts │ ├── ng-package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── package.json │ ├── karma.conf.js │ └── README.CN.md ├── tsconfig.spec.json ├── tsconfig.app.json ├── .editorconfig ├── .prettierrc.json ├── .gitignore ├── .prettierignore ├── LICENSE ├── tsconfig.json ├── .eslintrc.json ├── README.CN.md ├── package.json ├── README.md ├── angular.json ├── CHANGELOG.CN.md └── CHANGELOG.md /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/styles-5INURTSO.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/prerendered-routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": {} 3 | } -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigBear713/nb-trans/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigBear713/nb-trans/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/app/feature2/feature2.component.css: -------------------------------------------------------------------------------- 1 | a { 2 | color: aqua; 3 | cursor: pointer; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/feature1/widget/widget.component.scss: -------------------------------------------------------------------------------- 1 | a { 2 | color: aqua; 3 | cursor: pointer; 4 | } 5 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nb-trans.service'; 2 | export * from './nb-trans-tools.service'; 3 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/models/nb-translation.interface.ts: -------------------------------------------------------------------------------- 1 | export interface INbTranslation { 2 | [key: string]: INbTranslation | string; 3 | } 4 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/testing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data'; 2 | export * from './trans-loader'; 3 | export * from './nb-trans-testing.module'; 4 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/testing/data/index.ts: -------------------------------------------------------------------------------- 1 | export * from './handleSentenceWithParams.test-data'; 2 | export * from './translationSync.test-data'; 3 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/models/nb-trans-change-lang.interface.ts: -------------------------------------------------------------------------------- 1 | export interface INbTransChangeLang { 2 | result: boolean; 3 | curLang: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/feature1/feature1.component.scss: -------------------------------------------------------------------------------- 1 | a { 2 | color: blue; 3 | cursor: pointer; 4 | 5 | &:hover { 6 | text-decoration: underline; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nb-trans/nb-trans.component'; 2 | export * from './nb-trans-subcontent/nb-trans-subcontent.component'; 3 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/pipes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nb-trans.pipe'; 2 | export * from './nb-trans-content.pipe'; 3 | export * from './nb-sentence-item-type.pipe'; 4 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | h2 a { 2 | margin: 0 8px; 3 | } 4 | 5 | .actions { 6 | position: sticky; 7 | top: 0; 8 | background-color: #fff; 9 | } 10 | 11 | a { 12 | margin: 0 5px; 13 | } 14 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/constants/nb-trans-default-lang.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export const NB_TRANS_DEFAULT_LANG = new InjectionToken('nb-trans-default-lang'); 4 | -------------------------------------------------------------------------------- /src/app/feature1/widget/widget.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err)); 6 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/constants/nb-trans-loader.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { INbTranslation } from '../models'; 3 | 4 | export const NB_TRANS_LOADER = new InjectionToken<{ [key: string]: INbTranslation }>( 5 | 'nb-trans-loader' 6 | ); 7 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/constants/nb-trans-sentence-item.enum.ts: -------------------------------------------------------------------------------- 1 | export enum NbTransSentenceItem { 2 | STR, 3 | COMP, 4 | MULTI_COMP, 5 | } 6 | /** 7 | * @deprecated use "NbTransSentenceItem" please 8 | */ 9 | export const NbTransSentenceItemEnum = NbTransSentenceItem; 10 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/models/nb-trans-params.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The naming rules about param key: 3 | * 1. Consists of letters, numbers, _, and $ 4 | * 2. The number can't be the first character 5 | */ 6 | export interface INbTransParams { 7 | [key: string]: string; 8 | } 9 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/testing/localization/zh-CN/translations.ts: -------------------------------------------------------------------------------- 1 | export const trans = { 2 | title: '标题 ', 3 | content: { 4 | helloWorld: '你好,世界', 5 | }, 6 | helloWorld: '你好,世界!', 7 | component: '<0>组件', 8 | complexComponent: '<0>组件0<1>组件1', 9 | }; 10 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/constants/nb-trans-param-key-invalid-warning.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export const NB_TRANS_PARAM_KEY_INVALID_WARNING = new InjectionToken( 4 | 'Whether to print the warning info when the param key is invalid?' 5 | ); 6 | -------------------------------------------------------------------------------- /projects/nb-trans/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/nb-trans", 4 | "allowedNonPeerDependencies": ["@bigbear713/nb-common", "lodash-es", "tslib"], 5 | "lib": { 6 | "entryFile": "src/public-api.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": ["jasmine"] 7 | }, 8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/models/nb-trans-loader.interface.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { INbTranslation } from './nb-translation.interface'; 3 | 4 | export interface INbTransLoader { 5 | [langKey: string]: INbTranslation | (() => Observable | Promise); 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/testing/localization/en/translations.ts: -------------------------------------------------------------------------------- 1 | export const trans = { 2 | title: 'title ', 3 | content: { 4 | helloWorld: 'hello world', 5 | }, 6 | helloWorld: 'hello world!', 7 | component: '<0>component', 8 | complexComponent: '<0>component0<1>component1', 9 | }; 10 | -------------------------------------------------------------------------------- /projects/nb-trans/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/constants/nb-trans-max-retry.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export const NB_TRANS_MAX_RETRY = new InjectionToken('nb-trans-max-retry'); 4 | /** 5 | * @deprecated use "NB_TRANS_MAX_RETRY" please 6 | */ 7 | export const NB_TRANS_MAX_RETRY_TOKEN = NB_TRANS_MAX_RETRY; 8 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nb-trans-default-lang.token'; 2 | export * from './nb-trans-lang.enum'; 3 | export * from './nb-trans-loader.token'; 4 | export * from './nb-trans-max-retry.token'; 5 | export * from './nb-trans-sentence-item.enum'; 6 | export * from './nb-trans-param-key-invalid-warning.token'; 7 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nb-trans-change-lang.interface'; 2 | export * from './nb-trans-loader.interface'; 3 | export * from './nb-trans-options.interface'; 4 | export * from './nb-trans-params.interface'; 5 | export * from './nb-trans-sentence-part.interface'; 6 | export * from './nb-translation.interface'; 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/models/nb-trans-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { INbTransParams } from './nb-trans-params.interface'; 2 | 3 | export interface INbTransOptions { 4 | prefix?: string; 5 | params?: INbTransParams; 6 | // return the trans key when the trans content is empty, 7 | // default is true 8 | returnKeyWhenEmpty?: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /projects/nb-trans/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": ["jasmine"] 7 | }, 8 | "files": ["src/test.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /projects/nb-trans/src/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/constants'; 2 | export * from './lib/models'; 3 | 4 | export * from './lib/nb-trans.module'; 5 | export * from './lib/testing/nb-trans-testing.module'; 6 | 7 | export * from './lib/components'; 8 | 9 | export * from './lib/pipes/nb-trans.pipe'; 10 | 11 | export * from './lib/services/nb-trans.service'; 12 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/testing/nb-trans-testing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NbTransModule } from '../nb-trans.module'; 3 | import { NbTransService, NbTransToolsService } from '../services'; 4 | 5 | @NgModule({ 6 | imports: [NbTransModule], 7 | providers: [NbTransService, NbTransToolsService], 8 | exports: [NbTransModule], 9 | }) 10 | export class NbTransTestingModule {} 11 | -------------------------------------------------------------------------------- /projects/nb-trans/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "exclude": ["src/test.ts", "**/*.spec.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "semi": true, 6 | "bracketSpacing": true, 7 | "arrowParens": "avoid", 8 | "trailingComma": "es5", 9 | "bracketSameLine": true, 10 | "printWidth": 100, 11 | "endOfLine": "auto", 12 | "overrides": [ 13 | { 14 | "files": "*.html", 15 | "options": { 16 | "parser": "angular" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/app/app.service.ts: -------------------------------------------------------------------------------- 1 | import { NbTransService } from 'nb-trans'; 2 | 3 | import { Injectable } from '@angular/core'; 4 | import { lastValueFrom } from 'rxjs'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class AppService { 10 | constructor(private transService: NbTransService) {} 11 | 12 | resolve(): Promise { 13 | return lastValueFrom(this.transService.subscribeLoadDefaultOver()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/constants/nb-param-key-regexp.ts: -------------------------------------------------------------------------------- 1 | // The param key's naming rules consistent with JS variable names 2 | // 1. Consists of letters, numbers, _, and $ 3 | // 2. The number can't be the first character 4 | export const nbParamKeyRegExpRules = '[$_a-zA-Z]+[\\w$]*'; 5 | export const nbParamKeyRegExp = new RegExp(nbParamKeyRegExpRules, 'g'); 6 | export const nbParamKeyRegExp2Split = new RegExp(`({{\\s*${nbParamKeyRegExpRules}\\s*}})`, 'g'); 7 | -------------------------------------------------------------------------------- /src/app/feature1/feature1.routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { Feature1Component } from './feature1.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: Feature1Component, 10 | }, 11 | ]; 12 | 13 | @NgModule({ 14 | imports: [RouterModule.forChild(routes)], 15 | exports: [RouterModule], 16 | }) 17 | export class Feature1RoutingModule {} 18 | -------------------------------------------------------------------------------- /src/app/localization/zh-CN/translations.ts: -------------------------------------------------------------------------------- 1 | export const trans = { 2 | title: '标题', 3 | content: { 4 | helloWorld: '你好,世界', 5 | contentWithParams: 6 | '这是一个带有参数的句子。参数: {{params1}} - {{params2}} - {{params3}} - {{params2}}', 7 | complexContent: 8 | '这是一个句子 <0>组件1. <0> {{params1}} - {{params2}} - {{params3}} - {{params2}}.以上这些是参数。.<1><0>组件2 abc.<1>测试 <0>这些是参数: {{params1}} - {{params2}} - {{params3}} - {{params2}}.<2>这是参数3222', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/nb-trans.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NbTrans2Component, NbTransComponent, NbTransSubcontentComponent } from './components'; 3 | import { NbTransPipe } from './pipes'; 4 | 5 | const COMPONENTS = [NbTransComponent, NbTrans2Component, NbTransSubcontentComponent]; 6 | 7 | const PIPES = [NbTransPipe]; 8 | 9 | @NgModule({ 10 | imports: [...COMPONENTS, ...PIPES], 11 | exports: [...COMPONENTS, ...PIPES], 12 | }) 13 | export class NbTransModule {} 14 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/models/nb-trans-sentence-part.interface.ts: -------------------------------------------------------------------------------- 1 | export interface INbTransSentenceCompPart { 2 | // the component index, 3 | // will be used to get component instance from components properties 4 | index: number; 5 | // the content which will be rendered in html 6 | content: string; 7 | // the sentenceList from the trans content string of the current component 8 | list?: INbTransSentencePart[]; 9 | } 10 | 11 | export type INbTransSentencePart = string | INbTransSentenceCompPart; 12 | -------------------------------------------------------------------------------- /src/app/feature1/feature1.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { Feature1Component } from './feature1.component'; 5 | import { Feature1RoutingModule } from './feature1.routing.module'; 6 | import { WidgetComponent } from './widget/widget.component'; 7 | import { NbTransModule } from 'nb-trans'; 8 | 9 | @NgModule({ 10 | imports: [CommonModule, NbTransModule, Feature1RoutingModule], 11 | declarations: [Feature1Component, WidgetComponent], 12 | }) 13 | export class Feature1Module {} 14 | -------------------------------------------------------------------------------- /src/app/localization/en/translations.ts: -------------------------------------------------------------------------------- 1 | export const trans = { 2 | title: 'title', 3 | content: { 4 | helloWorld: 'hello world', 5 | contentWithParams: 6 | 'This is a sentence. params: {{params1}} - {{params2}} - {{params3}} - {{params2}}', 7 | complexContent: 8 | 'This is a sentence. <0>component1. <0>This is params: {{params1}} - {{params2}} - {{params3}} - {{params2}}.<1><0>component2 abc.<1>test <0>this is params: {{params1}} - {{params2}} - {{params3}} - {{params2}}.<2>this is component3222', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/pipes/nb-trans-content.pipe.ts: -------------------------------------------------------------------------------- 1 | import { inject, Pipe, PipeTransform } from '@angular/core'; 2 | import { INbTransParams } from '../models'; 3 | import { NbTransToolsService } from '../services'; 4 | 5 | @Pipe({ standalone: true, name: 'nbTransContent' }) 6 | export class NbTransContentPipe implements PipeTransform { 7 | private transToolsService: NbTransToolsService = inject(NbTransToolsService); 8 | 9 | transform(trans: string, params?: INbTransParams): string { 10 | return this.transToolsService.handleSentenceWithParams(trans, params); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /projects/nb-trans/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting, 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | // First, initialize the Angular testing environment. 12 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { 13 | teardown: { destroyAfterEach: true }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { AppService } from './app.service'; 3 | 4 | export const routes: Routes = [ 5 | { 6 | path: '', 7 | resolve: { 8 | defalutLangLoadOver: AppService, 9 | }, 10 | children: [ 11 | { 12 | path: '', 13 | loadChildren: () => import('./feature1/feature1.module').then(m => m.Feature1Module), 14 | }, 15 | { 16 | path: 'standalone', 17 | loadChildren: () => import('./feature2/feature2.component').then(m => m.routes), 18 | }, 19 | ], 20 | }, 21 | ]; 22 | -------------------------------------------------------------------------------- /docs/assets/localization/zh-CN/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "标题", 3 | "content": { 4 | "helloWorld": "你好,世界", 5 | "contentWithParams": "这是一个带有参数的句子。参数: {{params1}} - {{params2}} - {{params3}} - {{params2}}", 6 | "complexContent": "这是一个句子 <0>组件1. <0> {{params1}} - {{params2}} - {{params3}} - {{params2}}.以上这些是参数。.<1><0>组件2 abc.<1>测试 <0>这些是参数: {{params1}} - {{params2}} - {{params3}} - {{params2}}.<2>这是参数3222" 7 | }, 8 | "contentWithParams": "这是一个带有参数的句子 1111。参数: {{params1}} - {{params2}} - {{params3}} - {{params2}}", 9 | "currBrowserLang": "当前浏览器的语言" 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/localization/zh-CN/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "标题", 3 | "content": { 4 | "helloWorld": "你好,世界", 5 | "contentWithParams": "这是一个带有参数的句子。参数: {{params1}} - {{params2}} - {{params3}} - {{params2}}", 6 | "complexContent": "这是一个句子 <0>组件1. <0> {{params1}} - {{params2}} - {{params3}} - {{params2}}.以上这些是参数。.<1><0>组件2 abc.<1>测试 <0>这些是参数: {{params1}} - {{params2}} - {{params3}} - {{params2}}.<2>这是参数3222" 7 | }, 8 | "contentWithParams": "这是一个带有参数的句子 1111。参数: {{params1}} - {{params2}} - {{params3}} - {{params2}}", 9 | "currBrowserLang": "当前浏览器的语言" 10 | } 11 | -------------------------------------------------------------------------------- /docs/assets/localization/en/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "title", 3 | "content": { 4 | "helloWorld": "hello world", 5 | "contentWithParams": "This is a sentence. params: {{params1}} - {{params2}} - {{params3}} - {{params2}}", 6 | "complexContent": "This is a sentence. <0>component1. <0>This is params: {{params1}} - {{params2}} - {{params3}} - {{params2}}.<1><0>component2 abc.<1>test <0>this is params: {{params1}} - {{params2}} - {{params3}} - {{params2}}.<2>this is component3222" 7 | }, 8 | "contentWithParams": "This is a sentence 1111. params: {{params1}} - {{params2}} - {{params3}} - {{params2}}", 9 | "currBrowserLang": "Current browser lang" 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/localization/en/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "title", 3 | "content": { 4 | "helloWorld": "hello world", 5 | "contentWithParams": "This is a sentence. params: {{params1}} - {{params2}} - {{params3}} - {{params2}}", 6 | "complexContent": "This is a sentence. <0>component1. <0>This is params: {{params1}} - {{params2}} - {{params3}} - {{params2}}.<1><0>component2 abc.<1>test <0>this is params: {{params1}} - {{params2}} - {{params3}} - {{params2}}.<2>this is component3222" 7 | }, 8 | "contentWithParams": "This is a sentence 1111. params: {{params1}} - {{params2}} - {{params3}} - {{params2}}", 9 | "currBrowserLang": "Current browser lang" 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | package-lock.json 4 | 5 | # Compiled output 6 | /dist 7 | /tmp 8 | /out-tsc 9 | /bazel-out 10 | 11 | # Node 12 | /node_modules 13 | npm-debug.log 14 | yarn-error.log 15 | 16 | # IDEs and editors 17 | .idea/ 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # Visual Studio Code 26 | .vscode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # Miscellaneous 35 | /.angular/cache 36 | .sass-cache/ 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | testem.log 41 | /typings 42 | 43 | # System files 44 | .DS_Store 45 | Thumbs.db 46 | 47 | .nx -------------------------------------------------------------------------------- /src/app/feature1/widget/widget.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input, OnInit, TemplateRef } from '@angular/core'; 2 | import { INbTransSentencePart } from 'nb-trans'; 3 | 4 | @Component({ 5 | selector: 'app-widget', 6 | templateUrl: './widget.component.html', 7 | styleUrls: ['./widget.component.scss'], 8 | changeDetection: ChangeDetectionStrategy.OnPush, 9 | // eslint-disable-next-line @angular-eslint/prefer-standalone 10 | standalone: false, 11 | }) 12 | export class WidgetComponent implements OnInit { 13 | @Input() 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | comContent: string | TemplateRef = ''; 16 | 17 | @Input() list: INbTransSentencePart[] = []; 18 | 19 | constructor() {} 20 | 21 | ngOnInit() {} 22 | } 23 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | package-lock.json 4 | 5 | # Compiled output 6 | /dist 7 | /tmp 8 | /out-tsc 9 | /bazel-out 10 | 11 | # Node 12 | /node_modules 13 | npm-debug.log 14 | yarn-error.log 15 | 16 | # IDEs and editors 17 | .idea/ 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # Visual Studio Code 26 | .vscode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # Miscellaneous 35 | /.angular/cache 36 | .sass-cache/ 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | testem.log 41 | /typings 42 | /docs 43 | *.md 44 | 45 | # System files 46 | .DS_Store 47 | Thumbs.db 48 | 49 | .nx 50 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NbTrans 6 | 7 | 8 | 9 | 10 | 11 | 12 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/app/feature2/feature2.component.html: -------------------------------------------------------------------------------- 1 |
2 | 使用ng-trans组件,带有components参数和options参数。设置key值前缀和翻译文本中的参数,params参数为: 3 | {{ params | json }} 4 |
5 |

翻译文本原文:{{ 'complexContent' | nbTrans: { prefix: 'content' } }}

6 | 7 |
8 |
{{compStr1}}
9 |
10 | 11 |
12 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {{ comContent }} 29 | 30 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/pipes/nb-sentence-item-type.pipe.ts: -------------------------------------------------------------------------------- 1 | import { inject, Pipe, PipeTransform } from '@angular/core'; 2 | import { NbValueTypeService } from '@bigbear713/nb-common'; 3 | import { NbTransSentenceItem } from '../constants'; 4 | import { INbTransSentencePart } from '../models'; 5 | 6 | @Pipe({ standalone: true, name: 'nbSentenceItemType' }) 7 | export class NbSentenceItemTypePipe implements PipeTransform { 8 | private valueType: NbValueTypeService = inject(NbValueTypeService); 9 | 10 | transform(value: INbTransSentencePart): number | undefined { 11 | let type: number | undefined; 12 | 13 | if (this.valueType.isString(value)) { 14 | type = NbTransSentenceItem.STR; 15 | } else if (this.valueType.isNumber(value?.index)) { 16 | type = 17 | Array.isArray(value.list) && value.list.length 18 | ? NbTransSentenceItem.MULTI_COMP 19 | : NbTransSentenceItem.COMP; 20 | } 21 | 22 | return type; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [AppComponent], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have the 'nb-trans-demo' title`, () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app.title).toEqual('nb-trans-demo'); 21 | }); 22 | 23 | it('should render title', () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | fixture.detectChanges(); 26 | const compiled = fixture.nativeElement as HTMLElement; 27 | expect(compiled.querySelector('h1')?.textContent).toContain('Hello, nb-trans-demo'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 @bigbear713/nb-trans project 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NbTrans 6 | 7 | 8 | 9 | 10 | 11 | 12 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/testing/trans-loader.ts: -------------------------------------------------------------------------------- 1 | import { NbTransLang } from '../constants'; 2 | 3 | export const transLoader = { 4 | dynamicLoader: { 5 | [NbTransLang.EN]: () => import('./localization/en/translations').then(data => data.trans), 6 | [NbTransLang.ZH_CN]: () => import('./localization/zh-CN/translations').then(data => data.trans), 7 | }, 8 | staticLoader: { 9 | [NbTransLang.EN]: { 10 | title: 'title ', 11 | content: { 12 | helloWorld: 'hello world', 13 | }, 14 | helloWorld: 'hello world!', 15 | component: '<0>component', 16 | complexComponent: '<0>component0<1>component1', 17 | withParams: 18 | 'This is a sentence. params: {{params1}} - {{params2}} - {{params3}} - {{params2}}', 19 | }, 20 | [NbTransLang.ZH_CN]: { 21 | title: '标题 ', 22 | content: { 23 | helloWorld: '你好,世界', 24 | }, 25 | helloWorld: '你好,世界!', 26 | component: '<0>组件', 27 | complexComponent: '<0>组件0<1>组件1', 28 | withParams: 29 | '这是一个带有参数的句子。参数: {{params1}} - {{params2}} - {{params3}} - {{params2}}', 30 | }, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NbTrans 6 | 7 | 8 | 9 | 10 | 11 | 12 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "paths": { 14 | "nb-trans": [ 15 | "./dist/nb-trans/nb-trans", 16 | "./dist/nb-trans" 17 | ] 18 | }, 19 | "esModuleInterop": true, 20 | "sourceMap": true, 21 | "declaration": false, 22 | "experimentalDecorators": true, 23 | "moduleResolution": "bundler", 24 | "importHelpers": true, 25 | "target": "ES2022", 26 | "module": "ES2022", 27 | "useDefineForClassFields": false, 28 | "lib": [ 29 | "ES2022", 30 | "dom" 31 | ] 32 | }, 33 | "angularCompilerOptions": { 34 | "enableI18nLegacyMessageIdFormat": false, 35 | "strictInjectionParameters": true, 36 | "strictInputAccessModifiers": true, 37 | "strictTemplates": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /projects/nb-trans/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bigbear713/nb-trans", 3 | "version": "20.0.0", 4 | "homepage": "https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md", 5 | "description": "Angular translation lib by bigBear713.", 6 | "keywords": [ 7 | "ng", 8 | "angular", 9 | "angular2+", 10 | "angular13", 11 | "angular14", 12 | "angular15", 13 | "angular16", 14 | "angular17", 15 | "angular18", 16 | "angular19", 17 | "angular20", 18 | "i18n", 19 | "translation", 20 | "translate", 21 | "nb-trans", 22 | "ng-trans", 23 | "bigBear713" 24 | ], 25 | "license": "MIT", 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/bigBear713/nb-trans.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/bigBear713/nb-trans/issues" 32 | }, 33 | "peerDependencies": { 34 | "@angular/common": "^20.0.0", 35 | "@angular/core": "^20.0.0" 36 | }, 37 | "dependencies": { 38 | "@bigbear713/nb-common": "^20.0.0", 39 | "lodash-es": "^4.17.21", 40 | "tslib": "^2.3.0" 41 | }, 42 | "devDependencies": { 43 | "@types/lodash-es": "^4.17.6" 44 | }, 45 | "publishConfig": { 46 | "registry": "https://registry.npmjs.org/" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/components/nb-trans-subcontent/nb-trans-subcontent.component.ts: -------------------------------------------------------------------------------- 1 | import { NgTemplateOutlet } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, Input, TemplateRef } from '@angular/core'; 3 | import { NbIsStringPipe, NbTplContentPipe } from '@bigbear713/nb-common'; 4 | import { INbTransSentencePart } from '../../models'; 5 | 6 | const importsFromNgCommon = [NgTemplateOutlet]; 7 | const importsFromNbCommon = [NbIsStringPipe, NbTplContentPipe]; 8 | 9 | @Component({ 10 | standalone: true, 11 | imports: [...importsFromNgCommon, ...importsFromNbCommon], 12 | // eslint-disable-next-line @angular-eslint/component-selector 13 | selector: '[nb-trans-subcontent]', 14 | template: ` 15 | @switch (content | nbIsString) { 16 | @case (true) { 17 | {{ content }} 18 | } 19 | @default { 20 | 23 | } 24 | } 25 | `, 26 | changeDetection: ChangeDetectionStrategy.OnPush, 27 | }) 28 | export class NbTransSubcontentComponent { 29 | @Input({ alias: 'nb-trans-subcontent', required: true }) 30 | content: string | TemplateRef = ''; 31 | 32 | @Input() subcontentList: INbTransSentencePart[] = []; 33 | } 34 | -------------------------------------------------------------------------------- /projects/nb-trans/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true, // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, '../../coverage/nb-trans'), 29 | subdir: '.', 30 | reporters: [{ type: 'html' }, { type: 'text-summary' }], 31 | }, 32 | reporters: ['progress', 'kjhtml'], 33 | port: 9876, 34 | colors: true, 35 | logLevel: config.LOG_INFO, 36 | autoWatch: true, 37 | browsers: ['Chrome'], 38 | singleRun: false, 39 | restartOnFileChange: true, 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /src/app/g-tag.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import dayjs from 'dayjs'; 4 | import utc from 'dayjs/plugin/utc'; 5 | import { NbTransService } from 'nb-trans'; 6 | dayjs.extend(utc); 7 | 8 | const defaultGtag = () => {}; 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | const gtag = (window as any).gtag || defaultGtag; 11 | 12 | const website_id = '@bigbear713/nb-trans'; 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | const website_ga_id = (window as any).website_ga_id; 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | const libs_ga_id = (window as any).libs_ga_id; 17 | 18 | @Injectable({ 19 | providedIn: 'root', 20 | }) 21 | export class GTagService { 22 | constructor( 23 | private router: Router, 24 | private transService: NbTransService 25 | ) {} 26 | 27 | trackPage(props: object): void { 28 | this.trackEvent('View_Page', props); 29 | } 30 | 31 | trackButton(props: object): void { 32 | this.trackEvent('Click_Button', props); 33 | } 34 | 35 | trackLink(props: object): void { 36 | this.trackEvent('Visit_Link', props); 37 | } 38 | 39 | private trackEvent(eventName: string, props: object): void { 40 | const trackCurrProps = { 41 | send_to: website_ga_id, 42 | datetime: dayjs().utc().format(), 43 | url: this.router.url, 44 | language: this.transService.lang, 45 | ...props, 46 | }; 47 | const trackLibsProps = { 48 | ...trackCurrProps, 49 | send_to: libs_ga_id, 50 | website_id: website_id, 51 | }; 52 | gtag('event', eventName, trackCurrProps); 53 | gtag('event', eventName, trackLibsProps); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/testing/data/translationSync.test-data.ts: -------------------------------------------------------------------------------- 1 | export const translationSyncTestData = [ 2 | { 3 | title: 'options is undefined', 4 | test: { key: 'trans.key', options: undefined }, 5 | expect: { result: 'trans.key' }, 6 | }, 7 | { 8 | title: 'options is {}', 9 | test: { key: 'trans.key', options: {} }, 10 | expect: { result: 'trans.key' }, 11 | }, 12 | { 13 | title: 'returnKeyWhenEmpty is false', 14 | test: { key: 'trans.key', options: { returnKeyWhenEmpty: false } }, 15 | expect: { result: '' }, 16 | }, 17 | { 18 | title: 'returnKeyWhenEmpty is true', 19 | test: { key: 'trans.key', options: { returnKeyWhenEmpty: true } }, 20 | expect: { result: 'trans.key' }, 21 | }, 22 | { 23 | title: 'prefix is "prefix"', 24 | test: { key: 'trans.key', options: { prefix: 'prefix' } }, 25 | expect: { result: 'prefix.trans.key' }, 26 | }, 27 | { 28 | title: 'prefix is "prefix."', 29 | test: { key: 'trans.key', options: { prefix: 'prefix.' } }, 30 | expect: { result: 'prefix..trans.key' }, 31 | }, 32 | { 33 | title: 'prefix is " prefix "', 34 | test: { key: 'trans.key', options: { prefix: ' prefix ' } }, 35 | expect: { result: ' prefix .trans.key' }, 36 | }, 37 | { 38 | title: 'prefix is "content"', 39 | test: { key: 'helloWorld', options: { prefix: 'content' } }, 40 | expect: { result: '你好,世界' }, 41 | }, 42 | { 43 | title: 'key is "content", but options is undefined', 44 | test: { key: 'content', options: undefined }, 45 | expect: { result: 'content' }, 46 | }, 47 | { 48 | title: 'key is "content", prefix is undefined and returnKeyWhenEmpty is false', 49 | test: { key: 'content', options: { returnKeyWhenEmpty: false } }, 50 | expect: { result: '' }, 51 | }, 52 | ]; 53 | -------------------------------------------------------------------------------- /src/app/feature2/feature2.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 3 | import { Route } from '@angular/router'; 4 | import { NbTransModule } from 'nb-trans'; 5 | import { GTagService } from '../g-tag.service'; 6 | 7 | @Component({ 8 | standalone: true, 9 | imports: [NbTransModule, CommonModule], 10 | selector: 'app-feature2', 11 | templateUrl: './feature2.component.html', 12 | styleUrls: ['./feature2.component.css'], 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | }) 15 | export class Feature2Component implements OnInit { 16 | params = { 17 | params1: '{{params2}}', 18 | params2: '1111', 19 | params3: '2222', 20 | }; 21 | 22 | compStr1 = ` 23 |
24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {{comContent}} 37 | 38 | `; 39 | 40 | constructor(private gtagService: GTagService) { 41 | this.trackPage(); 42 | } 43 | 44 | // @angular-eslint/no-empty-lifecycle-method 45 | ngOnInit() { 46 | // console.log(''); 47 | } 48 | 49 | private trackPage() { 50 | this.gtagService.trackPage({ 51 | page_name: 'Standalone Component', 52 | }); 53 | } 54 | } 55 | 56 | export const routes: Route[] = [{ path: '', component: Feature2Component }]; 57 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/components/nb-trans/nb-trans.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @for (item of list; track item) { 5 | @switch (item | nbSentenceItemType) { 6 | @case (SentenceItemEnum.STR) { 7 | 8 | } 9 | @case (SentenceItemEnum.COMP) { 10 | 11 | } 12 | @case (SentenceItemEnum.MULTI_COMP) { 13 | 16 | } 17 | } 18 | } 19 | 20 | 21 | {{ content | nbTransContent: params }} 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 42 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/pipes/nb-trans.pipe.ts: -------------------------------------------------------------------------------- 1 | import { switchMap } from 'rxjs/operators'; 2 | import { ChangeDetectorRef, inject, OnDestroy, Pipe, PipeTransform } from '@angular/core'; 3 | import { INbTransOptions } from '../models'; 4 | import { NbTransService } from '../services'; 5 | import { isEqual } from 'lodash-es'; 6 | import { NbUnsubscribeService } from '@bigbear713/nb-common'; 7 | 8 | @Pipe({ standalone: true, name: 'nbTrans', pure: false }) 9 | export class NbTransPipe implements PipeTransform, OnDestroy { 10 | private changeDR: ChangeDetectorRef = inject(ChangeDetectorRef); 11 | private transService: NbTransService = inject(NbTransService); 12 | 13 | private latestValue: string = ''; 14 | 15 | private key: string = ''; 16 | 17 | private options: INbTransOptions | undefined; 18 | 19 | private unsubscribeService: NbUnsubscribeService; 20 | 21 | constructor() { 22 | this.unsubscribeService = new NbUnsubscribeService(); 23 | this.subscribeLangChange(); 24 | } 25 | 26 | transform(key: string, options?: INbTransOptions): string { 27 | const shouldUpdate = !this.latestValue || key !== this.key || !isEqual(options, this.options); 28 | if (shouldUpdate) { 29 | this.latestValue = this.transService.translationSync(key, options); 30 | 31 | this.key = key; 32 | this.options = options; 33 | } 34 | 35 | return this.latestValue; 36 | } 37 | 38 | ngOnDestroy(): void { 39 | this.unsubscribeService.ngOnDestroy(); 40 | } 41 | 42 | private subscribeLangChange(): void { 43 | const langChange$ = this.transService 44 | .subscribeLangChange() 45 | .pipe(switchMap(() => this.transService.translationAsync(this.key, this.options))); 46 | this.unsubscribeService 47 | .addUnsubscribeOperator(langChange$) 48 | .subscribe(latestValue => this.updateLatestValue(latestValue)); 49 | } 50 | 51 | private updateLatestValue(latestValue: string): void { 52 | this.latestValue = latestValue; 53 | this.changeDR.markForCheck(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

2 | @for (item of links; track item) { 3 | 4 | {{ item.title }} 5 | 6 | } 7 |

8 |
9 |
10 | 11 | 12 | 13 |
14 |
15 | 16 |
17 |
使用 nbTrans 管道,语言切换时自动获取最新的翻译
18 | {{ "{{'title'| nbTrans}}:" }} 19 | {{ 'title' | nbTrans }} 20 |
21 |
22 | 23 |
24 |
使用 nbTrans 管道,key值为多层
25 | {{"{{'content.helloWorld'| nbTrans}}:"}} 26 | {{ 'content.helloWorld' | nbTrans }} 27 |
28 |
29 | 30 |
31 |
32 | 使用 nbTrans 管道,带有options参数。 设置key值前缀和翻译文本中的参数,params参数为: 33 | {{ params | json }} 34 |
35 |

翻译文本原文:{{ 'contentWithParams' | nbTrans: { prefix: 'content' } }}

36 | {{"{{'contentWithParams'| nbTrans:({prefix:'content',params: params})}}:"}} 37 | {{ 'contentWithParams' | nbTrans: { prefix: 'content', params: params } }} 38 |
39 |
40 | 41 |
42 |
通过getter,调用translationSync()时时获取最新的翻译
43 | {{ "get title(){return this.transService.translationSync('title');}:" }} 44 | {{ title }} 45 |
46 |
47 | 48 |
49 |
50 | 调用translationAsync()得到一个Observable,结合 async 管道使用,语言切换时自动获取最新的翻译 51 |
52 |
{{ "this.title$ = this.transService.translationAsync('title');// ts" }}
53 | {{"{{title$ | async}}:"}} {{ title$ | async }} 54 |
55 |
56 | 57 |
58 | {{"{{'test.test'| nbTrans}}"}},当key对应的内容不存在, 默认返回key: {{ 'test.test' | nbTrans }} 59 |
60 |
61 | 62 |
63 | {{"{{'test.test'| nbTrans:({returnKeyWhenEmpty:false})}}"}},当key对应的内容不存在, 64 | 也可以设置返回空字符串: {{ 'test.test' | nbTrans: { returnKeyWhenEmpty: false } }} 65 |
66 |
67 | 68 |

children component

69 | Module Component 70 | Standalone Component 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterOutlet } from '@angular/router'; 4 | import { Observable } from 'rxjs'; 5 | import { GTagService } from './g-tag.service'; 6 | import { NbTransModule, NbTransService } from 'nb-trans'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | standalone: true, 11 | imports: [CommonModule, RouterOutlet, NbTransModule], 12 | templateUrl: './app.component.html', 13 | styleUrl: './app.component.scss', 14 | changeDetection: ChangeDetectionStrategy.OnPush, 15 | }) 16 | export class AppComponent implements OnInit { 17 | title$: Observable | undefined; 18 | 19 | params = { 20 | params1: '{{params2}}', 21 | params2: '1111', 22 | params3: '2222', 23 | }; 24 | 25 | links = [ 26 | { 27 | title: 'Changelog', 28 | link: 'https://github.com/bigBear713/nb-trans/blob/main/CHANGELOG.md', 29 | }, 30 | { 31 | title: 'Document', 32 | link: 'https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md', 33 | }, 34 | ]; 35 | 36 | get title() { 37 | return this.transService.translationSync('title'); 38 | } 39 | 40 | get lang(): string { 41 | return this.transService.lang; 42 | } 43 | 44 | constructor( 45 | private gtagService: GTagService, 46 | private transService: NbTransService 47 | ) {} 48 | 49 | ngOnInit(): void { 50 | this.title$ = this.transService.translationAsync('title'); 51 | } 52 | 53 | go2Link(target: { title: string; link: string }): void { 54 | this.gtagService.trackLink({ 55 | link_name: target.title, 56 | link: target.link, 57 | }); 58 | } 59 | 60 | onChangeLang(lang: string): void { 61 | this.transService.changeLang(lang).subscribe(result => { 62 | console.log(result); 63 | if (!result.result) { 64 | alert('切换语言失败,没有导入该语言包,当前语言是:' + result.curLang); 65 | } 66 | this.gtagService.trackButton({ 67 | button_name: 68 | 'zh-CN' === lang ? '切换为中文' : 'en' === lang ? '切换为英文' : '切换为其它不存在的语言', 69 | language: result.curLang, 70 | }); 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@angular-eslint/recommended", 11 | "plugin:@angular-eslint/template/process-inline-templates", 12 | "plugin:prettier/recommended" 13 | ], 14 | "rules": { 15 | "@angular-eslint/directive-selector": [ 16 | "error", 17 | { 18 | "type": "attribute", 19 | "style": "camelCase" 20 | } 21 | ], 22 | "@angular-eslint/component-selector": [ 23 | "error", 24 | { 25 | "type": "element", 26 | "style": "kebab-case" 27 | } 28 | ], 29 | "@angular-eslint/no-empty-lifecycle-method": ["off"] 30 | } 31 | }, 32 | { 33 | "files": ["projects/**/*.ts"], 34 | "excludedFiles": ["*.spec.ts"], 35 | "rules": { 36 | "@angular-eslint/directive-selector": [ 37 | "error", 38 | { 39 | "type": "attribute", 40 | "prefix": "nb", 41 | "style": "camelCase" 42 | } 43 | ], 44 | "@angular-eslint/component-selector": [ 45 | "error", 46 | { 47 | "type": "element", 48 | "prefix": "nb", 49 | "style": "kebab-case" 50 | } 51 | ], 52 | "@angular-eslint/pipe-prefix": [ 53 | "error", 54 | { 55 | "prefixes": ["nb"] 56 | } 57 | ], 58 | "@angular-eslint/no-input-rename": ["off"] 59 | } 60 | }, 61 | { 62 | "files": ["*.html"], 63 | "extends": [ 64 | "plugin:@angular-eslint/template/recommended", 65 | "plugin:@angular-eslint/template/accessibility", 66 | "plugin:prettier/recommended" 67 | ], 68 | "rules": {} 69 | }, 70 | { 71 | "files": ["*.html"], 72 | "excludedFiles": ["*inline-template-*.component.html"], 73 | "extends": ["plugin:prettier/recommended"], 74 | "rules": { 75 | "prettier/prettier": ["error", { "parser": "angular" }] 76 | } 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/constants/nb-trans-lang.enum.ts: -------------------------------------------------------------------------------- 1 | export enum NbTransLang { 2 | // 简体中文 3 | ZH_CN = 'zh-CN', 4 | // 繁体中文 5 | ZH_TW = 'zh-TW', 6 | ZH_HK = 'zh-HK', 7 | // 英语 8 | EN = 'en', 9 | // 阿拉伯 10 | AR_EG = 'ar-EG', 11 | // 亞美尼亞 12 | HY_AM = 'hy-AM', 13 | // 保加利亚语 14 | BG_BG = 'bg-BG', 15 | // 加泰罗尼亚语 16 | CA_ES = 'ca-ES', 17 | // 捷克语 18 | CS_CZ = 'cs-CZ', 19 | // 丹麦语 20 | DA_DK = 'da-DK', 21 | // 德语 22 | DE_DE = 'de-DE', 23 | // 希腊语 24 | EL_GR = 'el-GR', 25 | // 英语 26 | EN_GB = 'en-GB', 27 | // 英语(美式) 28 | EN_US = 'en-US', 29 | // 西班牙语 30 | ES_ES = 'es-ES', 31 | // 爱沙尼亚语 32 | ET_EE = 'et-EE', 33 | // 波斯语 34 | FA_IR = 'fa-IR', 35 | // 芬兰语 36 | FI_FI = 'fi-FI', 37 | // 法语(比利时) 38 | FR_BE = 'fr-BE', 39 | // 法语 40 | FR_FR = 'fr-FR', 41 | // 希伯来语 42 | HE_IL = 'he-IL', 43 | // 印地语 44 | HI_IN = 'hi-IN', 45 | // 克罗地亚语 46 | HR_HR = 'hr-HR', 47 | // 匈牙利 48 | HU_HU = 'hu-HU', 49 | // 冰岛语 50 | IS_IS = 'is-IS', 51 | // 印度尼西亚语 52 | ID_ID = 'id-ID', 53 | // 意大利语 54 | IT_IT = 'it-IT', 55 | // 日语 56 | JA_JP = 'ja-JP', 57 | // 格鲁吉亚语 58 | KA_GE = 'ka-GE', 59 | // 卡纳达语 60 | KN_IN = 'kn-IN', 61 | // 韩语/朝鲜语 62 | KO_KR = 'ko-KR', 63 | // 库尔德语 64 | KU_IQ = 'ku-IQ', 65 | // 拉脱维亚语 66 | LV_LV = 'lv-LV', 67 | // 马来语 68 | MS_MY = 'ms-MY', 69 | // 蒙古语 70 | MN_MN = 'mn-MN', 71 | // 挪威 72 | NB_NO = 'nb-NO', 73 | // 尼泊尔语 74 | NE_NP = 'ne-NP', 75 | // 荷兰语(比利时) 76 | NL_BE = 'nl-BE', 77 | // 荷兰语 78 | NL_NL = 'nl-NL', 79 | // 波兰语 80 | PL_PL = 'pl-PL', 81 | // 葡萄牙语(巴西) 82 | PT_BR = 'pt-BR', 83 | // 葡萄牙语 84 | PT_PT = 'pt-PT', 85 | // 斯洛伐克语 86 | SK_SK = 'sk-SK', 87 | // 塞尔维亚 88 | SR_RS = 'sr-RS', 89 | // 斯洛文尼亚 90 | SL_SI = 'sl-SI', 91 | // 瑞典语 92 | SV_SE = 'sv-SE', 93 | // 泰米尔语 94 | TA_IN = 'ta-IN', 95 | // 泰语 96 | TH_TH = 'th-TH', 97 | // 土耳其语 98 | TR_TR = 'tr-TR', 99 | // 罗马尼亚语 100 | RO_RO = 'ro-RO', 101 | // 俄罗斯语 102 | RU_RU = 'ru-RU', 103 | // 乌克兰语 104 | UK_UA = 'uk-UA', 105 | // 越南语 106 | VI_VN = 'vi-VN', 107 | } 108 | /** 109 | * @deprecated use "NbTransLang" please 110 | */ 111 | export const NbTransLangEnum = NbTransLang; 112 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | 4 | import { routes } from './app.routes'; 5 | import { 6 | NB_TRANS_DEFAULT_LANG, 7 | NB_TRANS_LOADER, 8 | NB_TRANS_PARAM_KEY_INVALID_WARNING, 9 | NbTransLang, 10 | } from 'nb-trans'; 11 | import { HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; 12 | import { lastValueFrom } from 'rxjs'; 13 | 14 | export const appConfig: ApplicationConfig = { 15 | providers: [ 16 | provideHttpClient(withInterceptorsFromDi()), 17 | // { 18 | // provide: NB_TRANS_MAX_RETRY, 19 | // useValue: 0 20 | // }, 21 | { 22 | provide: NB_TRANS_DEFAULT_LANG, 23 | useValue: NbTransLang.ZH_CN, 24 | }, 25 | // { 26 | // provide: NB_TRANS_LOADER, 27 | // useValue: { 28 | // // dyn load and the content is a ts file 29 | // [NbTransLang.EN]: () => import('./localization/en/translations').then(data => data.trans), 30 | // [NbTransLang.ZH_CN]: () => import('./localization/zh-CN/translations').then(data => data.trans), 31 | // // direct load 32 | // // [NbTransLang.ZH_CN]: trans, 33 | // }, 34 | // }, 35 | { 36 | provide: NB_TRANS_LOADER, 37 | useFactory: (http: HttpClient) => ({ 38 | // https://github.com/ngx-translate/core/issues/1207#issuecomment-673921899 39 | // it is expecting to get the translation file using HTTP via absolute URL when angualr SSR. 40 | // So here change the file's path as relative/absolute via environment 41 | 42 | // dyn load and the content is a json file 43 | // [NbTransLang.EN]: () => lastValueFrom(http.get('./assets/localization/en/translations.json')), 44 | [NbTransLang.EN]: () => http.get('./assets/localization/en/translations.json'), 45 | [NbTransLang.ZH_CN]: () => 46 | lastValueFrom(http.get('./assets/localization/zh-CN/translations.json')), 47 | // [NbTransLang.ZH_CN]: () => http.get('./assets/localization/zh-CN/translations.json'), 48 | }), 49 | deps: [HttpClient], 50 | }, 51 | // set as false will not display invalid warning info 52 | { provide: NB_TRANS_PARAM_KEY_INVALID_WARNING, useValue: false }, 53 | provideRouter(routes), 54 | ], 55 | }; 56 | -------------------------------------------------------------------------------- /docs/chunk-FQRN3WBV.js: -------------------------------------------------------------------------------- 1 | import{A as f,B as d,D as v,E as x,F as E,G as b,H as S,I as h,J as l,L as y,M as T,Z as B,_ as D,aa as k,ba as I,ca as P,j as i,k as g,l as F,n as _,r as p,s as a,t as c,u as s,y as u,z as r}from"./chunk-73BIQRKQ.js";var A=()=>({prefix:"content"}),R=(t,e,n)=>[t,e,n],j=t=>({params:t,prefix:"content"});function O(t,e){if(t&1&&s(0,"b",4),t&2){let n=e.content,o=e.list;p("nb-trans-subcontent",n)("subcontentList",o)}}function w(t,e){if(t&1&&s(0,"a",4),t&2){let n=e.content,o=e.list;p("nb-trans-subcontent",n)("subcontentList",o)}}function G(t,e){if(t&1&&(a(0,"b"),r(1),c()),t&2){let n=e.content;i(),f(n)}}var J=(()=>{let e=class e{constructor(o){this.gtagService=o,this.params={params1:"{{params2}}",params2:"1111",params3:"2222"},this.compStr1=` 2 |
3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {{comContent}} 16 | 17 | `,this.trackPage()}ngOnInit(){}trackPage(){this.gtagService.trackPage({page_name:"Standalone Component"})}};e.\u0275fac=function(m){return new(m||e)(g(P))},e.\u0275cmp=F({type:e,selectors:[["app-feature2"]],decls:18,vars:17,consts:[["com0",""],["com1",""],["com2",""],["key","complexContent",3,"components","options"],[3,"nb-trans-subcontent","subcontentList"]],template:function(m,C){if(m&1&&(a(0,"h5"),r(1),b(2,"json"),c(),a(3,"p"),r(4),b(5,"nbTrans"),c(),a(6,"div")(7,"pre")(8,"code"),r(9),c()()(),a(10,"div"),s(11,"nb-trans",3),c(),_(12,O,1,2,"ng-template",null,0,l)(14,w,1,2,"ng-template",null,1,l)(16,G,2,1,"ng-template",null,2,l)),m&2){let L=u(13),M=u(15),N=u(17);i(),d(" \u4F7F\u7528ng-trans\u7EC4\u4EF6\uFF0C\u5E26\u6709components\u53C2\u6570\u548Coptions\u53C2\u6570\u3002\u8BBE\u7F6Ekey\u503C\u524D\u7F00\u548C\u7FFB\u8BD1\u6587\u672C\u4E2D\u7684\u53C2\u6570,params\u53C2\u6570\u4E3A\uFF1A ",S(2,5,C.params),` 18 | `),i(3),d("\u7FFB\u8BD1\u6587\u672C\u539F\u6587\uFF1A",h(5,7,"complexContent",v(10,A))),i(5),f(C.compStr1),i(2),p("components",E(11,R,L,M,N))("options",x(15,j,C.params))}},dependencies:[I,D,k,T,B,y],styles:["a[_ngcontent-%COMP%]{color:#0ff;cursor:pointer}"],changeDetection:0});let t=e;return t})(),V=[{path:"",component:J}];export{J as Feature2Component,V as routes}; 19 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/pipes/test/nb-trans-content.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, inject } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { NbTransToolsService } from '../../services'; 4 | import { handleSentenceWithParamsTestData, NbTransTestingModule } from '../../testing'; 5 | import { NbTransContentPipe } from '../nb-trans-content.pipe'; 6 | import { NB_TRANS_PARAM_KEY_INVALID_WARNING } from '../../constants'; 7 | 8 | describe('Pipe: NbTransContente', () => { 9 | describe('used in normal case', () => { 10 | let pipe: NbTransContentPipe; 11 | 12 | beforeEach(async () => { 13 | await TestBed.configureTestingModule({ 14 | imports: [NbTransTestingModule], 15 | providers: [ 16 | { provide: NB_TRANS_PARAM_KEY_INVALID_WARNING, useValue: false }, 17 | NbTransToolsService, 18 | ], 19 | declarations: [], 20 | }).compileComponents(); 21 | }); 22 | 23 | beforeEach(() => { 24 | pipe = TestBed.runInInjectionContext(() => new NbTransContentPipe()); 25 | }); 26 | 27 | it('create an instance', () => { 28 | expect(pipe).toBeTruthy(); 29 | }); 30 | 31 | describe('#transform()', () => { 32 | handleSentenceWithParamsTestData.forEach(item => { 33 | it(item.title, () => { 34 | const result = pipe.transform(item.test.trans, item.test.params); 35 | expect(result).toEqual(item.expect.result); 36 | }); 37 | }); 38 | }); 39 | }); 40 | 41 | describe('used in standalone component', () => { 42 | [ 43 | { 44 | title: 'imported by standalone component', 45 | createComp: () => TestBed.createComponent(StandaloneComponent), 46 | }, 47 | ].forEach(item => { 48 | it(item.title, () => { 49 | const fixture = item.createComp(); 50 | const component = fixture.componentInstance; 51 | fixture.detectChanges(); 52 | 53 | expect(component.textContent).toEqual(handleSentenceWithParamsTestData[0].expect.result); 54 | }); 55 | }); 56 | }); 57 | }); 58 | 59 | const StandaloneCompConfig = { 60 | standalone: true, 61 | imports: [NbTransContentPipe], 62 | template: `{{trans|nbTransContent:params}}`, 63 | }; 64 | 65 | @Component(StandaloneCompConfig) 66 | class StandaloneComponent { 67 | private elementRef: ElementRef = inject(ElementRef); 68 | trans = handleSentenceWithParamsTestData[0].test.trans; 69 | params = handleSentenceWithParamsTestData[0].test.params; 70 | 71 | get textContent() { 72 | return this.elementRef.nativeElement.textContent?.trim(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /README.CN.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # @bigbear713/nb-trans 4 | 5 | Angular translation lib by bigBear713. 6 | 7 | [Online Demo](https://bigBear713.github.io/nb-trans/) 8 | 9 | [Bug Report](https://github.com/bigBear713/nb-trans/issues) 10 | 11 | [Feature Request](https://github.com/bigBear713/nb-trans/issues) 12 | 13 | [SSR Demo](https://github.com/bigBear713/nb-trans-ssr/) 14 | 15 |
16 | 17 | ## Document 18 | - [中文](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md "文档 - 中文") 19 | - [English](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md "Document - English") 20 | 21 | --- 22 | 23 | ## Changelog 24 | - [中文](https://github.com/bigBear713/nb-trans/blob/main/CHANGELOG.CN.md "更新日志 - 中文") 25 | - [English](https://github.com/bigBear713/nb-trans/blob/main/CHANGELOG.md "Changelog - English") 26 | 27 | --- 28 | 29 | ## Readme 30 | - [中文](https://github.com/bigBear713/nb-trans/blob/main/README.CN.md "文档 - 中文") 31 | - [English](https://github.com/bigBear713/nb-trans/blob/main/README.md "Document - English") 32 | 33 | --- 34 | 35 | ## Feature 36 | - 支持翻译文本懒加载,或者急性加载; 37 | - 支持切换语言时,不刷新页面自动更新翻译文本; 38 | - 支持设置翻译文本加载失败时的重试次数; 39 | - 支持翻译文本中带有参数; 40 | - 支持翻译文本中带有组件的复杂场景; 41 | - 支持组件的更新策略为`ChangeDetectionStrategy.OnPush`; 42 | - 支持在`standalone component`中使用; 43 | - 支持以`standalone component`的方式引入; 44 | 45 | --- 46 | 47 | ## Version 48 | ###### nb-trans的大版本和Angular的大版本保持对应关系 49 | | @bigbear713/nb-trans | @angular/core | 50 | | -------------------- | ------------- | 51 | | ^12.0.0 | ^12.0.0 | 52 | | ^13.0.0 | ^13.0.0 | 53 | | ^14.0.0 | ^14.0.0 | 54 | | ^15.0.0 | ^15.0.0 | 55 | | ^16.0.0 | ^16.0.0 | 56 | | ^17.0.0 | ^17.0.0 | 57 | | ^18.0.0 | ^18.0.0 | 58 | | ^19.0.0 | ^19.0.0 | 59 | | ^20.0.0 | ^20.0.0 | 60 | 61 | --- 62 | 63 | ## Installation 64 | ```bash 65 | $ npm i @bigbear713/nb-trans 66 | // or 67 | $ yarn add @bigbear713/nb-trans 68 | ``` 69 | 70 | --- 71 | 72 | ## 启动demo项目 73 | - 安装依赖: 74 | ```bash 75 | npm i 76 | ``` 77 | 78 | - 编译nb-trans库 79 | ```bash 80 | npm run build:lib 81 | ``` 82 | 83 | - 运行nb-trans单元测试 84 | ```bash 85 | npm run test:lib 86 | ``` 87 | 88 | - 启动demo项目 89 | ```bash 90 | npm start 91 | ``` 92 | 93 | - 部署demo 94 | ```bash 95 | npm run build 96 | ``` 97 | 98 | --- 99 | 100 | ## 贡献者 101 | > 欢迎提feature和PR,一起使该项目更好 102 | 103 | bigBear713 104 | 105 | --- 106 | 107 | ## License 108 | MIT 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nb-trans-demo", 3 | "version": "20.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test", 10 | "start:lib": "ng build --watch nb-trans", 11 | "build:lib": "npm run lint:lib && ng build nb-trans && npm run copy:readmeCn && npm run copy:changelog && npm run copy:license", 12 | "build:analyze": "npm run build -- --source-map && npm run view:analyze", 13 | "view:analyze": "node_modules/.bin/source-map-explorer dist/nb-trans-demo/*", 14 | "test:lib": "ng test nb-trans", 15 | "deploy": "ng build nb-trans-demo --configuration=deploy && cp docs/index.html docs/404.html", 16 | "publish:lib": "npm run build:lib && cd ./dist/nb-trans && npm publish --access=public", 17 | "copy:readmeCn": "cp projects/nb-trans/README.CN.md dist/nb-trans/", 18 | "copy:changelog": "cp CHANGELOG.* dist/nb-trans/", 19 | "copy:license": "cp LICENSE dist/nb-trans/", 20 | "pack:lib": "cd ./dist/nb-trans && npm pack", 21 | "lint": "ng lint", 22 | "lint:lib": "ng lint nb-trans" 23 | }, 24 | "private": true, 25 | "dependencies": { 26 | "@angular/animations": "^20.3.3", 27 | "@angular/common": "^20.3.3", 28 | "@angular/compiler": "^20.3.3", 29 | "@angular/core": "^20.3.3", 30 | "@angular/forms": "^20.3.3", 31 | "@angular/platform-browser": "^20.3.3", 32 | "@angular/platform-browser-dynamic": "^20.3.3", 33 | "@angular/router": "^20.3.3", 34 | "@bigbear713/nb-common": "^20.0.0", 35 | "dayjs": "^1.11.7", 36 | "lodash-es": "^4.17.21", 37 | "rxjs": "~7.8.0", 38 | "tslib": "^2.3.0", 39 | "zone.js": "~0.15.0" 40 | }, 41 | "devDependencies": { 42 | "@angular-eslint/builder": "20.3.0", 43 | "@angular-eslint/eslint-plugin": "20.3.0", 44 | "@angular-eslint/eslint-plugin-template": "20.3.0", 45 | "@angular-eslint/schematics": "20.3.0", 46 | "@angular-eslint/template-parser": "20.3.0", 47 | "@angular/build": "^20.3.4", 48 | "@angular/cli": "^20.3.4", 49 | "@angular/compiler-cli": "^20.3.3", 50 | "@types/jasmine": "~5.1.0", 51 | "@types/lodash-es": "^4.17.5", 52 | "@types/node": "^18.18.0", 53 | "@typescript-eslint/eslint-plugin": "^8.33.1", 54 | "@typescript-eslint/parser": "^8.33.1", 55 | "eslint": "^9.28.0", 56 | "eslint-config-prettier": "^9.1.0", 57 | "eslint-plugin-prettier": "^5.1.3", 58 | "jasmine-core": "~5.1.0", 59 | "karma": "~6.4.0", 60 | "karma-chrome-launcher": "~3.2.0", 61 | "karma-coverage": "~2.2.0", 62 | "karma-jasmine": "~5.1.0", 63 | "karma-jasmine-html-reporter": "~2.1.0", 64 | "ng-packagr": "^20.3.0", 65 | "prettier": "^3.4.2", 66 | "typescript": "~5.9.3" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # @bigbear713/nb-trans 4 | 5 | Angular translation lib by bigBear713. 6 | 7 | [Online Demo](https://bigBear713.github.io/nb-trans/) 8 | 9 | [Bug Report](https://github.com/bigBear713/nb-trans/issues) 10 | 11 | [Feature Request](https://github.com/bigBear713/nb-trans/issues) 12 | 13 | [SSR Demo](https://github.com/bigBear713/nb-trans-ssr/) 14 | 15 | 16 |
17 | 18 | ## Document 19 | - [中文](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md "文档 - 中文") 20 | - [English](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md "Document - English") 21 | 22 | --- 23 | 24 | ## Changelog 25 | - [中文](https://github.com/bigBear713/nb-trans/blob/main/CHANGELOG.CN.md "更新日志 - 中文") 26 | - [English](https://github.com/bigBear713/nb-trans/blob/main/CHANGELOG.md "Changelog - English") 27 | 28 | --- 29 | 30 | ## Readme 31 | - [中文](https://github.com/bigBear713/nb-trans/blob/main/README.CN.md "文档 - 中文") 32 | - [English](https://github.com/bigBear713/nb-trans/blob/main/README.md "Document - English") 33 | 34 | --- 35 | 36 | ## Feature 37 | - Support to direct/lazing loading translation file; 38 | - Support to update translation content in page directly and no need to reload page; 39 | - Support to reset the max retry time when failure to load the translation file; 40 | - Support there are some params in translation sentence; 41 | - Support there are some components in the translation sentence; 42 | - Support the changeDetection of components as `ChangeDetectionStrategy.OnPush`; 43 | - Support to used in `standalone component`; 44 | - Support to be imported as a `standalone component`; 45 | 46 | --- 47 | 48 | ## Version 49 | ###### The nb-trans's major version will keep up with the Angular's major version 50 | | @bigbear713/nb-trans | @angular/core | 51 | | -------------------- | ------------- | 52 | | ^12.0.0 | ^12.0.0 | 53 | | ^13.0.0 | ^13.0.0 | 54 | | ^14.0.0 | ^14.0.0 | 55 | | ^15.0.0 | ^15.0.0 | 56 | | ^17.0.0 | ^17.0.0 | 57 | | ^18.0.0 | ^18.0.0 | 58 | | ^19.0.0 | ^19.0.0 | 59 | | ^20.0.0 | ^20.0.0 | 60 | 61 | --- 62 | 63 | ## Installation 64 | ```bash 65 | $ npm i @bigbear713/nb-trans 66 | // or 67 | $ yarn add @bigbear713/nb-trans 68 | ``` 69 | 70 | --- 71 | 72 | ## Start the demo project 73 | - Install the dependencies: 74 | ```bash 75 | npm i 76 | ``` 77 | 78 | - Build the nb-trans lib 79 | ```bash 80 | npm run build:lib 81 | ``` 82 | 83 | - Run the nb-trans unit test 84 | ```bash 85 | npm run test:lib 86 | ``` 87 | 88 | - Start the demo 89 | ```bash 90 | npm start 91 | ``` 92 | 93 | - build the demo 94 | ```bash 95 | npm run build 96 | ``` 97 | 98 | --- 99 | 100 | ## Contribution 101 | > Feature and PR are welcome to make this project better together 102 | 103 | bigBear713 104 | 105 | --- 106 | 107 | ## License 108 | MIT 109 | -------------------------------------------------------------------------------- /src/app/feature1/feature1.component.html: -------------------------------------------------------------------------------- 1 |
2 |
使用 nbTrans 管道,语言切换时自动获取最新的翻译
3 | {{ "{{'title'| nbTrans}}:" }} {{ 'title' | nbTrans }} 4 |
5 |
6 | 7 |
8 |
使用 nbTrans 管道,key值为多层
9 | {{"{{'content.helloWorld'| nbTrans}}:"}} {{ 'content.helloWorld' | nbTrans }} 10 |
11 |
12 | 13 |
14 |
15 | 使用 nbTrans 管道,带有options参数。设置key值前缀和翻译文本中的参数,params参数为: 16 | {{ params | json }} 17 |
18 |

翻译文本原文:{{ 'contentWithParams' | nbTrans: { prefix: 'content' } }}

19 | {{ "{{'contentWithParams'| nbTrans:({prefix:'content',params: params})}}:" }} 20 | {{ 'contentWithParams' | nbTrans: { prefix: 'content', params: params } }} 21 |
22 | 23 |
动态调整options,options is {{ options | json }}
24 | 25 |

{{ 'contentWithParams' | nbTrans: options }}

26 | 27 |
28 | 29 |
30 |
通过getter,调用translationSync()时时获取最新的翻译
31 | {{ "get title(){return this.transService.translationSync('title');}:" }} {{ title }} 32 |
33 | 34 |
35 | 36 |
37 |
38 | 调用translationAsync()得到一个Observable,结合 async 管道使用,语言切换时自动获取最新的翻译 39 |
40 |
{{ "this.title$ = this.transService.translationAsync('title');// ts" }}
41 | {{"{{title$ | async}}:"}} {{ title$ | async }} 42 |
43 | 44 |
45 | 46 |

use {{ '\\' }}

47 | 48 |
49 | 50 |
51 | 52 |
53 | 54 |
55 | 使用{{ '\' }}组件,带有components参数和options参数。 56 | 设置key值前缀和翻译文本中的参数,params参数为:{{ params | json }} 57 |
58 |

翻译文本原文:{{ 'complexContent' | nbTrans: { prefix: 'content' } }}

59 | 60 |
61 |
{{compStr1}}
62 |
63 | 64 |
65 | 69 |
70 | 71 |
72 | 73 |

use {{ '\
\
' }}

74 | 75 |
76 |
77 | 78 |
79 | 使用{{ '\
\
' }}组件,带有components参数和options参数。 80 | 设置key值前缀和翻译文本中的参数,params参数为:{{ params | json }} 81 |
82 |

翻译文本原文:{{ 'complexContent' | nbTrans: { prefix: 'content' } }}

83 | 84 |
85 |
{{compStr2}}
86 |
87 | 88 |
89 |
93 |
94 | 95 |
96 | 97 |
{{ 'currBrowserLang' | nbTrans }}
98 |

{{ browserLang }}

99 |

{{ browserLangs | json }}

100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | {{ comContent }} 111 | 112 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/pipes/test/nb-sentence-item-type.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Component, ElementRef, inject } from '@angular/core'; 3 | import { TestBed } from '@angular/core/testing'; 4 | import { NbValueTypeService } from '@bigbear713/nb-common'; 5 | import { isString } from 'lodash-es'; 6 | import { NbTransSentenceItem } from '../../constants'; 7 | import { INbTransSentencePart } from '../../models'; 8 | import { NbSentenceItemTypePipe } from '../nb-sentence-item-type.pipe'; 9 | 10 | describe('Pipe: NbSentenceItemType', () => { 11 | describe('used in normal case', () => { 12 | let pipe: NbSentenceItemTypePipe; 13 | 14 | beforeEach(async () => { 15 | await TestBed.configureTestingModule({ 16 | providers: [NbValueTypeService], 17 | }).compileComponents(); 18 | }); 19 | 20 | beforeEach(() => { 21 | pipe = TestBed.runInInjectionContext(() => new NbSentenceItemTypePipe()); 22 | }); 23 | 24 | it('create an instance', () => { 25 | expect(pipe).toBeTruthy(); 26 | }); 27 | 28 | describe('#transform()', () => { 29 | [ 30 | { params: undefined as any, expect: undefined }, 31 | { params: 'strContent', expect: NbTransSentenceItem.STR }, 32 | { params: { index: 0, content: 'strContent', list: [] }, expect: NbTransSentenceItem.COMP }, 33 | { 34 | params: { 35 | index: 0, 36 | content: '<0>str', 37 | list: [{ index: 0, content: 'str', list: [] }], 38 | }, 39 | expect: NbTransSentenceItem.MULTI_COMP, 40 | }, 41 | { 42 | params: { 43 | index: undefined, 44 | content: 'strContent', 45 | list: [], 46 | } as unknown as INbTransSentencePart, 47 | expect: undefined, 48 | }, 49 | { 50 | params: { 51 | index: undefined, 52 | content: 'strContent', 53 | list: undefined, 54 | } as unknown as INbTransSentencePart, 55 | expect: undefined, 56 | }, 57 | ].forEach(item => { 58 | it(`the params is ${isString(item.params) ? item.params : JSON.stringify(item.params)}`, () => { 59 | const type = pipe.transform(item.params); 60 | expect(type).toEqual(item.expect); 61 | }); 62 | }); 63 | }); 64 | }); 65 | 66 | describe('used in standalone component', () => { 67 | [ 68 | { 69 | title: 'imported by standalone component', 70 | createComp: () => TestBed.createComponent(StandaloneComponent), 71 | }, 72 | ].forEach(item => { 73 | it(item.title, () => { 74 | const fixture = item.createComp(); 75 | const component = fixture.componentInstance; 76 | fixture.detectChanges(); 77 | 78 | expect(component.textContent).toEqual(NbTransSentenceItem.STR.toString()); 79 | }); 80 | }); 81 | }); 82 | }); 83 | 84 | const StandaloneCompConfig = { 85 | standalone: true, 86 | imports: [NbSentenceItemTypePipe], 87 | template: `{{value|nbSentenceItemType}}`, 88 | }; 89 | 90 | @Component(StandaloneCompConfig) 91 | class StandaloneComponent { 92 | private elementRef: ElementRef = inject(ElementRef); 93 | value = 'strContent'; 94 | 95 | get textContent() { 96 | return this.elementRef.nativeElement.textContent?.trim(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/app/feature1/feature1.component.ts: -------------------------------------------------------------------------------- 1 | import { INbTransOptions, NbTransService } from 'nb-trans'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 5 | import { GTagService } from '../g-tag.service'; 6 | 7 | @Component({ 8 | selector: 'app-feature1', 9 | templateUrl: './feature1.component.html', 10 | styleUrls: ['./feature1.component.scss'], 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | // eslint-disable-next-line @angular-eslint/prefer-standalone 13 | standalone: false, 14 | }) 15 | export class Feature1Component implements OnInit { 16 | title$: Observable | undefined; 17 | titleWithParams$: Observable | undefined; 18 | 19 | params = { 20 | params1: '{{params2}}', 21 | params2: '1111', 22 | params3: '2222', 23 | '#p^': 'test', 24 | }; 25 | 26 | options: INbTransOptions = { 27 | prefix: 'content', 28 | params: this.params, 29 | }; 30 | 31 | get lang() { 32 | return this.transService.lang; 33 | } 34 | 35 | get title() { 36 | return this.transService.translationSync('title'); 37 | } 38 | 39 | compStr1 = ` 40 |
41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {{comContent}} 54 | 55 | `; 56 | 57 | compStr2 = ` 58 |
59 |
60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | {{comContent}} 72 | 73 | `; 74 | 75 | browserLang: string | undefined = ''; 76 | browserLangs: readonly string[] | undefined = []; 77 | 78 | constructor( 79 | private gtagService: GTagService, 80 | private transService: NbTransService 81 | ) { 82 | this.trackPage(); 83 | } 84 | 85 | ngOnInit(): void { 86 | this.title$ = this.transService.translationAsync('title'); 87 | this.titleWithParams$ = this.transService.translationAsync('content.contentWithParams', { 88 | params: this.params, 89 | }); 90 | this.browserLang = NbTransService.getBrowserLang(); 91 | this.browserLangs = NbTransService.getBrowserLangs(); 92 | } 93 | 94 | changeOptions() { 95 | const prefix = this.options.prefix ? undefined : 'content'; 96 | this.options = { ...this.options, prefix: prefix }; 97 | 98 | this.gtagService.trackButton({ 99 | button_name: 'change options', 100 | }); 101 | } 102 | 103 | private trackPage() { 104 | this.gtagService.trackPage({ 105 | page_name: 'Module Component', 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/components/nb-trans/nb-trans.component.ts: -------------------------------------------------------------------------------- 1 | import { switchMap } from 'rxjs/operators'; 2 | import { 3 | ChangeDetectionStrategy, 4 | ChangeDetectorRef, 5 | Component, 6 | inject, 7 | Input, 8 | OnChanges, 9 | SimpleChanges, 10 | TemplateRef, 11 | } from '@angular/core'; 12 | import { INbTransOptions, INbTransParams, INbTransSentencePart } from '../../models'; 13 | import { NbTransService, NbTransToolsService } from '../../services'; 14 | import { NbTransSentenceItem } from '../../constants'; 15 | import { NgTemplateOutlet } from '@angular/common'; 16 | import { NbSentenceItemTypePipe, NbTransContentPipe } from '../../pipes'; 17 | import { NbTplContentPipe, NbUnsubscribeService } from '@bigbear713/nb-common'; 18 | 19 | const importsFromNgCommon = [NgTemplateOutlet]; 20 | const importsFromNbCommon = [NbTplContentPipe]; 21 | const importsFromSelf = [NbSentenceItemTypePipe, NbTransContentPipe]; 22 | 23 | @Component({ 24 | standalone: true, 25 | imports: [...importsFromNgCommon, ...importsFromNbCommon, ...importsFromSelf], 26 | selector: 'nb-trans', 27 | templateUrl: './nb-trans.component.html', 28 | changeDetection: ChangeDetectionStrategy.OnPush, 29 | providers: [NbUnsubscribeService], 30 | }) 31 | export class NbTransComponent implements OnChanges { 32 | private changeDR: ChangeDetectorRef = inject(ChangeDetectorRef); 33 | private transToolsService: NbTransToolsService = inject(NbTransToolsService); 34 | private transService: NbTransService = inject(NbTransService); 35 | private unsubscribeService: NbUnsubscribeService = inject(NbUnsubscribeService); 36 | 37 | @Input() components: TemplateRef<{ 38 | content: string | TemplateRef; 39 | list?: INbTransSentencePart[]; 40 | }>[] = []; 41 | 42 | @Input({ required: true }) key: string = ''; 43 | 44 | @Input() options: INbTransOptions = {}; 45 | 46 | params: INbTransParams | undefined; 47 | 48 | sentenceList: INbTransSentencePart[] = []; 49 | 50 | SentenceItemEnum = NbTransSentenceItem; 51 | 52 | private optionsWithoutParams: INbTransOptions = {}; 53 | 54 | private originTrans: string = ''; 55 | 56 | constructor() { 57 | this.subscribeLangChange(); 58 | } 59 | 60 | ngOnChanges(changes: SimpleChanges): void { 61 | const { key, options } = changes; 62 | if (options) { 63 | this.updateOptionsWithoutParams(); 64 | } 65 | if (key || options) { 66 | this.originTrans = this.transService.translationSync(this.key, this.optionsWithoutParams); 67 | this.reRender(); 68 | } 69 | } 70 | 71 | private reRender(): void { 72 | this.params = this.options?.params; 73 | 74 | const trans = this.originTrans; 75 | this.sentenceList = this.transToolsService.handleTrans(trans); 76 | 77 | this.changeDR.markForCheck(); 78 | } 79 | 80 | private subscribeLangChange(): void { 81 | const langChange$ = this.transService 82 | .subscribeLangChange() 83 | .pipe( 84 | switchMap(() => this.transService.translationAsync(this.key, this.optionsWithoutParams)) 85 | ); 86 | this.unsubscribeService.addUnsubscribeOperator(langChange$).subscribe(latestValue => { 87 | this.originTrans = latestValue; 88 | this.reRender(); 89 | }); 90 | } 91 | 92 | private updateOptionsWithoutParams() { 93 | // or origin trans string, the dynamic params don't need to be translated, because they will be translated in sentence item, 94 | // so here remove the options' params 95 | this.optionsWithoutParams = { 96 | ...(this.options || {}), 97 | params: undefined, 98 | }; 99 | } 100 | } 101 | 102 | @Component({ 103 | standalone: true, 104 | imports: [...importsFromNgCommon, ...importsFromNbCommon, ...importsFromSelf], 105 | // eslint-disable-next-line @angular-eslint/component-selector 106 | selector: '[nb-trans]', 107 | templateUrl: './nb-trans.component.html', 108 | changeDetection: ChangeDetectionStrategy.OnPush, 109 | providers: [NbUnsubscribeService], 110 | }) 111 | export class NbTrans2Component extends NbTransComponent { 112 | @Input('nb-trans-components') 113 | override components: TemplateRef<{ 114 | content: string | TemplateRef; 115 | list?: INbTransSentencePart[]; 116 | }>[] = []; 117 | 118 | @Input({ alias: 'nb-trans', required: true }) override key: string = ''; 119 | 120 | @Input('nb-trans-options') override options: INbTransOptions = {}; 121 | } 122 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/testing/data/handleSentenceWithParams.test-data.ts: -------------------------------------------------------------------------------- 1 | import { INbTransParams } from '../../models'; 2 | 3 | export const handleSentenceWithParamsTestData: { 4 | title: string; 5 | test: { trans: string; params?: INbTransParams }; 6 | expect: { result: string }; 7 | }[] = [ 8 | { 9 | title: 'no params', 10 | test: { trans: 'test trans', params: undefined }, 11 | expect: { result: 'test trans' }, 12 | }, 13 | { 14 | title: 'empty params', 15 | test: { trans: 'test trans', params: {} }, 16 | expect: { result: 'test trans' }, 17 | }, 18 | { 19 | title: '2 params', 20 | test: { trans: 'a {{p1}} {{p2}}', params: { p1: '123', p2: 'abc' } }, 21 | expect: { result: 'a 123 abc' }, 22 | }, 23 | { 24 | title: 'params key is upperCase, but params value is lowerCase', 25 | test: { trans: 'a {{P1}} {{p2}}', params: { p1: '123', p2: 'abc' } }, 26 | expect: { result: 'a {{P1}} abc' }, 27 | }, 28 | { 29 | title: 'params key is lowerCase, but params value is upperCase', 30 | test: { trans: 'a {{p1}} {{P2}}', params: { P1: '123', P2: 'abc' } }, 31 | expect: { result: 'a {{p1}} abc' }, 32 | }, 33 | { 34 | title: 'start with params', 35 | test: { trans: '{{p1}} and {{p2}}', params: { p1: '123', p2: 'abc' } }, 36 | expect: { result: '123 and abc' }, 37 | }, 38 | { 39 | title: 'params are in middle', 40 | test: { trans: 'a {{p1}} b {{p2}} c', params: { p1: '123', p2: 'abc' } }, 41 | expect: { result: 'a 123 b abc c' }, 42 | }, 43 | { 44 | title: 'params value is same with param key', 45 | test: { trans: '{{p1}}{{p2}}', params: { p1: '{{p2}}', p2: '{{p2}}' } }, 46 | expect: { result: '{{p2}}{{p2}}' }, 47 | }, 48 | { 49 | title: 'err params format:{}', 50 | test: { trans: 'test {p1}', params: { p1: '123' } }, 51 | expect: { result: 'test {p1}' }, 52 | }, 53 | { 54 | title: 'err params format:{ {}}', 55 | test: { trans: 'test { {p1}}', params: { p1: '123' } }, 56 | expect: { result: 'test { {p1}}' }, 57 | }, 58 | { 59 | title: 'valid params format:{{ }}', 60 | test: { trans: 'test {{ p1 }} and {{p1}} and {{ p1 }}', params: { p1: '123' } }, 61 | expect: { result: 'test 123 and 123 and 123' }, 62 | }, 63 | { 64 | title: 'err params format:{{{}}}', 65 | test: { trans: 'test {{{p1}}}', params: { p1: '123' } }, 66 | expect: { result: 'test {123}' }, 67 | }, 68 | { 69 | title: 'err params format:[]', 70 | test: { trans: 'test [p1]', params: { p1: '123' } }, 71 | expect: { result: 'test [p1]' }, 72 | }, 73 | { 74 | title: 'err params format:[[]]', 75 | test: { trans: 'test [[p2]]', params: { p2: '123' } }, 76 | expect: { result: 'test [[p2]]' }, 77 | }, 78 | { 79 | title: 'param key contain with $', 80 | test: { 81 | trans: 'This is {{$p1}} and {{p2$}} and {{p$p}}', 82 | params: { $p1: '123', p2$: 'abc', p$p: 'test' }, 83 | }, 84 | expect: { result: 'This is 123 and abc and test' }, 85 | }, 86 | { 87 | title: 'param key contain with _', 88 | test: { 89 | trans: 'This is {{_p1}} and {{p2_}} and {{p_p}}', 90 | params: { _p1: '123', p2_: 'abc', p_p: 'test' }, 91 | }, 92 | expect: { result: 'This is 123 and abc and test' }, 93 | }, 94 | { 95 | title: 'param key contain with number', 96 | test: { trans: 'This is {{1p1}} and {{p22}}', params: { '1p1': '123', p22: 'abc' } }, 97 | expect: { result: 'This is {{1p1}} and abc' }, 98 | }, 99 | { 100 | title: 'param key contain with other symbol', 101 | test: { 102 | trans: 103 | 'This is {{p!}}, {{p@}}, {{p#}}, {{p%}}, {{p^}}, {{p……}}, {{p&}}, {{p*}}, {{p(}}, {{p)}}, {{p-}}, {{p+}}, {{p=}},{{p;}}, {{p:}}, {{p\'}},{{p"}}, {{p<}}, {{p>}}, {{p,}},{{p.}}, {{p\\}},{{p|}},{{p/}},{{p?}},{{p[}},{{p]}}', 104 | params: { 105 | 'p!': '123', 106 | 'p@': 'abc', 107 | 'p#': 'abc', 108 | 'p%': 'abc', 109 | 'p^': 'abc', 110 | 'p……': 'abc', 111 | 'p&': 'abc', 112 | 'p*': 'abc', 113 | 'p(': 'abc', 114 | 'p)': 'abc', 115 | 'p-': 'abc', 116 | 'p+': 'abc', 117 | 'p=': 'abc', 118 | 'p;': 'abc', 119 | 'p:': 'abc', 120 | "p'": 'abc', 121 | 'p"': 'abc', 122 | 'p<': 'abc', 123 | 'p>': 'abc', 124 | 'p,': 'abc', 125 | 'p.': 'abc', 126 | 'p\\': 'abc', 127 | 'p|': 'abc', 128 | 'p/': 'abc', 129 | 'p[': 'abc', 130 | 'p]': 'abc', 131 | }, 132 | }, 133 | expect: { 134 | result: 135 | 'This is {{p!}}, {{p@}}, {{p#}}, {{p%}}, {{p^}}, {{p……}}, {{p&}}, {{p*}}, {{p(}}, {{p)}}, {{p-}}, {{p+}}, {{p=}},{{p;}}, {{p:}}, {{p\'}},{{p"}}, {{p<}}, {{p>}}, {{p,}},{{p.}}, {{p\\}},{{p|}},{{p/}},{{p?}},{{p[}},{{p]}}', 136 | }, 137 | }, 138 | { 139 | title: 'param key contain with whitespace', 140 | test: { trans: 'This is {{ p1}} and {{p2 }}', params: { p1: '123', p2: 'abc' } }, 141 | expect: { result: 'This is 123 and abc' }, 142 | }, 143 | ]; 144 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/components/nb-trans-subcontent/test/nb-trans-subcontent.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { 3 | ChangeDetectorRef, 4 | Component, 5 | TemplateRef, 6 | ViewChild, 7 | ElementRef, 8 | inject, 9 | } from '@angular/core'; 10 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 11 | import { NbTransTestingModule } from '../../../testing'; 12 | import { NbTransSubcontentComponent } from '../nb-trans-subcontent.component'; 13 | 14 | @Component({ 15 | selector: 'mock-tpl-ref', 16 | template: ` 17 | {{ content }} 18 | 19 | 20 | @for (item of list; track item) { 21 |

{{ item }}

22 | } 23 |
24 | `, 25 | // eslint-disable-next-line @angular-eslint/prefer-standalone 26 | standalone: false, 27 | }) 28 | export class MockTplRefComponent { 29 | @ViewChild('tplRef') tplRef!: TemplateRef; 30 | @ViewChild('tplRefWithList') tplRefWithList!: TemplateRef; 31 | 32 | content = 'mock templateRef content'; 33 | } 34 | 35 | describe('Component: NbTransSubcontent', () => { 36 | describe('used in normal component', () => { 37 | let component: NbTransSubcontentComponent; 38 | let fixture: ComponentFixture; 39 | let hostEle: HTMLElement; 40 | 41 | beforeEach(async () => { 42 | await TestBed.configureTestingModule({ 43 | imports: [NbTransTestingModule], 44 | declarations: [MockTplRefComponent], 45 | }).compileComponents(); 46 | }); 47 | 48 | beforeEach(() => { 49 | fixture = TestBed.createComponent(NbTransSubcontentComponent); 50 | component = fixture.componentInstance; 51 | fixture.detectChanges(); 52 | hostEle = fixture.debugElement.nativeElement; 53 | }); 54 | 55 | it('should be created', () => { 56 | expect(component).toBeTruthy(); 57 | }); 58 | 59 | it('the content is a string value', () => { 60 | const content = 'test content'; 61 | component.content = content; 62 | 63 | detectChanges(); 64 | 65 | expect(hostEle.textContent?.trim()).toEqual(content); 66 | }); 67 | 68 | it('the content is a templateRef type value', () => { 69 | const mockTplRefFixture = TestBed.createComponent(MockTplRefComponent); 70 | const mockTplRefComp = mockTplRefFixture.componentInstance; 71 | mockTplRefFixture.detectChanges(); 72 | 73 | const content = mockTplRefComp.tplRef; 74 | component.content = content; 75 | 76 | detectChanges(); 77 | 78 | expect(hostEle.textContent?.trim()).toEqual(mockTplRefComp.content); 79 | }); 80 | 81 | it('the content is a templateRef type value with string list param', () => { 82 | const mockList = ['mock list 1', 'mock list 2']; 83 | 84 | const mockTplRefFixture = TestBed.createComponent(MockTplRefComponent); 85 | const mockTplRefComp = mockTplRefFixture.componentInstance; 86 | mockTplRefFixture.detectChanges(); 87 | 88 | const content = mockTplRefComp.tplRefWithList; 89 | component.content = content; 90 | component.subcontentList = mockList; 91 | 92 | detectChanges(); 93 | 94 | const listFromDom = Array.from(hostEle.querySelectorAll('p')).map(item => 95 | item.textContent?.trim() 96 | ); 97 | expect(listFromDom).toEqual(mockList); 98 | }); 99 | 100 | function detectChanges() { 101 | const changeDR = fixture.componentRef.injector.get(ChangeDetectorRef); 102 | changeDR.markForCheck(); 103 | fixture.detectChanges(); 104 | } 105 | }); 106 | 107 | describe('used in standalone component', () => { 108 | [ 109 | { 110 | title: 'imported by standalone component', 111 | createComp: () => TestBed.createComponent(StandaloneComponent), 112 | }, 113 | { 114 | title: 'imported by ngModule', 115 | createComp: () => TestBed.createComponent(StandaloneComponentWithNgModule), 116 | }, 117 | ].forEach(item => { 118 | it(item.title, () => { 119 | const fixture = item.createComp(); 120 | const component = fixture.componentInstance; 121 | const content = 'test content'; 122 | component.content = content; 123 | fixture.detectChanges(); 124 | 125 | expect(component.textContent).toEqual(content); 126 | }); 127 | }); 128 | }); 129 | }); 130 | 131 | const StandaloneCompConfig = { 132 | standalone: true, 133 | imports: [NbTransSubcontentComponent], 134 | template: ``, 135 | }; 136 | 137 | @Component(StandaloneCompConfig) 138 | class StandaloneComponent { 139 | private elementRef: ElementRef = inject(ElementRef); 140 | 141 | content: string = ''; 142 | 143 | get textContent() { 144 | return this.elementRef.nativeElement.textContent?.trim(); 145 | } 146 | } 147 | 148 | @Component({ 149 | ...StandaloneCompConfig, 150 | imports: [NbTransTestingModule], 151 | }) 152 | // eslint-disable-next-line @angular-eslint/component-class-suffix 153 | class StandaloneComponentWithNgModule extends StandaloneComponent {} 154 | -------------------------------------------------------------------------------- /docs/main-HLG3KWLN.js: -------------------------------------------------------------------------------- 1 | import{A as O,B as E,C as m,D as C,E as P,G as p,H as c,I as f,K as j,L as R,M as V,N as W,O as w,P as z,Q as $,R as G,S as H,U as K,V as g,W as U,X as Z,Y as v,Z as J,ba as Y,c as F,ca as q,d as A,f as _,g as S,h as x,i as k,j as u,k as B,l as D,o as y,p as T,q as I,r as N,s as i,t as e,u as l,v as L,w as d,x as M,z as t}from"./chunk-73BIQRKQ.js";var Q=(()=>{let r=class r{constructor(o){this.transService=o}resolve(){return F(this.transService.subscribeLoadDefaultOver())}};r.\u0275fac=function(a){return new(a||r)(_(v))},r.\u0275prov=A({token:r,factory:r.\u0275fac,providedIn:"root"});let n=r;return n})();var X=[{path:"",resolve:{defalutLangLoadOver:Q},children:[{path:"",loadChildren:()=>import("./chunk-AT77VTST.js").then(n=>n.Feature1Module)},{path:"standalone",loadChildren:()=>import("./chunk-FQRN3WBV.js").then(n=>n.routes)}]}];var ee={providers:[z($()),{provide:K,useValue:g.ZH_CN},{provide:U,useFactory:n=>({[g.EN]:()=>n.get("./assets/localization/en/translations.json"),[g.ZH_CN]:()=>F(n.get("./assets/localization/zh-CN/translations.json"))}),deps:[w]},{provide:Z,useValue:!1},H(X)]};var re=()=>({prefix:"content"}),ae=n=>({prefix:"content",params:n}),oe=()=>({returnKeyWhenEmpty:!1});function ue(n,r){if(n&1){let h=L();i(0,"a",5),d("click",function(){let a=S(h).$implicit,s=M();return x(s.go2Link(a))}),t(1),e()}if(n&2){let h=r.$implicit;N("href",h.link,k),u(),E(" ",h.title," ")}}var te=(()=>{let r=class r{get title(){return this.transService.translationSync("title")}get lang(){return this.transService.lang}constructor(o,a){this.gtagService=o,this.transService=a,this.params={params1:"{{params2}}",params2:"1111",params3:"2222"},this.links=[{title:"Changelog",link:"https://github.com/bigBear713/nb-trans/blob/main/CHANGELOG.md"},{title:"Document",link:"https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md"}]}ngOnInit(){this.title$=this.transService.translationAsync("title")}go2Link(o){this.gtagService.trackLink({link_name:o.title,link:o.link})}onChangeLang(o){this.transService.changeLang(o).subscribe(a=>{console.log(a),a.result||alert("\u5207\u6362\u8BED\u8A00\u5931\u8D25\uFF0C\u6CA1\u6709\u5BFC\u5165\u8BE5\u8BED\u8A00\u5305,\u5F53\u524D\u8BED\u8A00\u662F:"+a.curLang),this.gtagService.trackButton({button_name:o==="zh-CN"?"\u5207\u6362\u4E3A\u4E2D\u6587":o==="en"?"\u5207\u6362\u4E3A\u82F1\u6587":"\u5207\u6362\u4E3A\u5176\u5B83\u4E0D\u5B58\u5728\u7684\u8BED\u8A00",language:a.curLang})})}};r.\u0275fac=function(a){return new(a||r)(B(q),B(v))},r.\u0275cmp=D({type:r,selectors:[["app-root"]],decls:62,vars:40,consts:[["target","_blank",3,"href"],[1,"actions"],[3,"click"],["routerLink","/"],["routerLink","/standalone"],["target","_blank",3,"click","href"]],template:function(a,s){a&1&&(i(0,"h2"),T(1,ue,2,2,"a",0,y),e(),l(3,"hr"),i(4,"div",1)(5,"button",2),d("click",function(){return s.onChangeLang("zh-CN")}),t(6,"\u5207\u6362\u4E3A\u4E2D\u6587"),e(),i(7,"button",2),d("click",function(){return s.onChangeLang("en")}),t(8,"\u5207\u6362\u4E3A\u82F1\u6587"),e(),i(9,"button",2),d("click",function(){return s.onChangeLang("other")}),t(10,"\u5207\u6362\u4E3A\u5176\u5B83\u4E0D\u5B58\u5728\u7684\u8BED\u8A00"),e(),l(11,"hr"),e(),i(12,"div")(13,"h5"),t(14,"\u4F7F\u7528 nbTrans \u7BA1\u9053\uFF0C\u8BED\u8A00\u5207\u6362\u65F6\u81EA\u52A8\u83B7\u53D6\u6700\u65B0\u7684\u7FFB\u8BD1"),e(),t(15),p(16,"nbTrans"),e(),l(17,"hr"),i(18,"div")(19,"h5"),t(20,"\u4F7F\u7528 nbTrans \u7BA1\u9053\uFF0Ckey\u503C\u4E3A\u591A\u5C42"),e(),t(21),p(22,"nbTrans"),e(),l(23,"hr"),i(24,"div")(25,"h5"),t(26),p(27,"json"),e(),i(28,"p"),t(29),p(30,"nbTrans"),e(),t(31),p(32,"nbTrans"),e(),l(33,"hr"),i(34,"div")(35,"h5"),t(36,"\u901A\u8FC7getter\uFF0C\u8C03\u7528translationSync()\u65F6\u65F6\u83B7\u53D6\u6700\u65B0\u7684\u7FFB\u8BD1"),e(),t(37),e(),l(38,"hr"),i(39,"div")(40,"h5"),t(41," \u8C03\u7528translationAsync()\u5F97\u5230\u4E00\u4E2AObservable,\u7ED3\u5408 async \u7BA1\u9053\u4F7F\u7528\uFF0C\u8BED\u8A00\u5207\u6362\u65F6\u81EA\u52A8\u83B7\u53D6\u6700\u65B0\u7684\u7FFB\u8BD1 "),e(),i(42,"div"),t(43),e(),t(44),p(45,"async"),e(),l(46,"hr"),i(47,"div"),t(48),p(49,"nbTrans"),e(),l(50,"hr"),i(51,"div"),t(52),p(53,"nbTrans"),e(),l(54,"hr"),i(55,"h3"),t(56,"children component"),e(),i(57,"a",3),t(58,"Module Component"),e(),i(59,"a",4),t(60,"Standalone Component"),e(),l(61,"router-outlet")),a&2&&(u(),I(s.links),u(14),m(" ","{{'title'| nbTrans}}\uFF1A"," ",c(16,17,"title"),` 2 | `),u(6),m(" ","{{'content.helloWorld'| nbTrans}}:"," ",c(22,19,"content.helloWorld"),` 3 | `),u(5),E(" \u4F7F\u7528 nbTrans \u7BA1\u9053\uFF0C\u5E26\u6709options\u53C2\u6570\u3002 \u8BBE\u7F6Ekey\u503C\u524D\u7F00\u548C\u7FFB\u8BD1\u6587\u672C\u4E2D\u7684\u53C2\u6570,params\u53C2\u6570\u4E3A\uFF1A ",c(27,21,s.params)," "),u(3),E("\u7FFB\u8BD1\u6587\u672C\u539F\u6587\uFF1A",f(30,23,"contentWithParams",C(36,re))),u(2),m(" ","{{'contentWithParams'| nbTrans:({prefix:'content',params: params})}}:"," ",f(32,26,"contentWithParams",P(37,ae,s.params)),` 4 | `),u(6),m(" ","get title(){return this.transService.translationSync('title');}:"," ",s.title,` 5 | `),u(6),O("this.title$ = this.transService.translationAsync('title');// ts"),u(),m(" ","{{title$ | async}}:"," ",c(45,29,s.title$),` 6 | `),u(4),m(" ","{{'test.test'| nbTrans}}","\uFF0C\u5F53key\u5BF9\u5E94\u7684\u5185\u5BB9\u4E0D\u5B58\u5728, \u9ED8\u8BA4\u8FD4\u56DEkey: ",c(49,31,"test.test"),` 7 | `),u(4),m(" ","{{'test.test'| nbTrans:({returnKeyWhenEmpty:false})}}","\uFF0C\u5F53key\u5BF9\u5E94\u7684\u5185\u5BB9\u4E0D\u5B58\u5728, \u4E5F\u53EF\u4EE5\u8BBE\u7F6E\u8FD4\u56DE\u7A7A\u5B57\u7B26\u4E32: ",f(53,33,"test.test",C(39,oe)),` 8 | `))},dependencies:[V,G,Y,j,R,J],styles:["h2[_ngcontent-%COMP%] a[_ngcontent-%COMP%]{margin:0 8px}.actions[_ngcontent-%COMP%]{position:sticky;top:0;background-color:#fff}a[_ngcontent-%COMP%]{margin:0 5px}"],changeDetection:0});let n=r;return n})();W(te,ee).catch(n=>console.error(n)); 9 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "nb-trans-demo": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular/build:application", 19 | "options": { 20 | "outputPath": "dist/nb-trans-demo", 21 | "index": "src/index.html", 22 | "browser": "src/main.ts", 23 | "polyfills": ["zone.js"], 24 | "tsConfig": "tsconfig.app.json", 25 | "inlineStyleLanguage": "scss", 26 | "assets": ["src/favicon.ico", "src/assets"], 27 | "styles": ["src/styles.scss"], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "production": { 32 | "budgets": [ 33 | { 34 | "type": "initial", 35 | "maximumWarning": "500kb", 36 | "maximumError": "1mb" 37 | }, 38 | { 39 | "type": "anyComponentStyle", 40 | "maximumWarning": "2kb", 41 | "maximumError": "4kb" 42 | } 43 | ], 44 | "outputHashing": "all" 45 | }, 46 | "deploy": { 47 | "budgets": [ 48 | { 49 | "type": "initial", 50 | "maximumWarning": "500kb", 51 | "maximumError": "1mb" 52 | }, 53 | { 54 | "type": "anyComponentStyle", 55 | "maximumWarning": "2kb", 56 | "maximumError": "4kb" 57 | } 58 | ], 59 | "fileReplacements": [], 60 | "outputHashing": "all", 61 | "outputPath": { 62 | "base": "docs", 63 | "browser": "" 64 | }, 65 | "baseHref": "/nb-trans/" 66 | }, 67 | "development": { 68 | "optimization": false, 69 | "extractLicenses": false, 70 | "sourceMap": true 71 | } 72 | }, 73 | "defaultConfiguration": "production" 74 | }, 75 | "serve": { 76 | "builder": "@angular/build:dev-server", 77 | "configurations": { 78 | "production": { 79 | "buildTarget": "nb-trans-demo:build:production" 80 | }, 81 | "development": { 82 | "buildTarget": "nb-trans-demo:build:development" 83 | } 84 | }, 85 | "defaultConfiguration": "development" 86 | }, 87 | "extract-i18n": { 88 | "builder": "@angular/build:extract-i18n", 89 | "options": { 90 | "buildTarget": "nb-trans-demo:build" 91 | } 92 | }, 93 | "test": { 94 | "builder": "@angular/build:karma", 95 | "options": { 96 | "polyfills": ["zone.js", "zone.js/testing"], 97 | "tsConfig": "tsconfig.spec.json", 98 | "inlineStyleLanguage": "scss", 99 | "assets": ["src/favicon.ico", "src/assets"], 100 | "styles": ["src/styles.scss"], 101 | "scripts": [] 102 | } 103 | }, 104 | "lint": { 105 | "builder": "@angular-eslint/builder:lint", 106 | "options": { 107 | "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] 108 | } 109 | } 110 | } 111 | }, 112 | "nb-trans": { 113 | "projectType": "library", 114 | "root": "projects/nb-trans", 115 | "sourceRoot": "projects/nb-trans/src", 116 | "prefix": "lib", 117 | "architect": { 118 | "build": { 119 | "builder": "@angular/build:ng-packagr", 120 | "options": { 121 | "project": "projects/nb-trans/ng-package.json" 122 | }, 123 | "configurations": { 124 | "production": { 125 | "tsConfig": "projects/nb-trans/tsconfig.lib.prod.json" 126 | }, 127 | "development": { 128 | "tsConfig": "projects/nb-trans/tsconfig.lib.json" 129 | } 130 | }, 131 | "defaultConfiguration": "production" 132 | }, 133 | "test": { 134 | "builder": "@angular/build:karma", 135 | "options": { 136 | "tsConfig": "projects/nb-trans/tsconfig.spec.json", 137 | "polyfills": ["zone.js", "zone.js/testing"], 138 | "watch": true, 139 | "progress": true, 140 | "codeCoverage": true 141 | } 142 | }, 143 | "lint": { 144 | "builder": "@angular-eslint/builder:lint", 145 | "options": { 146 | "lintFilePatterns": ["projects/**/*.ts", "projects/**/*.html"] 147 | } 148 | } 149 | } 150 | } 151 | }, 152 | "cli": { 153 | "analytics": "d069ab4c-f6c2-4179-96b6-5c058dd3384a", 154 | "schematicCollections": ["@angular-eslint/schematics"] 155 | }, 156 | "schematics": { 157 | "@schematics/angular:component": { 158 | "type": "component" 159 | }, 160 | "@schematics/angular:directive": { 161 | "type": "directive" 162 | }, 163 | "@schematics/angular:service": { 164 | "type": "service" 165 | }, 166 | "@schematics/angular:guard": { 167 | "typeSeparator": "." 168 | }, 169 | "@schematics/angular:interceptor": { 170 | "typeSeparator": "." 171 | }, 172 | "@schematics/angular:module": { 173 | "typeSeparator": "." 174 | }, 175 | "@schematics/angular:pipe": { 176 | "typeSeparator": "." 177 | }, 178 | "@schematics/angular:resolver": { 179 | "typeSeparator": "." 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/services/test/nb-trans-tools.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { NbTransToolsService } from '../nb-trans-tools.service'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { INbTransParams } from '../../models'; 4 | import { handleSentenceWithParamsTestData } from '../../testing'; 5 | import { NbCommonTestingModule } from '@bigbear713/nb-common'; 6 | import { NB_TRANS_PARAM_KEY_INVALID_WARNING } from '../../constants'; 7 | 8 | describe('Service: NbTransTools', () => { 9 | let service: NbTransToolsService; 10 | 11 | beforeEach(async () => { 12 | await TestBed.configureTestingModule({ 13 | imports: [NbCommonTestingModule], 14 | providers: [ 15 | NbTransToolsService, 16 | { provide: NB_TRANS_PARAM_KEY_INVALID_WARNING, useValue: false }, 17 | ], 18 | }); 19 | }); 20 | 21 | beforeEach(() => { 22 | service = TestBed.inject(NbTransToolsService); 23 | }); 24 | 25 | it('should be created', () => { 26 | expect(service).toBeTruthy(); 27 | }); 28 | 29 | describe('#getFinalKey()', () => { 30 | [ 31 | { params: { key: 'transKey' }, expect: 'transKey' }, 32 | { params: { key: 'transKey', prefix: 'prefix' }, expect: 'prefix.transKey' }, 33 | ].forEach(item => { 34 | it(`the params is ${JSON.stringify(item.params)}`, () => { 35 | const { key, prefix } = item.params; 36 | const finalKey = service.getFinalKey(key, prefix); 37 | expect(finalKey).toEqual(item.expect); 38 | }); 39 | }); 40 | }); 41 | 42 | describe('#handleSentenceWithParams()', () => { 43 | handleSentenceWithParamsTestData.forEach(item => { 44 | it(item.title, () => { 45 | const params: INbTransParams | undefined = item.test.params; 46 | const result = service.handleSentenceWithParams(item.test.trans, params); 47 | expect(result).toEqual(item.expect.result); 48 | }); 49 | }); 50 | }); 51 | 52 | describe('#handleTrans()', () => { 53 | [ 54 | { params: { trans: 'str1' }, expect: ['str1'] }, 55 | { 56 | params: { trans: 'str1 <0>str2' }, 57 | expect: ['str1 ', { index: 0, content: 'str2', list: [] }], 58 | }, 59 | { 60 | params: { trans: ' str1 <0>str2 str3 ' }, 61 | expect: [' str1 ', { index: 0, content: 'str2', list: [] }, ' str3 '], 62 | }, 63 | { 64 | params: { trans: '<0>str1 str2 <0>str3 str4 <0> str5 ' }, 65 | expect: [ 66 | { index: 0, content: 'str1', list: [] }, 67 | ' str2 ', 68 | { index: 0, content: 'str3', list: [] }, 69 | ' str4 ', 70 | { index: 0, content: ' str5 ', list: [] }, 71 | ], 72 | }, 73 | { 74 | params: { 75 | trans: 76 | '<0><1>str1 str2 <0> str3 <2>str3 str3 str4 <0>str5 <5> str5 str5 <5> str5 str5', 77 | }, 78 | expect: [ 79 | { index: 0, content: '<1>str1', list: [{ index: 1, content: 'str1', list: [] }] }, 80 | ' str2 ', 81 | { 82 | index: 0, 83 | content: ' str3 <2>str3 str3 ', 84 | list: [' str3 ', { index: 2, content: 'str3', list: [] }, ' str3 '], 85 | }, 86 | ' str4 ', 87 | { 88 | index: 0, 89 | content: 'str5 <5> str5 str5 <5> str5 str5', 90 | list: [ 91 | 'str5 ', 92 | { index: 5, content: ' str5 ', list: [] }, 93 | ' str5 ', 94 | { index: 5, content: ' str5 ', list: [] }, 95 | ' str5', 96 | ], 97 | }, 98 | ], 99 | }, 100 | ].forEach(item => { 101 | it(`the params is ${JSON.stringify(item.params)}`, () => { 102 | const { trans } = item.params; 103 | const handleResult = service.handleTrans(trans); 104 | expect(handleResult).toEqual(item.expect); 105 | }); 106 | }); 107 | }); 108 | 109 | it('#NbTransToolsService.checkWindow()', () => { 110 | expect(NbTransToolsService.checkWindow()).toEqual(true); 111 | }); 112 | 113 | it('#NbTransToolsService.checkNavigator()', () => { 114 | expect(NbTransToolsService.checkNavigator()).toEqual(true); 115 | }); 116 | 117 | describe('#isTranslatedStringValid()', () => { 118 | [ 119 | { title: 'value is undefined', trans: undefined, expect: false }, 120 | { title: 'value is string', trans: 'abc', expect: true }, 121 | { title: 'value is empty string', trans: '', expect: false }, 122 | { title: 'value only include some whitespace', trans: ' ', expect: true }, 123 | { title: 'value is number', trans: 123, expect: false }, 124 | { title: 'value is boolean', trans: true, expect: false }, 125 | { title: 'value is array', trans: [], expect: false }, 126 | { title: 'value is symbol', trans: Symbol(), expect: false }, 127 | { title: 'value is object', trans: { p1: 'abc' }, expect: false }, 128 | { title: 'value is function', trans: () => true, expect: false }, 129 | ].forEach(item => { 130 | it(item.title, () => { 131 | expect(service.isTranslatedStringValid(item.trans)).toEqual(item.expect); 132 | }); 133 | }); 134 | }); 135 | 136 | describe('Do not print the warning info about the invalid param key', () => { 137 | let service2: NbTransToolsService; 138 | 139 | beforeEach(async () => { 140 | TestBed.resetTestingModule(); 141 | await TestBed.configureTestingModule({ 142 | imports: [NbCommonTestingModule], 143 | providers: [ 144 | NbTransToolsService, 145 | { provide: NB_TRANS_PARAM_KEY_INVALID_WARNING, useValue: true }, 146 | ], 147 | }); 148 | service2 = TestBed.inject(NbTransToolsService); 149 | }); 150 | 151 | it('will not print warning info when the param key is invalid', () => { 152 | const spyFn = spyOn(console, 'warn').and.callThrough(); 153 | 154 | const trans = 'This is {{p!}}'; 155 | const params = { 'p!': '123' }; 156 | service2.handleSentenceWithParams(trans, params); 157 | expect(spyFn).toHaveBeenCalledTimes(1); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/pipes/test/nb-trans.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectorRef, Component, ElementRef, inject } from '@angular/core'; 3 | import { TestBed } from '@angular/core/testing'; 4 | import { switchMap, take } from 'rxjs/operators'; 5 | import { NB_TRANS_LOADER, NB_TRANS_DEFAULT_LANG, NbTransLang } from '../../constants'; 6 | import { INbTransOptions } from '../../models'; 7 | import { NbTransService } from '../../services'; 8 | import { translationSyncTestData, transLoader, NbTransTestingModule } from '../../testing'; 9 | import { NbTransPipe } from '../nb-trans.pipe'; 10 | import { isEqual } from 'lodash-es'; 11 | 12 | describe('Pipe: NbTrans', () => { 13 | describe('used in normal component', () => { 14 | let pipe: NbTransPipe; 15 | let transService: NbTransService; 16 | 17 | beforeEach(async () => { 18 | await TestBed.configureTestingModule({ 19 | imports: [CommonModule, NbTransTestingModule], 20 | declarations: [], 21 | providers: [ 22 | { 23 | provide: ChangeDetectorRef, 24 | useValue: jasmine.createSpyObj(ChangeDetectorRef, ['markForCheck']), 25 | }, 26 | { provide: NB_TRANS_DEFAULT_LANG, useValue: NbTransLang.ZH_CN }, 27 | { provide: NB_TRANS_LOADER, useValue: transLoader.dynamicLoader }, 28 | NbTransService, 29 | ], 30 | }).compileComponents(); 31 | }); 32 | 33 | beforeEach(() => { 34 | pipe = TestBed.runInInjectionContext(() => new NbTransPipe()); 35 | transService = TestBed.inject(NbTransService); 36 | }); 37 | 38 | beforeEach(async () => { 39 | await transService.subscribeLoadDefaultOver().toPromise(); 40 | }); 41 | 42 | it('create an instance', () => { 43 | expect(pipe).toBeTruthy(); 44 | }); 45 | 46 | describe('#transform()', () => { 47 | translationSyncTestData 48 | .map(item => { 49 | const expect = { 50 | resultZHCN: item.expect.result, 51 | resultEN: item.expect.result, 52 | }; 53 | // This test data can get right result, so the result has to be handled with Chinese and English 54 | if (isEqual({ key: 'helloWorld', options: { prefix: 'content' } }, item.test)) { 55 | expect.resultEN = 'hello world'; 56 | expect.resultZHCN = '你好,世界'; 57 | } 58 | return { 59 | ...item, 60 | expect, 61 | }; 62 | }) 63 | .forEach(item => { 64 | it(item.title, done => { 65 | const verifyResult = (expectResult: string) => { 66 | const result = pipe.transform(item.test.key, item.test.options); 67 | expect(result).toEqual(expectResult); 68 | }; 69 | verifyResult(item.expect.resultZHCN); 70 | 71 | transService 72 | .changeLang(NbTransLang.EN) 73 | .pipe(take(1)) 74 | .subscribe(() => { 75 | verifyResult(item.expect.resultEN); 76 | done(); 77 | }); 78 | }); 79 | }); 80 | }); 81 | 82 | it('#ngOnDestroy()', done => { 83 | transService 84 | .changeLang(NbTransLang.EN) 85 | .pipe( 86 | switchMap(() => { 87 | pipe.ngOnDestroy(); 88 | spyOn(transService, 'translationAsync').and.callThrough(); 89 | return transService.changeLang(NbTransLang.ZH_CN); 90 | }), 91 | take(1) 92 | ) 93 | .subscribe(() => { 94 | expect(transService.translationAsync).toHaveBeenCalledTimes(0); 95 | done(); 96 | }); 97 | }); 98 | 99 | it('verify the trans text will be updated when options has been updated', () => { 100 | let options: INbTransOptions = { prefix: 'content' }; 101 | const result1 = pipe.transform('helloWorld', options); 102 | expect(result1).toEqual('你好,世界'); 103 | 104 | options = { prefix: undefined }; 105 | const result2 = pipe.transform('helloWorld', options); 106 | expect(result2).toEqual('你好,世界!'); 107 | }); 108 | 109 | it('verify the trans text will be updated when key has been updated', () => { 110 | const result1 = pipe.transform('content.helloWorld'); 111 | expect(result1).toEqual('你好,世界'); 112 | 113 | const result2 = pipe.transform('helloWorld'); 114 | expect(result2).toEqual('你好,世界!'); 115 | }); 116 | }); 117 | 118 | describe('used in standalone component', () => { 119 | beforeEach(async () => { 120 | await TestBed.configureTestingModule({ 121 | providers: [ 122 | { provide: NB_TRANS_DEFAULT_LANG, useValue: NbTransLang.ZH_CN }, 123 | { provide: NB_TRANS_LOADER, useValue: transLoader.staticLoader }, 124 | ], 125 | }).compileComponents(); 126 | const transService = TestBed.inject(NbTransService); 127 | await transService.subscribeLoadDefaultOver().toPromise(); 128 | }); 129 | 130 | [ 131 | { 132 | title: 'imported by standalone component', 133 | createComp: () => TestBed.createComponent(StandaloneComponent), 134 | }, 135 | { 136 | title: 'imported by ngModule', 137 | createComp: () => TestBed.createComponent(StandaloneComponentWithNgModule), 138 | }, 139 | ].forEach(item => { 140 | it(item.title, () => { 141 | const fixture = item.createComp(); 142 | const component = fixture.componentInstance; 143 | fixture.detectChanges(); 144 | 145 | expect(component.textContent).toEqual('你好,世界'); 146 | }); 147 | }); 148 | }); 149 | }); 150 | 151 | const StandaloneCompConfig = { 152 | standalone: true, 153 | imports: [NbTransPipe], 154 | template: `{{key|nbTrans:options}}`, 155 | }; 156 | 157 | @Component(StandaloneCompConfig) 158 | class StandaloneComponent { 159 | private elementRef: ElementRef = inject(ElementRef); 160 | key = 'helloWorld'; 161 | options: INbTransOptions = { prefix: 'content' }; 162 | 163 | get textContent() { 164 | return this.elementRef.nativeElement.textContent?.trim(); 165 | } 166 | } 167 | 168 | @Component({ 169 | ...StandaloneCompConfig, 170 | imports: [NbTransTestingModule], 171 | }) 172 | // eslint-disable-next-line @angular-eslint/component-class-suffix 173 | class StandaloneComponentWithNgModule extends StandaloneComponent {} 174 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/services/nb-trans-tools.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, inject, isDevMode } from '@angular/core'; 2 | import { INbTransSentencePart, INbTransParams } from '../models'; 3 | import { NbValueTypeService } from '@bigbear713/nb-common'; 4 | import { 5 | nbParamKeyRegExp, 6 | nbParamKeyRegExp2Split, 7 | nbParamKeyRegExpRules, 8 | } from '../constants/nb-param-key-regexp'; 9 | import { NB_TRANS_PARAM_KEY_INVALID_WARNING } from '../constants'; 10 | 11 | const isInDevMode = isDevMode(); 12 | 13 | @Injectable({ providedIn: 'root' }) 14 | export class NbTransToolsService { 15 | private warnParamKeyInvalid: boolean | null = inject(NB_TRANS_PARAM_KEY_INVALID_WARNING, { 16 | optional: true, 17 | }); 18 | private valueType: NbValueTypeService = inject(NbValueTypeService); 19 | 20 | static checkNavigator(): boolean { 21 | return NbTransToolsService.checkWindow() && typeof window.navigator !== 'undefined'; 22 | } 23 | 24 | static checkWindow(): boolean { 25 | return typeof window !== 'undefined'; 26 | } 27 | 28 | constructor() { 29 | this.setWarnParamKeyInvalidDefault(); 30 | } 31 | 32 | getFinalKey(key: string, prefix?: string): string { 33 | return prefix ? `${prefix}.${key}` : key; 34 | } 35 | 36 | handleSentenceWithParams(trans: string, params?: INbTransParams): string { 37 | if (!params) { 38 | return trans; 39 | } 40 | 41 | const paramsKeys = Object.keys(params); 42 | if (!paramsKeys.length) { 43 | return trans; 44 | } 45 | 46 | // if the cleanedParams is empty, no need to split the trans string, 47 | // return trans string directly, and the performance is improved 48 | const cleanedParams = this.cleanParams(params, paramsKeys); 49 | if (!Object.keys(cleanedParams).length) { 50 | return trans; 51 | } 52 | // First, split the trans string to string array via params key, 53 | // like this: 'This is {{p1}} and {{p2}} and {{p1}}.' --> 54 | // ['This is ','{{p1}}',' and ','{{p2}}',' and ','{{p1}}','.']. 55 | // Then replace the params key as params value, like this: 56 | // ['This is ','param1',' and ','param2',' and ','param1','.'] 57 | // Last, make array join as string, like this: 'This is param1 and param2 and param1.' 58 | const splitStrArr = trans.split(nbParamKeyRegExp2Split); 59 | return this.replaceAsParamsValueInSplitArr(splitStrArr, cleanedParams).join(''); 60 | } 61 | 62 | handleTrans(trans: string): INbTransSentencePart[] { 63 | const sentenceList: INbTransSentencePart[] = []; 64 | while (trans.length) { 65 | const firstStartFlagIndex = trans.search(/<\d+>/); 66 | if (firstStartFlagIndex > 0) { 67 | const contentBeforeFirstComp = trans.slice(0, firstStartFlagIndex); 68 | sentenceList.push(contentBeforeFirstComp); 69 | } 70 | 71 | const handleResult = this.handleCompStr(trans); 72 | if (this.valueType.isString(handleResult)) { 73 | sentenceList.push(handleResult); 74 | trans = ''; 75 | } else { 76 | sentenceList.push({ 77 | index: handleResult.index, 78 | content: handleResult.content, 79 | list: handleResult.list, 80 | }); 81 | trans = handleResult.otherContent; 82 | } 83 | } 84 | return sentenceList; 85 | } 86 | 87 | /** 88 | * verify the trans is valid. 89 | * If it is undefined, empty string('') or the value is not a string type, 90 | * will return false 91 | * @param trans 92 | * @returns 93 | */ 94 | isTranslatedStringValid(trans: unknown): boolean { 95 | return !!(trans && this.valueType.isString(trans)); 96 | } 97 | 98 | private cleanParams(params: INbTransParams, paramsKeys: string[]) { 99 | // because after calling RegExp's test function, the lastIndex value will be changed, so have to set it as 0. 100 | // so create a new value every time to make sure the regexp will not affect anywhere 101 | const paramKeyRegExp = new RegExp(`{{${nbParamKeyRegExpRules}}}`); 102 | return paramsKeys 103 | .filter(key => { 104 | const isValid = paramKeyRegExp.test(`{{${key}}}`); 105 | paramKeyRegExp.lastIndex = 0; 106 | 107 | if (!isValid) this.logParamKeyIsInvalid(key); 108 | return isValid; 109 | }) 110 | .reduce((prev, key) => { 111 | prev[key] = params[key]; 112 | return prev; 113 | }, {} as INbTransParams); 114 | } 115 | 116 | private handleCompStr(content: string) { 117 | const startFlagIndex = content.search(/<\d+>/); 118 | if (startFlagIndex === -1) { 119 | return content; 120 | } 121 | 122 | let list: INbTransSentencePart[] = []; 123 | const startFlagEndIndex = content.indexOf('>', startFlagIndex); 124 | const comIndex = Number(content.slice(startFlagIndex + 1, startFlagEndIndex)); 125 | 126 | const endFlag = ``; 127 | const endFlagIndex = content.indexOf(endFlag); 128 | const comContent = content.slice(startFlagEndIndex + 1, endFlagIndex); 129 | 130 | if (comContent.search(/<\d+>/) > -1) { 131 | list = this.handleTrans(comContent); 132 | } 133 | 134 | const otherContent = content.slice(endFlagIndex + endFlag.length, content.length); 135 | 136 | return { 137 | index: comIndex, 138 | content: comContent, 139 | list, 140 | otherContent, 141 | }; 142 | } 143 | 144 | private logParamKeyIsInvalid(paramKey: string) { 145 | if (!isInDevMode || !this.warnParamKeyInvalid) return; 146 | 147 | console.warn( 148 | `The param key: "${paramKey}" is invalid! 149 | It should consist of "letter", "number", "_" or "$", 150 | and the "number" can't be the first character. 151 | See this changelog: https://github.com/bigBear713/nb-trans/blob/main/CHANGELOG.md#v1600` 152 | ); 153 | } 154 | 155 | private replaceAsParamsValueInSplitArr(transSplitArr: string[], params: INbTransParams) { 156 | const isParamKeyRegExp = new RegExp(nbParamKeyRegExp2Split); 157 | const verifyIsParamKey = (data: string): boolean => { 158 | const isParamsKey = isParamKeyRegExp.test(data); 159 | isParamKeyRegExp.lastIndex = 0; 160 | return isParamsKey; 161 | }; 162 | 163 | transSplitArr.forEach((item, index) => { 164 | if (!verifyIsParamKey(item)) return; 165 | 166 | const key = item.match(nbParamKeyRegExp)![0]; 167 | const paramValue = params[key]; 168 | if (paramValue) { 169 | transSplitArr[index] = paramValue; 170 | } 171 | }); 172 | return transSplitArr; 173 | } 174 | 175 | private setWarnParamKeyInvalidDefault() { 176 | if (this.warnParamKeyInvalid !== false) { 177 | this.warnParamKeyInvalid = true; 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /docs/chunk-AT77VTST.js: -------------------------------------------------------------------------------- 1 | import{$ as H,A as d,B as C,C as F,D as S,E as y,F as _,G as u,H as c,I as h,J as A,K as V,L as G,M as J,T as w,Y as g,Z as q,_ as z,a as M,aa as D,b as N,ba as K,ca as Q,e as v,g as W,h as j,j as o,k as T,l as E,m as x,n as O,r as l,s as r,t as e,u as s,v as $,w as R,y as B,z as i}from"./chunk-73BIQRKQ.js";var U=(()=>{let t=class t{constructor(){this.comContent="",this.list=[]}ngOnInit(){}};t.\u0275fac=function(p){return new(p||t)},t.\u0275cmp=E({type:t,selectors:[["app-widget"]],inputs:{comContent:"comContent",list:"list"},standalone:!1,decls:1,vars:2,consts:[[3,"nb-trans-subcontent","subcontentList"]],template:function(p,a){p&1&&s(0,"a",0),p&2&&l("nb-trans-subcontent",a.comContent)("subcontentList",a.list)},dependencies:[D],styles:["a[_ngcontent-%COMP%]{color:#0ff;cursor:pointer}"],changeDetection:0});let n=t;return n})();var k=()=>({prefix:"content"}),it=n=>({prefix:"content",params:n}),X=(n,t,b)=>[n,t,b],Y=n=>({params:n,prefix:"content"});function ot(n,t){if(n&1&&s(0,"b",8),n&2){let b=t.content,m=t.list;l("nb-trans-subcontent",b)("subcontentList",m)}}function rt(n,t){if(n&1&&s(0,"app-widget",9),n&2){let b=t.content,m=t.list;l("comContent",b)("list",m)}}function at(n,t){if(n&1&&(r(0,"b"),i(1),e()),n&2){let b=t.content;o(),d(b)}}var Z=(()=>{let t=class t{get lang(){return this.transService.lang}get title(){return this.transService.translationSync("title")}constructor(m,p){this.gtagService=m,this.transService=p,this.params={params1:"{{params2}}",params2:"1111",params3:"2222","#p^":"test"},this.options={prefix:"content",params:this.params},this.compStr1=` 2 |
3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {{comContent}} 16 | 17 | `,this.compStr2=` 18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {{comContent}} 32 | 33 | `,this.browserLang="",this.browserLangs=[],this.trackPage()}ngOnInit(){this.title$=this.transService.translationAsync("title"),this.titleWithParams$=this.transService.translationAsync("content.contentWithParams",{params:this.params}),this.browserLang=g.getBrowserLang(),this.browserLangs=g.getBrowserLangs()}changeOptions(){let m=this.options.prefix?void 0:"content";this.options=N(M({},this.options),{prefix:m}),this.gtagService.trackButton({button_name:"change options"})}trackPage(){this.gtagService.trackPage({page_name:"Module Component"})}};t.\u0275fac=function(p){return new(p||t)(T(Q),T(g))},t.\u0275cmp=E({type:t,selectors:[["app-feature1"]],standalone:!1,decls:92,vars:84,consts:[["com0",""],["com1",""],["com2",""],[3,"click"],["key","contentWithParams",3,"options"],["key","complexContent",3,"components","options"],["nb-trans","contentWithParams",3,"nb-trans-options"],["nb-trans","complexContent",3,"nb-trans-components","nb-trans-options"],[3,"nb-trans-subcontent","subcontentList"],[3,"comContent","list"]],template:function(p,a){if(p&1){let f=$();r(0,"div")(1,"h5"),i(2,"\u4F7F\u7528 nbTrans \u7BA1\u9053\uFF0C\u8BED\u8A00\u5207\u6362\u65F6\u81EA\u52A8\u83B7\u53D6\u6700\u65B0\u7684\u7FFB\u8BD1"),e(),i(3),u(4,"nbTrans"),e(),s(5,"hr"),r(6,"div")(7,"h5"),i(8,"\u4F7F\u7528 nbTrans \u7BA1\u9053\uFF0Ckey\u503C\u4E3A\u591A\u5C42"),e(),i(9),u(10,"nbTrans"),e(),s(11,"hr"),r(12,"div")(13,"h5"),i(14),u(15,"json"),e(),r(16,"p"),i(17),u(18,"nbTrans"),e(),i(19),u(20,"nbTrans"),e(),r(21,"h5"),i(22),u(23,"json"),e(),r(24,"button",3),R("click",function(){return W(f),j(a.changeOptions())}),i(25,"change options"),e(),r(26,"p"),i(27),u(28,"nbTrans"),e(),s(29,"hr"),r(30,"div")(31,"h5"),i(32,"\u901A\u8FC7getter\uFF0C\u8C03\u7528translationSync()\u65F6\u65F6\u83B7\u53D6\u6700\u65B0\u7684\u7FFB\u8BD1"),e(),i(33),e(),s(34,"hr"),r(35,"div")(36,"h5"),i(37," \u8C03\u7528translationAsync()\u5F97\u5230\u4E00\u4E2AObservable,\u7ED3\u5408 async \u7BA1\u9053\u4F7F\u7528\uFF0C\u8BED\u8A00\u5207\u6362\u65F6\u81EA\u52A8\u83B7\u53D6\u6700\u65B0\u7684\u7FFB\u8BD1 "),e(),r(38,"div"),i(39),e(),i(40),u(41,"async"),e(),s(42,"hr"),r(43,"h3"),i(44),e(),r(45,"div"),s(46,"nb-trans",4),e(),s(47,"hr"),r(48,"h5"),i(49),u(50,"json"),e(),r(51,"p"),i(52),u(53,"nbTrans"),e(),r(54,"div")(55,"pre")(56,"code"),i(57),e()()(),r(58,"div"),s(59,"nb-trans",5),e(),s(60,"hr"),r(61,"h3"),i(62),e(),s(63,"div",6)(64,"hr"),r(65,"h5"),i(66),u(67,"json"),e(),r(68,"p"),i(69),u(70,"nbTrans"),e(),r(71,"div")(72,"pre")(73,"code"),i(74),e()()(),r(75,"div"),s(76,"div",7),e(),s(77,"hr"),r(78,"h5"),i(79),u(80,"nbTrans"),e(),r(81,"p"),i(82),e(),r(83,"p"),i(84),u(85,"json"),e(),O(86,ot,1,2,"ng-template",null,0,A)(88,rt,1,2,"ng-template",null,1,A)(90,at,2,1,"ng-template",null,2,A)}if(p&2){let f=B(87),I=B(89),L=B(91);o(3),F(" ","{{'title'| nbTrans}}\uFF1A"," ",c(4,34,"title"),` 34 | `),o(6),F(" ","{{'content.helloWorld'| nbTrans}}:"," ",c(10,36,"content.helloWorld"),` 35 | `),o(5),C(" \u4F7F\u7528 nbTrans \u7BA1\u9053\uFF0C\u5E26\u6709options\u53C2\u6570\u3002\u8BBE\u7F6Ekey\u503C\u524D\u7F00\u548C\u7FFB\u8BD1\u6587\u672C\u4E2D\u7684\u53C2\u6570,params\u53C2\u6570\u4E3A\uFF1A ",c(15,38,a.params)," "),o(3),C("\u7FFB\u8BD1\u6587\u672C\u539F\u6587\uFF1A",h(18,40,"contentWithParams",S(67,k))),o(2),F(" ","{{'contentWithParams'| nbTrans:({prefix:'content',params: params})}}:"," ",h(20,43,"contentWithParams",y(68,it,a.params)),` 36 | `),o(3),C("\u52A8\u6001\u8C03\u6574options,options is ",c(23,46,a.options)),o(5),d(h(28,48,"contentWithParams",a.options)),o(6),F(" ","get title(){return this.transService.translationSync('title');}:"," ",a.title,` 37 | `),o(6),d("this.title$ = this.transService.translationAsync('title');// ts"),o(),F(" ","{{title$ | async}}:"," ",c(41,51,a.title$),` 38 | `),o(4),C("use ",""),o(2),l("options",a.options),o(3),F(" \u4F7F\u7528","","\u7EC4\u4EF6\uFF0C\u5E26\u6709components\u53C2\u6570\u548Coptions\u53C2\u6570\u3002 \u8BBE\u7F6Ekey\u503C\u524D\u7F00\u548C\u7FFB\u8BD1\u6587\u672C\u4E2D\u7684\u53C2\u6570,params\u53C2\u6570\u4E3A\uFF1A",c(50,53,a.params),` 39 | `),o(3),C("\u7FFB\u8BD1\u6587\u672C\u539F\u6587\uFF1A",h(53,55,"complexContent",S(70,k))),o(5),d(a.compStr1),o(2),l("components",_(71,X,f,I,L))("options",y(75,Y,a.params)),o(3),C("use ","
"),o(),l("nb-trans-options",a.options),o(3),F(" \u4F7F\u7528","
","\u7EC4\u4EF6\uFF0C\u5E26\u6709components\u53C2\u6570\u548Coptions\u53C2\u6570\u3002 \u8BBE\u7F6Ekey\u503C\u524D\u7F00\u548C\u7FFB\u8BD1\u6587\u672C\u4E2D\u7684\u53C2\u6570,params\u53C2\u6570\u4E3A\uFF1A",c(67,58,a.params),` 40 | `),o(3),C("\u7FFB\u8BD1\u6587\u672C\u539F\u6587\uFF1A",h(70,60,"complexContent",S(77,k))),o(5),d(a.compStr2),o(2),l("nb-trans-components",_(78,X,f,I,L))("nb-trans-options",y(82,Y,a.params)),o(3),d(c(80,63,"currBrowserLang")),o(3),d(a.browserLang),o(2),d(c(85,65,a.browserLangs))}},dependencies:[z,H,D,U,V,G,q],styles:["a[_ngcontent-%COMP%]{color:#00f;cursor:pointer}a[_ngcontent-%COMP%]:hover{text-decoration:underline}"],changeDetection:0});let n=t;return n})();var st=[{path:"",component:Z}],tt=(()=>{let t=class t{};t.\u0275fac=function(p){return new(p||t)},t.\u0275mod=x({type:t}),t.\u0275inj=v({imports:[w.forChild(st),w]});let n=t;return n})();var Bt=(()=>{let t=class t{};t.\u0275fac=function(p){return new(p||t)},t.\u0275mod=x({type:t}),t.\u0275inj=v({imports:[J,K,tt]});let n=t;return n})();export{Bt as Feature1Module}; 41 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/services/nb-trans.service.ts: -------------------------------------------------------------------------------- 1 | import { get, isFunction } from 'lodash-es'; 2 | import { BehaviorSubject, from, Observable, of, Subject, timer } from 'rxjs'; 3 | import { catchError, map, retry, skipWhile, switchMap, tap } from 'rxjs/operators'; 4 | import { Injectable, inject } from '@angular/core'; 5 | import { 6 | NB_TRANS_DEFAULT_LANG, 7 | NB_TRANS_LOADER, 8 | NB_TRANS_MAX_RETRY, 9 | NbTransLang, 10 | } from '../constants'; 11 | import { INbTransChangeLang, INbTransLoader, INbTransOptions, INbTranslation } from '../models'; 12 | import { NbTransToolsService } from './nb-trans-tools.service'; 13 | 14 | @Injectable({ providedIn: 'root' }) 15 | export class NbTransService { 16 | private transDefaultLang: string = 17 | inject(NB_TRANS_DEFAULT_LANG, { optional: true }) || NbTransLang.ZH_CN; 18 | private transLoader: INbTransLoader | null = inject(NB_TRANS_LOADER, { optional: true }); 19 | private maxRetry: number | null = inject(NB_TRANS_MAX_RETRY, { optional: true }); 20 | private transToolsService: NbTransToolsService = inject(NbTransToolsService); 21 | 22 | private lang$ = new BehaviorSubject(NbTransLang.ZH_CN); 23 | 24 | private loadDefaultOver$ = new BehaviorSubject(false); 25 | 26 | private loadLangTrans$ = new Subject(); 27 | 28 | private retry: number = 5; 29 | 30 | private translations: { [key: string]: INbTranslation } = {}; 31 | 32 | /** 33 | * Current language value 34 | */ 35 | get lang(): string { 36 | return this.lang$.value; 37 | } 38 | 39 | /** 40 | * Whether the translated file of the default language is loaded 41 | */ 42 | get loadDefaultOver(): boolean { 43 | return this.loadDefaultOver$.value; 44 | } 45 | 46 | /** 47 | * Get the first language of browser 48 | */ 49 | static getBrowserLang(): string | undefined { 50 | if (!NbTransToolsService.checkNavigator()) { 51 | return undefined; 52 | } 53 | return window?.navigator?.language; 54 | } 55 | 56 | /** 57 | * Get a language array known to the user, by order of preference 58 | */ 59 | static getBrowserLangs(): readonly string[] | undefined { 60 | if (!NbTransToolsService.checkNavigator()) { 61 | return undefined; 62 | } 63 | return window?.navigator?.languages; 64 | } 65 | 66 | constructor() { 67 | // if the maxRetry is undefined/null, use default settings, 68 | // so can set the retry valus as 0 to cancel retry action. 69 | this.retry = this.maxRetry == null ? this.retry : this.maxRetry; 70 | 71 | this.transLoader = this.transLoader || {}; 72 | 73 | this.lang$.next(this.transDefaultLang); 74 | this.loadDefaultTrans(); 75 | } 76 | 77 | /** 78 | * Switch language async 79 | * @param lang language key 80 | */ 81 | changeLang(lang: string): Observable { 82 | const successResult: INbTransChangeLang = { 83 | curLang: lang, 84 | result: true, 85 | }; 86 | const failureResult: INbTransChangeLang = { 87 | curLang: this.lang, 88 | result: false, 89 | }; 90 | 91 | // the lang has been loaded, 92 | if (this.translations[lang]) { 93 | this.lang$.next(lang); 94 | return of(successResult); 95 | } 96 | 97 | // there is no any lang loader 98 | if (!this.transLoader || !this.transLoader[lang]) { 99 | timer(1).subscribe(() => this.loadLangTrans$.next(false)); 100 | return of(failureResult); 101 | } 102 | 103 | return this.loadLangTrans(lang).pipe( 104 | switchMap(loadResult => { 105 | let curLang = this.lang; 106 | let result = failureResult; 107 | if (loadResult) { 108 | curLang = lang; 109 | result = successResult; 110 | } 111 | this.lang$.next(curLang); 112 | return of(result); 113 | }) 114 | ); 115 | } 116 | 117 | /** 118 | * Switch language sync 119 | * @param lang language key 120 | */ 121 | changeLangSync(lang: string): void { 122 | this.changeLang(lang).subscribe(); 123 | } 124 | 125 | /** 126 | * get the first language of browser 127 | * @deprecated 128 | */ 129 | getBrowserLang(): string | undefined { 130 | console.warn( 131 | 'The function will be deprecated in the future, we recommend using NbTransService.getBrowserLang()!' 132 | ); 133 | return NbTransService.getBrowserLang(); 134 | } 135 | 136 | /** 137 | * get a language array known to the user, by order of preference 138 | * @deprecated 139 | */ 140 | getBrowserLangs(): readonly string[] | undefined { 141 | console.warn( 142 | 'The function will be deprecated in the future, we recommend using NbTransService.getBrowserLangs()!' 143 | ); 144 | return NbTransService.getBrowserLangs(); 145 | } 146 | 147 | /** 148 | * Get translated text asynchronously based on key and options 149 | * @param key trans key 150 | * @param options trans options 151 | */ 152 | translationAsync(key: string, options?: INbTransOptions): Observable { 153 | return this.lang$.pipe( 154 | switchMap(() => { 155 | return this.translations[this.lang] 156 | ? of({ trans: this.translations[this.lang], result: true }) 157 | : this.loadLangTrans$; 158 | }), 159 | map(() => this.translationSync(key, options)) 160 | ); 161 | } 162 | 163 | /** 164 | * Synchronously get translated text according to key and options 165 | * @param key trans key 166 | * @param options trans options 167 | */ 168 | translationSync(key: string, options?: INbTransOptions): string { 169 | const finalKey = this.transToolsService.getFinalKey(key, options?.prefix); 170 | const emptyTrans = options?.returnKeyWhenEmpty === false ? '' : finalKey; 171 | let trans = get(this.translations[this.lang], finalKey); 172 | 173 | // if the trans is boolean/number or other types, it is invalid. 174 | // Although boolean and number can be implicitly converted to string types, 175 | // it would be more expected and better when let the developer provide the value of the string type directly. 176 | // if the trans only include some whitespace, like ' ', it is valid 177 | if (!this.transToolsService.isTranslatedStringValid(trans)) { 178 | trans = get(this.translations[this.transDefaultLang], finalKey); 179 | } 180 | 181 | if (!this.transToolsService.isTranslatedStringValid(trans)) { 182 | return emptyTrans; 183 | } 184 | 185 | const params = options?.params; 186 | return this.transToolsService.handleSentenceWithParams(trans as string, params); 187 | } 188 | 189 | /** 190 | * An subscribe event of switching language 191 | */ 192 | subscribeLangChange(): Observable { 193 | return this.lang$.asObservable(); 194 | } 195 | 196 | /** 197 | * Whethe the translated file of default lang has been load over 198 | */ 199 | subscribeLoadDefaultOver(): Observable { 200 | return this.loadDefaultOver 201 | ? of(true) 202 | : this.loadDefaultOver$.asObservable().pipe( 203 | // the loadDefaultOver$ is BehaviorSubject, 204 | // so the user will get a value immediately when subscribe it, 205 | // but it doesn't make sense, so here will skip it 206 | skipWhile((result, index) => !result && index === 0) 207 | ); 208 | } 209 | 210 | private loadDefaultTrans(): void { 211 | this.loadTrans(this.lang) 212 | .pipe(map(trans => !!trans)) 213 | .subscribe(result => { 214 | this.loadDefaultOver$.next(result); 215 | this.loadDefaultOver$.complete(); 216 | this.loadLangTrans$.next(result); 217 | }); 218 | } 219 | 220 | private loadLangTrans(lang: string): Observable { 221 | return this.loadTrans(lang).pipe( 222 | map(trans => !!trans), 223 | tap(result => this.loadLangTrans$.next(result)) 224 | ); 225 | } 226 | 227 | private loadTrans(lang: string): Observable { 228 | const loader = this.transLoader?.[lang]; 229 | if (!loader) { 230 | return of(null); 231 | } 232 | 233 | const loaderFn: Observable = isFunction(loader) 234 | ? // switch map as load lang observable, 235 | // so it will retry when failure to load the lang content 236 | of(null).pipe(switchMap(() => from(loader()) as Observable)) 237 | : of(loader); 238 | return loaderFn.pipe( 239 | tap(trans => (this.translations[lang] = trans)), 240 | retry(this.retry), 241 | catchError(() => of(null)) 242 | ); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/components/nb-trans/test/nb-trans2.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Component, SimpleChange, TemplateRef, ElementRef, inject } from '@angular/core'; 3 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { take } from 'rxjs/operators'; 5 | import { NB_TRANS_DEFAULT_LANG, NB_TRANS_LOADER } from '../../../constants'; 6 | import { NbTransLang } from '../../../constants'; 7 | import { INbTransOptions } from '../../../models'; 8 | import { NbTransService, NbTransToolsService } from '../../../services'; 9 | import { transLoader, NbTransTestingModule } from '../../../testing'; 10 | import { NbTrans2Component } from '../nb-trans.component'; 11 | import { MockComp1Component, MockTplRefComponent } from './nb-trans.component.spec'; 12 | 13 | describe('Component: NbTrans2', () => { 14 | describe('used in normal component', () => { 15 | let component: NbTrans2Component; 16 | let fixture: ComponentFixture; 17 | let transToolsService: NbTransToolsService; 18 | 19 | beforeEach(async () => { 20 | await TestBed.configureTestingModule({ 21 | imports: [NbTransTestingModule], 22 | declarations: [MockTplRefComponent, MockComp1Component], 23 | providers: [{ provide: NB_TRANS_LOADER, useValue: transLoader.staticLoader }], 24 | }).compileComponents(); 25 | }); 26 | 27 | beforeEach(() => { 28 | transToolsService = TestBed.inject(NbTransToolsService); 29 | 30 | fixture = TestBed.createComponent(NbTrans2Component); 31 | component = fixture.componentInstance; 32 | fixture.detectChanges(); 33 | }); 34 | 35 | it('should be created', () => { 36 | expect(component).toBeTruthy(); 37 | }); 38 | 39 | describe('#ngOnChanges()', () => { 40 | it('key param and options param all have values', () => { 41 | const spyFn = spyOn(transToolsService, 'handleTrans').and.callThrough(); 42 | 43 | component.key = 'title'; 44 | component.options = {}; 45 | const changes = { 46 | key: new SimpleChange(undefined, component.key, true), 47 | options: new SimpleChange(undefined, component.options, true), 48 | }; 49 | component.ngOnChanges(changes); 50 | 51 | expect(spyFn).toHaveBeenCalledTimes(1); 52 | }); 53 | 54 | it('only key param has value', () => { 55 | const spyFn = spyOn(transToolsService, 'handleTrans').and.callThrough(); 56 | 57 | component.key = 'title'; 58 | component.options = undefined as any; 59 | const changes = { 60 | key: new SimpleChange(undefined, component.key, true), 61 | }; 62 | component.ngOnChanges(changes); 63 | 64 | expect(spyFn).toHaveBeenCalledTimes(1); 65 | }); 66 | 67 | it('only option param has value', () => { 68 | const spyFn = spyOn(transToolsService, 'handleTrans').and.callThrough(); 69 | 70 | component.key = 'title'; 71 | component.options = {}; 72 | const changes = { 73 | options: new SimpleChange(undefined, component.options, true), 74 | }; 75 | component.ngOnChanges(changes); 76 | 77 | expect(spyFn).toHaveBeenCalledTimes(1); 78 | }); 79 | }); 80 | 81 | it('verify has subscribed lang change event', done => { 82 | const transService = TestBed.inject(NbTransService); 83 | spyOn(transService, 'subscribeLangChange').and.callThrough(); 84 | spyOn(transToolsService, 'handleTrans').and.callThrough(); 85 | 86 | component.key = 'title'; 87 | component.options = {}; 88 | 89 | transService 90 | .changeLang(NbTransLang.EN) 91 | .pipe(take(1)) 92 | .subscribe(() => { 93 | expect(transToolsService.handleTrans).toHaveBeenCalledTimes(1); 94 | done(); 95 | }); 96 | }); 97 | 98 | describe('verify the UI', () => { 99 | let tpls: TemplateRef[] = []; 100 | let uiComp: NbTrans2Component; 101 | let uiFixture: ComponentFixture; 102 | let hostEle: HTMLElement; 103 | 104 | beforeEach(() => { 105 | const tplFixture = TestBed.createComponent(MockTplRefComponent); 106 | const tplComp = tplFixture.componentInstance; 107 | tplFixture.detectChanges(); 108 | tpls = [tplComp.tpl1, tplComp.tpl2]; 109 | 110 | uiFixture = TestBed.createComponent(NbTrans2Component); 111 | uiComp = uiFixture.componentInstance; 112 | uiFixture.detectChanges(); 113 | hostEle = uiFixture.debugElement.nativeElement; 114 | }); 115 | 116 | beforeEach(async () => { 117 | const transService = TestBed.inject(NbTransService); 118 | return transService.subscribeLoadDefaultOver().toPromise(); 119 | }); 120 | 121 | it(`the content is string`, () => { 122 | uiComp.key = 'title'; 123 | uiComp.components = []; 124 | const changes = { 125 | key: new SimpleChange(undefined, uiComp.key, true), 126 | }; 127 | uiComp.ngOnChanges(changes); 128 | uiFixture.detectChanges(); 129 | 130 | expect(hostEle.textContent?.trim()).toEqual('标题'); 131 | }); 132 | 133 | it(`the content is component`, () => { 134 | uiComp.key = 'component'; 135 | uiComp.components = [tpls[1]]; 136 | const changes = { 137 | key: new SimpleChange(undefined, uiComp.key, true), 138 | }; 139 | uiComp.ngOnChanges(changes); 140 | uiFixture.detectChanges(); 141 | 142 | const comp1Instance = hostEle.querySelector('comp1'); 143 | expect(!!comp1Instance).toEqual(true); 144 | expect(comp1Instance?.textContent?.trim()).toEqual('组件'); 145 | }); 146 | 147 | it(`the content is complex component`, () => { 148 | uiComp.key = 'complexComponent'; 149 | uiComp.components = tpls; 150 | const changes = { 151 | key: new SimpleChange(undefined, uiComp.key, true), 152 | }; 153 | uiComp.ngOnChanges(changes); 154 | uiFixture.detectChanges(); 155 | 156 | const hasSubContentEle = hostEle.querySelector('div.has-subcontent'); 157 | expect(!!hasSubContentEle).toEqual(true); 158 | expect(hasSubContentEle?.textContent?.trim()).toEqual('组件0组件1'); 159 | 160 | const comp1Instance = hasSubContentEle?.querySelector('comp1'); 161 | expect(!!comp1Instance).toEqual(true); 162 | expect(comp1Instance?.textContent?.trim()).toEqual('组件1'); 163 | }); 164 | 165 | it('verify the sentence with dynamic params', () => { 166 | const params = { 167 | params1: '{{params2}}', 168 | params2: '1111', 169 | params3: '2222', 170 | }; 171 | uiComp.options = { params }; 172 | uiComp.key = 'withParams'; 173 | uiComp.components = tpls; 174 | const changes = { 175 | key: new SimpleChange(undefined, uiComp.key, true), 176 | options: new SimpleChange(undefined, uiComp.options, true), 177 | }; 178 | uiComp.ngOnChanges(changes); 179 | uiFixture.detectChanges(); 180 | 181 | expect(hostEle.textContent?.trim()).toEqual( 182 | '这是一个带有参数的句子。参数: {{params2}} - 1111 - 2222 - 1111' 183 | ); 184 | }); 185 | }); 186 | }); 187 | 188 | describe('used in standalone component', () => { 189 | beforeEach(async () => { 190 | await TestBed.configureTestingModule({ 191 | providers: [ 192 | { provide: NB_TRANS_DEFAULT_LANG, useValue: NbTransLang.ZH_CN }, 193 | { provide: NB_TRANS_LOADER, useValue: transLoader.staticLoader }, 194 | ], 195 | }).compileComponents(); 196 | const transService = TestBed.inject(NbTransService); 197 | await transService.subscribeLoadDefaultOver().toPromise(); 198 | }); 199 | 200 | [ 201 | { 202 | title: 'imported by standalone component', 203 | createComp: () => TestBed.createComponent(StandaloneComponent), 204 | }, 205 | { 206 | title: 'imported by ngModule', 207 | createComp: () => TestBed.createComponent(StandaloneComponentWithNgModule), 208 | }, 209 | ].forEach(item => { 210 | it(item.title, () => { 211 | const fixture = item.createComp(); 212 | const component = fixture.componentInstance; 213 | fixture.detectChanges(); 214 | 215 | expect(component.textContent).toEqual('你好,世界'); 216 | }); 217 | }); 218 | }); 219 | }); 220 | 221 | const StandaloneCompConfig = { 222 | standalone: true, 223 | imports: [NbTrans2Component], 224 | template: `
`, 225 | }; 226 | 227 | @Component(StandaloneCompConfig) 228 | class StandaloneComponent { 229 | private elementRef: ElementRef = inject(ElementRef); 230 | key = 'helloWorld'; 231 | options: INbTransOptions = { prefix: 'content' }; 232 | 233 | get textContent() { 234 | return this.elementRef.nativeElement.textContent?.trim(); 235 | } 236 | } 237 | 238 | @Component({ 239 | ...StandaloneCompConfig, 240 | imports: [NbTransTestingModule], 241 | }) 242 | // eslint-disable-next-line @angular-eslint/component-class-suffix 243 | class StandaloneComponentWithNgModule extends StandaloneComponent {} 244 | -------------------------------------------------------------------------------- /CHANGELOG.CN.md: -------------------------------------------------------------------------------- 1 | # v20.0.0 2 | ## 破坏性更新 3 | - feat: `angular`升级到`v20`; 4 | - feat: `@bigbear713/nb-common`升级到`^20.0.0`; 5 | 6 | --- 7 | 8 | # v19.0.0 9 | ## 破坏性更新 10 | - feat: `angular`升级到`v19`; 11 | - feat: `@bigbear713/nb-common`升级到`^19.0.0`; 12 | 13 | --- 14 | 15 | 16 | # v18.0.0 17 | ## 破坏性更新 18 | - feat: `angular`升级到`v18`; 19 | - feat: `@bigbear713/nb-common`升级到`^18.0.0`; 20 | 21 | --- 22 | 23 | # v17.0.0 24 | ## 破坏性更新 25 | - feat: `angular`升级到`v17`; 26 | - feat: `@bigbear713/nb-common`升级到`^17.0.0`; 27 | 28 | --- 29 | 30 | # v16.0.0 31 | ## 破坏性更新 32 | - feat: `angular`升级到`^16.0.0`; 33 | - feat: `@bigbear713/nb-common`升级到`^16.0.0`; 34 | - feat: [INbTransParams](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#inbtransparams) - 限制 params 中的 `key` 的命名规则:由**字母**、**数字**、**_** 和 **$** 组成,且 **数字** 不能为第一个字符; 35 | - feat: [``](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nb-transnb-trans) - `key`属性添加必填校验:[issue/25](https://github.com/bigBear713/nb-trans/issues/25); 36 | 37 | ## 依赖 38 | - chore: 移除 `uuid` 库; 39 | 40 | ## [Services](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Services "Services") 41 | - refactor: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtransservice "NbTransService") - 重构翻译文本中动态参数的处理方式; 42 | - fix: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtransservice "NbTransService") - 修复当 params 的 key 值不完整时会得到object类型的数据的问题:[issue/27](https://github.com/bigBear713/nb-trans/issues/27); 43 | - feat: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtransservice "NbTransService") - 支持在翻译文本中,`动态参数`和`{{}}`之间存在空格:[issue/34](https://github.com/bigBear713/nb-trans/issues/34); 44 | 45 | ## [Components](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Components "Components") 46 | - feat: [`[nb-trans]`](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nb-trans) - 新增选择器为`[nb-trans]`的组件:[issue/22](https://github.com/bigBear713/nb-trans/issues/22); 47 | - perf: [``](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nb-transnb-trans) - 使用 UnsubscribeService 管理rxjs的订阅事件; 48 | - fix: [``](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nb-transnb-trans) - 修复在一些情况下,翻译结果错误的问题:[issue/28](https://github.com/bigBear713/nb-trans/issues/28); 49 | 50 | ## [Pipes](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Pipes "Pipes") 51 | - feat: [nbTrans](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtrans-transformkey-string-options-inbtransoptions-string) - 使用 UnsubscribeService 管理rxjs的订阅事件; 52 | 53 | ## [Tokens](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Tokens "Tokens") 54 | - feat: [NB_TRANS_PARAM_KEY_INVALID_WARNING](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nb_trans_param_key_invalid_warning) - 当 param key 不符合规则时,是否在 console 中打印警告信息; 55 | 56 | --- 57 | 58 | # v15.1.0 59 | ## [Components](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Components "Components") 60 | - feat: [``](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nb-transnb-trans) - 支持以`standalone component`的方式引入; 61 | - feat: [`[nb-trans-subcontent]`](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nb-trans-subcontent) - 支持以`standalone component`的方式引入; 62 | 63 | ## [Pipes](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Pipes "Pipes") 64 | - feat: [nbTrans](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtrans-transformkey-string-options-inbtransoptions-string) - 支持以`standalone component`的方式引入; 65 | 66 | --- 67 | 68 | # v15.0.0 69 | ## 破坏性更新 70 | - feat: `angular`升级到`^15.0.0`; 71 | - feat: `@bigbear713/nb-common`升级到`^15.0.0`; 72 | 73 | ## 依赖 74 | - feat: `uuid`升级到`^9.0.0`; 75 | 76 | ## [Tokens](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Tokens "Tokens") 77 | - feat: [NB_TRANS_MAX_RETRY](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nb_trans_max_retry) - 增加`NB_TRANS_MAX_RETRY`,`NB_TRANS_MAX_RETRY_TOKEN`标记为`deprecated`; 78 | 79 | ## [Enums](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Enums "Enums") 80 | - feat: [NbTransLang](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtranslang) - 增加`NbTransLang`,`NbTransLangEnum`标记为`deprecated`; 81 | - feat: [NbTransSentenceItem](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtranssentenceitem) - 增加`NbTransSentenceItem`,`NbTransSentenceItemEnum`标记为`deprecated`; 82 | 83 | ## [Services](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Services "Services") 84 | - refactor: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtransservice "NbTransService") - 优化代码; 85 | 86 | ## [Pipes](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Pipes "Pipes") 87 | - refactor: [nbTrans](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtrans-transformkey-string-options-inbtransoptions-string) - 优化代码; 88 | 89 | --- 90 | 91 | # v14.0.0 92 | ## 破坏性更新 93 | - feat: `angular`升级到`^14.0.0`; 94 | - feat: `@bigbear713/nb-common`升级到`^14.0.0`; 95 | 96 | --- 97 | 98 | # v13.0.1 99 | ## 破坏性更新 100 | - fix: `nb-common`版本调整为`^13.0.0`; 101 | 102 | --- 103 | 104 | # v13.0.0 105 | ## 破坏性更新 106 | - feat: `angular`升级到`^13.0.0`; 107 | 108 | --- 109 | 110 | # v12.1.0 111 | ## [Services](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Services "Services") 112 | - feat: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtransservice "NbTransService") - `NbTransService.getBrowserLang()`可以直接获取浏览器的首选语言; 113 | - depr: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtransservice "NbTransService") - `getBrowserLang()`被标志为`deprecated`; 114 | - feat: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtransservice "NbTransService") - `NbTransService.getBrowserLangs()`可以直接获取一个用户已知语言的数组; 115 | - depr: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtransservice "NbTransService") - `getBrowserLangs()`被标志为`deprecated`; 116 | 117 | --- 118 | 119 | # v12.0.0 120 | ## [Module](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Module "Module") 121 | - feat: [NbTransModule](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtransmodule) - 提供可用的`component`, `pipe`; 122 | - feat: [NbTransTestingModule](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtranstestingmodule) - 提供单元测试环境; 123 | 124 | ## [Services](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Services "Services") 125 | - feat: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtransservice "NbTransService") - 提供多语言翻译功能; 126 | 127 | ## [Components](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Components "Components") 128 | - feat: [``](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nb-transnb-trans) - 当翻译文本中含有组件等复杂场景时使用的组件; 129 | - feat: [`[nb-trans-subcontent]`](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nb-trans-subcontent) - 当翻译文本中含有组件嵌套时使用的一种官方提供的方案(可根据需要有自己的实现方式),会将嵌套的组件内容渲染出来; 130 | 131 | ## [Pipes](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Pipes "Pipes") 132 | - feat: [nbTrans](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtrans-transformkey-string-options-inbtransoptions-string) - 翻译文本的管道; 133 | 134 | ## [Tokens](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Tokens "Tokens") 135 | - feat: [NB_TRANS_DEFAULT_LANG](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nb_trans_default_lang) - 设置默认语言; 136 | - feat: [NB_TRANS_LOADER](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nb_trans_loader) - 翻译文本加载器; 137 | - feat: [NB_TRANS_MAX_RETRY](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nb_trans_max_retry) - 翻译文本加载失败时的最大重试次数; 138 | 139 | ## [Interfaces](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Interfaces "Interfaces") 140 | - feat: [INbTransLoader](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#inbtransloader) - 文本加载器; 141 | - feat: [INbTransOptions](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#inbtransoptions) - 翻译配置; 142 | - feat: [INbTransParams](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#inbtransparams) - 翻译文本中的参数; 143 | - feat: [INbTransChangeLang](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#inbtranschangelang) - 切换语言的结果; 144 | - feat: [INbTransSentencePart](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#inbtranssentencepart) - 句子部分; 145 | - feat: [INbTransSentenceCompPart](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#inbtranssentencecomppart) - 句子中含有组件的部分; 146 | 147 | ## [Enums](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#Enums "Enums") 148 | - feat: [NbTransLang](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtranslang) - 常用语言枚举; 149 | - feat: [NbTransSentenceItem](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#nbtranssentenceitem) - 句子项类型枚举; 150 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/components/nb-trans/test/nb-trans.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Component, SimpleChange, TemplateRef, ViewChild, ElementRef, inject } from '@angular/core'; 3 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { take } from 'rxjs/operators'; 5 | import { NB_TRANS_DEFAULT_LANG, NB_TRANS_LOADER } from '../../../constants'; 6 | import { NbTransLang } from '../../../constants'; 7 | import { INbTransOptions } from '../../../models'; 8 | import { NbTransService, NbTransToolsService } from '../../../services'; 9 | import { transLoader, NbTransTestingModule } from '../../../testing'; 10 | import { NbTransComponent } from '../nb-trans.component'; 11 | 12 | @Component({ 13 | selector: 'comp1', 14 | template: ` `, 15 | // eslint-disable-next-line @angular-eslint/prefer-standalone 16 | standalone: false, 17 | }) 18 | export class MockComp1Component {} 19 | 20 | @Component({ 21 | selector: 'mock-tpl-ref', 22 | template: ` 23 | 24 |
25 |
26 | {{ content }} 29 | `, 30 | // eslint-disable-next-line @angular-eslint/prefer-standalone 31 | standalone: false, 32 | }) 33 | export class MockTplRefComponent { 34 | @ViewChild('tpl1') tpl1!: TemplateRef; 35 | @ViewChild('tpl2') tpl2!: TemplateRef; 36 | } 37 | 38 | describe('Component: NbTrans', () => { 39 | describe('used in normal component', () => { 40 | let component: NbTransComponent; 41 | let fixture: ComponentFixture; 42 | let transToolsService: NbTransToolsService; 43 | 44 | beforeEach(async () => { 45 | await TestBed.configureTestingModule({ 46 | imports: [NbTransTestingModule], 47 | declarations: [MockTplRefComponent, MockComp1Component], 48 | providers: [{ provide: NB_TRANS_LOADER, useValue: transLoader.staticLoader }], 49 | }).compileComponents(); 50 | }); 51 | 52 | beforeEach(() => { 53 | transToolsService = TestBed.inject(NbTransToolsService); 54 | 55 | fixture = TestBed.createComponent(NbTransComponent); 56 | component = fixture.componentInstance; 57 | fixture.detectChanges(); 58 | }); 59 | 60 | it('should be created', () => { 61 | expect(component).toBeTruthy(); 62 | }); 63 | 64 | describe('#ngOnChanges()', () => { 65 | it('key param and options param all have values', () => { 66 | const spyFn = spyOn(transToolsService, 'handleTrans').and.callThrough(); 67 | 68 | component.key = 'title'; 69 | component.options = {}; 70 | const changes = { 71 | key: new SimpleChange(undefined, component.key, true), 72 | options: new SimpleChange(undefined, component.options, true), 73 | }; 74 | component.ngOnChanges(changes); 75 | 76 | expect(spyFn).toHaveBeenCalledTimes(1); 77 | }); 78 | 79 | it('only key param has value', () => { 80 | const spyFn = spyOn(transToolsService, 'handleTrans').and.callThrough(); 81 | 82 | component.key = 'title'; 83 | component.options = undefined as any; 84 | const changes = { 85 | key: new SimpleChange(undefined, component.key, true), 86 | options: new SimpleChange(undefined, component.options, true), 87 | }; 88 | component.ngOnChanges(changes); 89 | 90 | expect(spyFn).toHaveBeenCalledTimes(1); 91 | }); 92 | 93 | it('only option param has value', () => { 94 | const spyFn = spyOn(transToolsService, 'handleTrans').and.callThrough(); 95 | 96 | component.key = 'title'; 97 | component.options = {}; 98 | const changes = { 99 | options: new SimpleChange(undefined, component.options, true), 100 | }; 101 | component.ngOnChanges(changes); 102 | 103 | expect(spyFn).toHaveBeenCalledTimes(1); 104 | }); 105 | }); 106 | 107 | it('verify has subscribed lang change event', done => { 108 | const transService = TestBed.inject(NbTransService); 109 | spyOn(transService, 'subscribeLangChange').and.callThrough(); 110 | spyOn(transToolsService, 'handleTrans').and.callThrough(); 111 | 112 | component.key = 'title'; 113 | component.options = {}; 114 | 115 | transService 116 | .changeLang(NbTransLang.EN) 117 | .pipe(take(1)) 118 | .subscribe(() => { 119 | expect(transToolsService.handleTrans).toHaveBeenCalledTimes(1); 120 | done(); 121 | }); 122 | }); 123 | 124 | describe('verify the UI', () => { 125 | let tpls: TemplateRef[] = []; 126 | let uiComp: NbTransComponent; 127 | let uiFixture: ComponentFixture; 128 | let hostEle: HTMLElement; 129 | 130 | beforeEach(() => { 131 | const tplFixture = TestBed.createComponent(MockTplRefComponent); 132 | const tplComp = tplFixture.componentInstance; 133 | tplFixture.detectChanges(); 134 | tpls = [tplComp.tpl1, tplComp.tpl2]; 135 | 136 | uiFixture = TestBed.createComponent(NbTransComponent); 137 | uiComp = uiFixture.componentInstance; 138 | uiFixture.detectChanges(); 139 | hostEle = uiFixture.debugElement.nativeElement; 140 | }); 141 | 142 | beforeEach(async () => { 143 | const transService = TestBed.inject(NbTransService); 144 | return transService.subscribeLoadDefaultOver().toPromise(); 145 | }); 146 | 147 | it(`the content is string`, () => { 148 | uiComp.key = 'title'; 149 | uiComp.components = []; 150 | const changes = { 151 | key: new SimpleChange(undefined, uiComp.key, true), 152 | }; 153 | uiComp.ngOnChanges(changes); 154 | uiFixture.detectChanges(); 155 | 156 | expect(hostEle.textContent?.trim()).toEqual('标题'); 157 | }); 158 | 159 | it(`the content is component`, () => { 160 | uiComp.key = 'component'; 161 | uiComp.components = [tpls[1]]; 162 | const changes = { 163 | key: new SimpleChange(undefined, uiComp.key, true), 164 | }; 165 | uiComp.ngOnChanges(changes); 166 | uiFixture.detectChanges(); 167 | 168 | const comp1Instance = hostEle.querySelector('comp1'); 169 | expect(!!comp1Instance).toEqual(true); 170 | expect(comp1Instance?.textContent?.trim()).toEqual('组件'); 171 | }); 172 | 173 | it(`the content is complex component`, () => { 174 | uiComp.key = 'complexComponent'; 175 | uiComp.components = tpls; 176 | const changes = { 177 | key: new SimpleChange(undefined, uiComp.key, true), 178 | }; 179 | uiComp.ngOnChanges(changes); 180 | uiFixture.detectChanges(); 181 | 182 | const hasSubContentEle = hostEle.querySelector('div.has-subcontent'); 183 | expect(!!hasSubContentEle).toEqual(true); 184 | expect(hasSubContentEle?.textContent?.trim()).toEqual('组件0组件1'); 185 | 186 | const comp1Instance = hasSubContentEle?.querySelector('comp1'); 187 | expect(!!comp1Instance).toEqual(true); 188 | expect(comp1Instance?.textContent?.trim()).toEqual('组件1'); 189 | }); 190 | 191 | it('verify the sentence with dynamic params', () => { 192 | const params = { 193 | params1: '{{params2}}', 194 | params2: '1111', 195 | params3: '2222', 196 | }; 197 | uiComp.options = { params }; 198 | uiComp.key = 'withParams'; 199 | uiComp.components = tpls; 200 | const changes = { 201 | key: new SimpleChange(undefined, uiComp.key, true), 202 | options: new SimpleChange(undefined, uiComp.options, true), 203 | }; 204 | uiComp.ngOnChanges(changes); 205 | uiFixture.detectChanges(); 206 | 207 | expect(hostEle.textContent?.trim()).toEqual( 208 | '这是一个带有参数的句子。参数: {{params2}} - 1111 - 2222 - 1111' 209 | ); 210 | }); 211 | }); 212 | }); 213 | 214 | describe('used in standalone component', () => { 215 | beforeEach(async () => { 216 | await TestBed.configureTestingModule({ 217 | providers: [ 218 | { provide: NB_TRANS_DEFAULT_LANG, useValue: NbTransLang.ZH_CN }, 219 | { provide: NB_TRANS_LOADER, useValue: transLoader.staticLoader }, 220 | ], 221 | }).compileComponents(); 222 | const transService = TestBed.inject(NbTransService); 223 | await transService.subscribeLoadDefaultOver().toPromise(); 224 | }); 225 | 226 | [ 227 | { 228 | title: 'imported by standalone component', 229 | createComp: () => TestBed.createComponent(StandaloneComponent), 230 | }, 231 | { 232 | title: 'imported by ngModule', 233 | createComp: () => TestBed.createComponent(StandaloneComponentWithNgModule), 234 | }, 235 | ].forEach(item => { 236 | it(item.title, () => { 237 | const fixture = item.createComp(); 238 | const component = fixture.componentInstance; 239 | fixture.detectChanges(); 240 | 241 | expect(component.textContent).toEqual('你好,世界'); 242 | }); 243 | }); 244 | }); 245 | }); 246 | 247 | const StandaloneCompConfig = { 248 | standalone: true, 249 | imports: [NbTransComponent], 250 | template: ``, 251 | }; 252 | 253 | @Component(StandaloneCompConfig) 254 | class StandaloneComponent { 255 | private elementRef: ElementRef = inject(ElementRef); 256 | key = 'helloWorld'; 257 | options: INbTransOptions = { prefix: 'content' }; 258 | 259 | get textContent() { 260 | return this.elementRef.nativeElement.textContent?.trim(); 261 | } 262 | } 263 | 264 | @Component({ 265 | ...StandaloneCompConfig, 266 | imports: [NbTransTestingModule], 267 | }) 268 | // eslint-disable-next-line @angular-eslint/component-class-suffix 269 | class StandaloneComponentWithNgModule extends StandaloneComponent {} 270 | -------------------------------------------------------------------------------- /projects/nb-trans/src/lib/services/test/nb-trans.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { inject, TestBed } from '@angular/core/testing'; 3 | import { filter, switchMap, take } from 'rxjs/operators'; 4 | import { 5 | NB_TRANS_DEFAULT_LANG, 6 | NB_TRANS_LOADER, 7 | NB_TRANS_MAX_RETRY, 8 | NbTransLang, 9 | } from '../../constants'; 10 | import { translationSyncTestData, transLoader, NbTransTestingModule } from '../../testing'; 11 | import { NbTransService } from '../nb-trans.service'; 12 | import { NbTransToolsService } from '../nb-trans-tools.service'; 13 | 14 | describe('Service: NbTrans', () => { 15 | beforeEach(async () => { 16 | await TestBed.configureTestingModule({ 17 | imports: [NbTransTestingModule], 18 | }); 19 | }); 20 | 21 | it('should be created', inject([NbTransService], (service: NbTransService) => { 22 | expect(service).toBeTruthy(); 23 | })); 24 | 25 | describe('#changeLang()', () => { 26 | [ 27 | { title: 'dynamic load language', loader: transLoader.dynamicLoader }, 28 | { title: 'static load language', loader: transLoader.staticLoader }, 29 | ].forEach(loaderMethodItem => { 30 | describe(loaderMethodItem.title, () => { 31 | let service: NbTransService; 32 | 33 | beforeEach(async () => { 34 | await TestBed.configureTestingModule({ 35 | imports: [NbTransTestingModule], 36 | providers: [ 37 | { provide: NB_TRANS_DEFAULT_LANG, useValue: NbTransLang.ZH_CN }, 38 | { provide: NB_TRANS_LOADER, useValue: loaderMethodItem.loader }, 39 | ], 40 | }); 41 | service = TestBed.inject(NbTransService); 42 | }); 43 | 44 | it('#subscribeLoadDefaultOver()', done => { 45 | service 46 | .subscribeLoadDefaultOver() 47 | .pipe(take(1)) 48 | .subscribe(result => { 49 | expect(result).toEqual(true); 50 | done(); 51 | }); 52 | }); 53 | 54 | [ 55 | { 56 | lang: NbTransLang.ZH_CN, 57 | expect: { 58 | changeResult: { curLang: NbTransLang.ZH_CN, result: true }, 59 | transResult: '标题 ', 60 | }, 61 | }, 62 | { 63 | lang: NbTransLang.EN, 64 | expect: { 65 | changeResult: { curLang: NbTransLang.EN, result: true }, 66 | transResult: 'title ', 67 | }, 68 | }, 69 | { 70 | lang: NbTransLang.AR_EG, 71 | expect: { 72 | changeResult: { curLang: NbTransLang.ZH_CN, result: false }, 73 | transResult: '标题 ', 74 | }, 75 | }, 76 | ].forEach(item => { 77 | it(`change lang as ${item.lang}`, done => { 78 | service 79 | .subscribeLoadDefaultOver() 80 | .pipe( 81 | filter(result => result), 82 | switchMap(() => service.changeLang(item.lang)) 83 | ) 84 | .pipe() 85 | .subscribe(result => { 86 | expect(result).toEqual(item.expect.changeResult); 87 | expect(service.lang).toEqual(item.expect.changeResult.curLang); 88 | expect(service.translationSync('title')).toEqual(item.expect.transResult); 89 | done(); 90 | }); 91 | }); 92 | }); 93 | 94 | describe('#subscribeLangChange()', () => { 95 | [ 96 | { lang: NbTransLang.ZH_CN, expect: NbTransLang.ZH_CN }, 97 | { lang: NbTransLang.EN, expect: NbTransLang.EN }, 98 | { lang: NbTransLang.AR_EG, expect: NbTransLang.ZH_CN }, 99 | ].forEach(item => { 100 | it(`change lang as ${item.lang}`, done => { 101 | service 102 | .subscribeLoadDefaultOver() 103 | .pipe( 104 | filter(result => result), 105 | switchMap(() => service.changeLang(item.lang)) 106 | ) 107 | .subscribe(() => { 108 | service 109 | .subscribeLangChange() 110 | .pipe() 111 | .subscribe(lang => { 112 | expect(lang).toEqual(item.expect); 113 | done(); 114 | }); 115 | }); 116 | }); 117 | }); 118 | }); 119 | }); 120 | }); 121 | }); 122 | 123 | it('#changeLangSync()', inject([NbTransService], (service: NbTransService) => { 124 | spyOn(service, 'changeLang').and.callThrough(); 125 | service.changeLangSync(NbTransLang.BG_BG); 126 | expect(service.changeLang).toHaveBeenCalledTimes(1); 127 | })); 128 | 129 | describe('#translationSync()', () => { 130 | let service: NbTransService; 131 | let toolService: NbTransToolsService; 132 | beforeEach(async () => { 133 | await TestBed.configureTestingModule({ 134 | imports: [NbTransTestingModule], 135 | providers: [ 136 | { provide: NB_TRANS_DEFAULT_LANG, useValue: NbTransLang.ZH_CN }, 137 | { provide: NB_TRANS_LOADER, useValue: transLoader.staticLoader }, 138 | ], 139 | }); 140 | service = TestBed.inject(NbTransService); 141 | toolService = TestBed.inject(NbTransToolsService); 142 | }); 143 | 144 | translationSyncTestData.forEach(item => { 145 | it(item.title, done => { 146 | service 147 | .subscribeLoadDefaultOver() 148 | .pipe( 149 | filter(isLoadOver => isLoadOver), 150 | take(1) 151 | ) 152 | .subscribe(() => { 153 | spyOn(toolService, 'isTranslatedStringValid').and.callThrough(); 154 | const result = service.translationSync(item.test.key, item.test.options); 155 | expect(result).toEqual(item.expect.result); 156 | expect(toolService.isTranslatedStringValid).toHaveBeenCalled(); 157 | done(); 158 | }); 159 | }); 160 | }); 161 | }); 162 | 163 | describe('#translationAsync()', () => { 164 | let service: NbTransService; 165 | beforeEach(async () => { 166 | await TestBed.configureTestingModule({ 167 | imports: [NbTransTestingModule], 168 | providers: [ 169 | { provide: NB_TRANS_DEFAULT_LANG, useValue: NbTransLang.ZH_CN }, 170 | { provide: NB_TRANS_LOADER, useValue: transLoader.dynamicLoader }, 171 | ], 172 | }); 173 | service = TestBed.inject(NbTransService); 174 | }); 175 | 176 | it('not change lang', done => { 177 | service 178 | .subscribeLoadDefaultOver() 179 | .pipe( 180 | filter(result => result), 181 | switchMap(() => service.translationAsync('title')) 182 | ) 183 | .pipe(take(1)) 184 | .subscribe(transContent => { 185 | expect(transContent).toEqual('标题 '); 186 | done(); 187 | }); 188 | }); 189 | 190 | it('change lang as en', done => { 191 | service 192 | .subscribeLoadDefaultOver() 193 | .pipe( 194 | filter(result => result), 195 | switchMap(() => service.changeLang(NbTransLang.EN)), 196 | switchMap(() => service.translationAsync('title')) 197 | ) 198 | .pipe(take(1)) 199 | .subscribe(transContent => { 200 | expect(transContent).toEqual('title '); 201 | done(); 202 | }); 203 | }); 204 | }); 205 | 206 | it('when failure to load default lang', done => { 207 | const langLoader = () => Promise.reject(); 208 | const transLoader = { 209 | [NbTransLang.EN_US]: langLoader, 210 | }; 211 | TestBed.configureTestingModule({ 212 | imports: [NbTransTestingModule], 213 | providers: [ 214 | { provide: NB_TRANS_DEFAULT_LANG, useValue: NbTransLang.EN_US }, 215 | { provide: NB_TRANS_LOADER, useValue: transLoader }, 216 | { provide: NB_TRANS_MAX_RETRY, useValue: 3 }, 217 | ], 218 | }); 219 | spyOn(transLoader, NbTransLang.EN_US).and.callThrough(); 220 | const service = TestBed.inject(NbTransService); 221 | service 222 | .subscribeLoadDefaultOver() 223 | .pipe(take(1)) 224 | .subscribe(() => { 225 | expect(transLoader[NbTransLang.EN_US]).toHaveBeenCalledTimes(4); 226 | done(); 227 | }); 228 | }); 229 | 230 | it('#getBrowserLang()', inject([NbTransService], (service: NbTransService) => { 231 | expect(service.getBrowserLang()).toEqual(window.navigator.language); 232 | 233 | spyOnProperty(window.navigator, 'language').and.returnValue(undefined as any); 234 | expect(service.getBrowserLang()).toEqual(undefined); 235 | 236 | spyOnProperty(window, 'navigator').and.returnValue(undefined as any); 237 | expect(service.getBrowserLang()).toEqual(undefined); 238 | })); 239 | 240 | it('#NbTransService.getBrowserLang()', () => { 241 | expect(NbTransService.getBrowserLang()).toEqual(window.navigator.language); 242 | 243 | spyOnProperty(window.navigator, 'language').and.returnValue(undefined as any); 244 | expect(NbTransService.getBrowserLang()).toEqual(undefined); 245 | 246 | spyOnProperty(window, 'navigator').and.returnValue(undefined as any); 247 | expect(NbTransService.getBrowserLang()).toEqual(undefined); 248 | }); 249 | 250 | it('#getBrowserLangs()', inject([NbTransService], (service: NbTransService) => { 251 | expect(service.getBrowserLangs()).toEqual(window.navigator.languages); 252 | 253 | spyOnProperty(window.navigator, 'languages').and.returnValue(undefined as any); 254 | expect(service.getBrowserLangs()).toEqual(undefined); 255 | 256 | spyOnProperty(window, 'navigator').and.returnValue(undefined as any); 257 | expect(service.getBrowserLangs()).toEqual(undefined); 258 | })); 259 | 260 | it('#NbTransService.getBrowserLangs()', () => { 261 | expect(NbTransService.getBrowserLangs()).toEqual(window.navigator.languages); 262 | 263 | spyOnProperty(window.navigator, 'languages').and.returnValue(undefined as any); 264 | expect(NbTransService.getBrowserLangs()).toEqual(undefined); 265 | 266 | spyOnProperty(window, 'navigator').and.returnValue(undefined as any); 267 | expect(NbTransService.getBrowserLangs()).toEqual(undefined); 268 | }); 269 | }); 270 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v20.0.0 2 | ## Breaking Changes 3 | - feat: Upgrade `angular` to `v20`; 4 | - feat: Upgrade `@bigbear713/nb-common` to `^20.0.0`; 5 | 6 | --- 7 | 8 | # v19.0.0 9 | ## Breaking Changes 10 | - feat: Upgrade `angular` to `v19`; 11 | - feat: Upgrade `@bigbear713/nb-common` to `^19.0.0`; 12 | 13 | --- 14 | 15 | # v18.0.0 16 | ## Breaking Changes 17 | - feat: Upgrade `angular` to `v18`; 18 | - feat: Upgrade `@bigbear713/nb-common` to `^18.0.0`; 19 | 20 | --- 21 | 22 | # v17.0.0 23 | ## Breaking Changes 24 | - feat: Upgrade `angular` to `v17`; 25 | - feat: Upgrade `@bigbear713/nb-common` to `^17.0.0`; 26 | 27 | --- 28 | 29 | # v16.0.0 30 | ## Breaking Changes 31 | - feat: Upgrade `angular` to `^16.0.0`; 32 | - feat: Upgrade `@bigbear713/nb-common` to `^16.0.0`; 33 | - feat: [INbTransParams](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#inbtransparams) - Limit the naming rules about params's key: Consists of `letters, numbers, _, and $`, and the number can't be the first character; 34 | - feat: [``](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nb-transnb-trans) - The `key` prop is required: [issue/25](https://github.com/bigBear713/nb-trans/issues/25); 35 | 36 | ## Dependencies 37 | - chore: Remove `uuid` lib; 38 | 39 | ## [Services](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Services "Services") 40 | - refactor: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtransservice "NbTransService") - Refactor the function for handling the dynamic params in translated string; 41 | - fix: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtransservice "NbTransService") - Fix the bug abouf will get object data when the trans key is incomplete: [issue/27](https://github.com/bigBear713/nb-trans/issues/27); 42 | - feat: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtransservice "NbTransService") - Support for spaces between `dynamic params` and `{{}}` in translated string: [issue/34](https://github.com/bigBear713/nb-trans/issues/34); 43 | 44 | ## [Components](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Components "Components") 45 | - feat: [`[nb-trans]`](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nb-trans) - Add the component which the selector is `[nb-trans]`: [issue/22](https://github.com/bigBear713/nb-trans/issues/22); 46 | - perf: [``](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nb-transnb-trans) - Use the UnsubscribeService to manage the rxjs subscription; 47 | - fix: [``](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nb-transnb-trans) - Fix the bug about the trans result is wrong in some case: [issue/28](https://github.com/bigBear713/nb-trans/issues/28); 48 | 49 | ## [Pipes](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Pipes "Pipes") 50 | - feat: [nbTrans](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtrans-transformkey-string-options-inbtransoptions-string) - Use the UnsubscribeService to manage the rxjs subscription; 51 | 52 | ## [Tokens](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Tokens "Tokens") 53 | - feat: [NB_TRANS_PARAM_KEY_INVALID_WARNING](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nb_trans_param_key_invalid_warning) - Whether to print a warning info in the console, when a param key is invalid; 54 | 55 | --- 56 | 57 | # v15.1.0 58 | ## [Components](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Components "Components") 59 | - feat: [``](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nb-transnb-trans) - Support to be imported as a `standalone component`; 60 | - feat: [`[nb-trans-subcontent]`](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nb-trans-subcontent) - Support to be imported as a `standalone component`; 61 | 62 | ## [Pipes](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Pipes "Pipes") 63 | - feat: [nbTrans](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtrans-transformkey-string-options-inbtransoptions-string) - Support to be imported as a `standalone component`; 64 | 65 | --- 66 | 67 | # v15.0.0 68 | ## Breaking Changes 69 | - feat: Upgrade `angular` to `^15.0.0`; 70 | - feat: Upgrade `@bigbear713/nb-common` to `^15.0.0`; 71 | 72 | ## Dependencies 73 | - feat: Upgrade `uuid` to `^9.0.0`; 74 | 75 | ## [Tokens](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Tokens "Tokens") 76 | - feat: [NB_TRANS_MAX_RETRY](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nb_trans_max_retry) - Add `NB_TRANS_MAX_RETRY`, mark `NB_TRANS_MAX_RETRY_TOKEN` as `deprecated`; 77 | 78 | ## [Enums](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Enums "Enums") 79 | - feat: [NbTransLang](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtranslang) - Add `NbTransLang`, mark `NbTransLangEnum` as `deprecated`; 80 | - feat: [NbTransSentenceItem](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtranssentenceitem) - Add `NbTransSentenceItem`, mark `NbTransSentenceItemEnum` as `deprecated`; 81 | 82 | ## [Services](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Services "Services") 83 | - refactor: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtransservice "NbTransService") - optimize code; 84 | 85 | ## [Pipes](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Pipes "Pipes") 86 | - refactor: [nbTrans](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtrans-transformkey-string-options-inbtransoptions-string) - optimize code; 87 | 88 | --- 89 | 90 | # v14.0.0 91 | ## Breaking Changes 92 | - feat: Upgrade `angular` to `^14.0.0`; 93 | - feat: Upgrade `@bigbear713/nb-common` to `^14.0.0`; 94 | 95 | --- 96 | 97 | # v13.0.1 98 | ## Breaking Changes 99 | - fix: Update the version of `nb-common` as `^13.0.0`; 100 | 101 | --- 102 | 103 | # v13.0.0 104 | ## Breaking Changes 105 | - feat: Upgrade `angular` to `^13.0.0`; 106 | 107 | --- 108 | 109 | # v12.1.0 110 | ## [Services](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Services "Services") 111 | - feat: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtransservice "NbTransService") - `NbTransService.getBrowserLang()` can the first language of browser directly; 112 | - depr: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtransservice "NbTransService") - `getBrowserLang()` has been marked as `deprecated`; 113 | - feat: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtransservice "NbTransService") - `NbTransService.getBrowserLangs()` can a language array known directly; 114 | - depr: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtransservice "NbTransService") - `getBrowserLangs()` has been marked as `deprecated`; 115 | 116 | --- 117 | 118 | # v12.0.0 119 | ## [Module](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Module "Module") 120 | - feat: [NbTransModule](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtransmodule) - provide useful `component`, `pipe`; 121 | - feat: [NbTransTestingModule](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtranstestingmodule) - provide the env to unit test; 122 | 123 | ## [Services](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Services "Services") 124 | - feat: [NbTransService](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtransservice "NbTransService") - provide the translate feature; 125 | 126 | ## [Components](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Components "Components") 127 | - feat: [``](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nb-transnb-trans) - when you need to translate the sentence which include components; 128 | - feat: [`[nb-trans-subcontent]`](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nb-trans-subcontent) - it is a common solution when the sentence include some nested componets (you can impletement yourself to meet the requirement); 129 | 130 | ## [Pipes](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Pipes "Pipes") 131 | - feat: [nbTrans](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtrans-transformkey-string-options-inbtransoptions-string) - the pipe which to tranlate the text; 132 | 133 | ## [Tokens](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Tokens "Tokens") 134 | - feat: [NB_TRANS_DEFAULT_LANG](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nb_trans_default_lang) - set the default langs; 135 | - feat: [NB_TRANS_LOADER](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nb_trans_loader) - the loader of translated text; 136 | - feat: [NB_TRANS_MAX_RETRY](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nb_trans_max_retry) - the max retry time when failure to load translated file; 137 | 138 | ## [Interfaces](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Interfaces "Interfaces") 139 | - feat: [INbTransLoader](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#inbtransloader) - the translated file loader; 140 | - feat: [INbTransOptions](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#inbtransoptions) - the config of translation; 141 | - feat: [INbTransParams](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#inbtransparams) - the params in the translated text; 142 | - feat: [INbTransChangeLang](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#inbtranschangelang) - the result of switching language; 143 | - feat: [INbTransSentencePart](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#inbtranssentencepart) - the part of sentence; 144 | - feat: [INbTransSentenceCompPart](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#inbtranssentencecomppart) - the part which include component in sentence; 145 | 146 | ## [Enums](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#Enums "Enums") 147 | - feat: [NbTransLang](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtranslang) - the enum of common language; 148 | - feat: [NbTransSentenceItem](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md#nbtranssentenceitem) - the enum of sentence item; 149 | -------------------------------------------------------------------------------- /projects/nb-trans/README.CN.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # @bigbear713/nb-trans 4 | 5 | Angular translation lib by bigBear713. 6 | 7 | [OnlineDemo](https://bigBear713.github.io/nb-trans/) 8 | 9 | [Bug Report](https://github.com/bigBear713/nb-trans/issues) 10 | 11 | [Feature Request](https://github.com/bigBear713/nb-trans/issues) 12 | 13 |
14 | 15 | ## Document 16 | - [中文](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md "文档 - 中文") 17 | - [English](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.md "Document - English") 18 | 19 |
20 | 21 | --- 22 | 23 | ## Changelog 24 | - [中文](https://github.com/bigBear713/nb-trans/blob/main/CHANGELOG.CN.md "更新日志 - 中文") 25 | - [English](https://github.com/bigBear713/nb-trans/blob/main/CHANGELOG.md "Changelog - English") 26 | 27 |
28 | 29 | --- 30 | 31 | ## Feature 32 | - 支持翻译文本懒加载,或者急性加载; 33 | - 支持切换语言时,不刷新页面自动更新翻译文本; 34 | - 支持设置翻译文本加载失败时的重试次数; 35 | - 支持翻译文本中带有参数; 36 | - 支持翻译文本中带有组件的复杂场景; 37 | - 支持组件的更新策略为`ChangeDetectionStrategy.OnPush`; 38 | - 支持在`standalone component`中使用; 39 | - 支持以`standalone component`的方式引入; 40 | 41 |
42 | 43 | --- 44 | 45 | ## Version 46 | ###### nb-trans的大版本和Angular的大版本保持对应关系 47 | | @bigbear713/nb-trans | @angular/core | 48 | | --- | --- | 49 | | ^12.0.0 | ^12.0.0 | 50 | | ^13.0.0 | ^13.0.0 | 51 | | ^14.0.0 | ^14.0.0 | 52 | | ^15.0.0 | ^15.0.0 | 53 | | ^16.0.0 | ^16.0.0 | 54 | | ^17.0.0 | ^17.0.0 | 55 | | ^18.0.0 | ^18.0.0 | 56 | | ^19.0.0 | ^19.0.0 | 57 | | ^20.0.0 | ^20.0.0 | 58 | 59 |
60 | 61 | --- 62 | 63 | ## Installation 64 | ```bash 65 | $ npm i @bigbear713/nb-trans 66 | // or 67 | $ yarn add @bigbear713/nb-trans 68 | ``` 69 | 70 |
71 | 72 | --- 73 | 74 | ## API 75 | ### Module 76 | 77 | #### NbTransModule 78 | ###### 多语言模块。引入该模块后,可使用`component`,`pipe`。`service`不需要引入该模块也可使用,默认为全局。 79 | 80 | #### NbTransTestingModule 81 | ###### 多语言测试模块。用于Unit Test。 82 | 83 |
84 | 85 | --- 86 | 87 | ### Services 88 | 89 | #### NbTransService 90 | ##### `v12.0.0` 91 | ###### 提供多语言翻译功能的`service` 92 | 93 | ##### Properties 94 | | Properties | Type | Description | Version | 95 | | ------------ | ------------ | ------------ | ------------ | 96 | | lang | `string` | 当前语言值 | `v12.0.0` | 97 | | loadDefaultOver | `boolean` | 默认语言的翻译文本是否加载完毕 | `v12.0.0` | 98 | 99 | ##### Methods 100 | | Name | Return | Description | Scenes | Version | 101 | | ------------ | ------------ | ------------ | ------------ | ------------ | 102 | | changeLang(lang: string) | `Observable` | 切换语言。lang参数需要和`NB_TRANS_LOADER`中的key值相对应。是一个观察者异步事件。当切换的语言的翻译文本被加载完成后才会返回结果。订阅后无需取消订阅,因为当语言切换后(不管是否成功),将自动complete。结果的具体内容见下方[`INbTransChangeLang`](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#inbtranschangelang)的定义 | 需要切换语言时 | `v12.0.0` | 103 | | changeLangSync(lang: string) | `void` | 切换语言。lang参数需要和`NB_TRANS_LOADER`中的key值相对应。是一个同步事件。但是并不保证语言切换成功,以及何时成功。 | 适合只想触发切换语言操作,并不关心切换后的结果的场景 | `v12.0.0` | 104 | | getBrowserLang()`deprecated` | `string | undefined` | 获取浏览器的首选语言 | 适合只关心浏览器界面语言的场景 | `v12.0.0` | 105 | | NbTransService.getBrowserLang() | `string | undefined` | 获取浏览器的首选语言 | 适合只关心浏览器界面语言的场景 | `v12.1.0` | 106 | | getBrowserLangs()`deprecated` | `readonly string[]| undefined` | 返回一个用户已知语言的数组,并按照优先级排列 | 适合需要知道用户已知语言的场景 | `v12.0.0` | 107 | | NbTransService.getBrowserLangs() | `readonly string[]| undefined` | 返回一个用户已知语言的数组,并按照优先级排列 | 适合需要知道用户已知语言的场景 | `v12.1.0` | 108 | | translationAsync(key: string, options?: INbTransOptions) | `Observable` | 根据key和options异步获取翻译文本。options选填,具体配置见下方[`INbTransOptions`](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#inbtransoptions)定义。返回一个观察者对象。获取值后如果未取消订阅,当语言被切换时,将会订阅、获取切换后的语言下的翻译文本 | 适合将订阅事件变量在模板中使用,推荐结合ng官方的`async`管道使用。 | `v12.0.0` | 109 | | translationSync(key: string, options?: INbTransOptions) | `string` | 根据key和options同步获取翻译文本。options选填,具体配置见下方[`INbTransOptions`](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#inbtransoptions)定义。因为是同步获取,所以返回的获取后的文本内容。当语言被切换时,需要重新调用该方法才能获取切换后的语言下的文本。 | 适合文本内容临时使用,每次显示文本都需要重新获取的场景。比如通过service动态创建modal时,设置modal的title。 | `v12.0.0` | 110 | | subscribeLangChange() | `Observable` | 语言切换的订阅事件。返回一个观察者对象。当订阅未取消时,语言被切换时,会自动被订阅到。订阅的内容为切换后的语言值 | 适合需要根据不同语言进行动态调整的地方 | `v12.0.0` | 111 | | subscribeLoadDefaultOver() | `Observable` | 默认语言翻译文本是否加载完成的订阅事件。加载成功时订阅到的值为true,反之为false。加载完成后(不管是否加载成功)会自动complete,因此可以不用取消订阅 | 适合整个项目最外层的数据准备。当默认语言的翻译文本被加载完成后再显示整个项目,体验效果更好. | `v12.0.0` | 112 | 113 | ##### Usage 114 | ```ts 115 | constructor(private transService: NbTransService) {} 116 | 117 | // 切换语言,异步事件,subscribe()是必需的 118 | this.transService.changeLang(lang).subscribe(result=>{ 119 | // result是切换后的结果 120 | }); 121 | 122 | // 切换语言,同步事件,但不保证语言切换成功 123 | this.transService.changeLangSync(lang); 124 | 125 | NbTransService.transService.getBrowserLang(); // 'en' 126 | 127 | NbTransService.transService.getBrowserLangs(); // ['en'] 128 | 129 | // 语言异步翻译。可订阅获取翻译后的值,也可在模板中和async管道结合使用 130 | const trans$ = this.transService.translationAsync('title'); 131 | trans$.subscribe(trans=>{ 132 | // trans是翻译后的文本 133 | }); 134 | 135 | // 语言同步翻译。获取当前语言下的翻译内容 136 | const trans = this.transService.translationSync('title'); // trans是翻译后的文本 137 | 138 | // 语言切换订阅。当语言被切换时,会触发订阅事件,得到切换后的语言 139 | this.transService.subscribeLangChange().subscribe(lang=>{ 140 | // lang是切换后的语言值 141 | }); 142 | 143 | // 默认语言翻译文本加载结束订阅事件。当翻译文本被加载完成时,会触发订阅事件 144 | this.transService.subscribeLoadDefaultOver().subscribe(over=>{ 145 | // over是加载后的结果 146 | }); 147 | ``` 148 | 149 |
150 | 151 | --- 152 | 153 | ### Components 154 | 155 | #### `` 156 | ##### `v12.0.0` 157 | ##### 从`v15.1.0`开始为`standalone component` 158 | ###### 当翻译文本中含有组件等复杂场景时使用的组件。当语言被切换时,组件渲染的内容将自动更新 159 | 160 | ##### Input 161 | | Name | Type | Mandatory | Default | Description | Version | 162 | | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | 163 | | components | `TemplateRef<{ content: string | TemplateRef; list?: INbTransSentencePart[] }>[]` | false | [] | 翻译文本中的对应的组件。 | `v12.0.0` | 164 | | key | `string` | true | `''` | 获取翻译文本的key值。自`v16.0.0`起,为必需属性。 | `v12.0.0` | 165 | | options | `INbTransOptions` | false | {} | 翻译的配置信息。具体配置见下方的[`INbTransOptions`](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#inbtransoptions)定义。 | `v12.0.0` | 166 | 167 | ##### Usage 168 | ```html 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | {{compContent}} 183 | 184 | 185 | 186 | 187 | ``` 188 | ```ts 189 | // v15.1.0新增 190 | // 在NgModule中引入 191 | @NgModule({ 192 | imports:[NbTransComponent], 193 | // ... 194 | }) 195 | export class XXXModule{} 196 | 197 | // 在standalone component中引入 198 | @Component({ 199 | standalone:true, 200 | imports:[NbTransComponent], 201 | // ... 202 | }) 203 | export class XXXComponent{} 204 | ``` 205 | 206 |
207 | 208 | #### `[nb-trans]` 209 | ##### `v16.0.0` 210 | ###### 当翻译文本中含有组件等复杂场景时使用的组件。当不想使用"\"标签元素,而是自己选择原生html标签时使用,比如"\
","\"。当语言被切换时,组件渲染的内容将自动更新。 211 | 212 | ##### Input 213 | | Name | Type | Mandatory | Default | Description | Version | 214 | | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | 215 | | nb-trans | `string` | true | `''` | 获取翻译文本的key值 | `v16.0.0` | 216 | | nb-trans-components | `TemplateRef<{ content: string | TemplateRef; list?: INbTransSentencePart[] }>[]` | false | [] | 翻译文本中的对应的组件。 | `v16.0.0` | 217 | | nb-trans-options | `INbTransOptions` | false | {} | 翻译的配置信息。具体配置见下方的[`INbTransOptions`](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#inbtransoptions)定义。 | `v16.0.0` | 218 | 219 | ##### Usage 220 | ```html 221 | 222 |
223 |
224 | 225 | 226 |

227 |

228 | 229 | 230 |
231 |
232 | 233 | 234 |
235 | 236 | {{compContent}} 237 | 238 | 239 | 240 | 241 | ``` 242 | ```ts 243 | // imported in NgModule 244 | @NgModule({ 245 | imports:[NbTrans2Component], 246 | // ... 247 | }) 248 | export class XXXModule{} 249 | 250 | // imported in standalone component 251 | @Component({ 252 | standalone:true, 253 | imports:[NbTrans2Component], 254 | // ... 255 | }) 256 | export class XXXComponent{} 257 | ``` 258 | 259 |
260 | 261 | #### `[nb-trans-subcontent]` 262 | ##### `v12.0.0` 263 | ##### 从`v15.1.0`开始为`standalone component` 264 | ###### 当翻译文本中含有组件嵌套时使用的一种官方提供的方案(可根据需要有自己的实现方式),会将嵌套的组件内容渲染出来。selector为attribute,可用于`
`, ``, ``,``等。该组件是搭配``使用,请勿单独使用。 265 | 266 | ##### Input 267 | | Name | Type | Mandatory | Default | Description | Version | 268 | | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | 269 | | nb-trans-subcontent | `string | TemplateRef` | true | `''` | 要显示的子内容。接受`string`类型和`TemplateRef`类型。当为`string`类型时,直接渲染出来,`subcontentList`输入参数不起作用。当为`TemplateRef`类型时,`subcontentList`参数将起作用。自`v16.0.0`起,为必需属性 | `v12.0.0` | 270 | | subcontentList | `INbTransSentencePart[]` | false | [] | 仅当`nb-trans-subcontent`为`TemplateRef`类型时,且该内容为``的components输入属性的子内容时有效。`[nb-trans-subcontent]`会将该参数的值传到template的context中。详情见下方Usage | `v12.0.0` | 271 | 272 | ##### Usage 273 | ```html 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | {{comContent}} 285 | 286 | ``` 287 | ```ts 288 | // v15.1.0新增 289 | // 在NgModule中引入 290 | @NgModule({ 291 | imports:[NbTransSubcontentComponent], 292 | // ... 293 | }) 294 | export class XXXModule{} 295 | 296 | // 在standalone component中引入 297 | @Component({ 298 | standalone:true, 299 | imports:[NbTransSubcontentComponent], 300 | // ... 301 | }) 302 | export class XXXComponent{} 303 | ``` 304 | 305 |
306 | 307 | --- 308 | 309 | ### Pipes 310 | 311 | #### nbTrans: `transform(key: string, options?: INbTransOptions): string` 312 | ##### `v12.0.0` 313 | ##### 从`v15.1.0`开始为`standalone component` 314 | ###### 翻译文本的管道,可用于在模版中根据key值翻译文本。当语言被切换时,组件渲染的内容将自动更新 315 | 316 | ##### Params 317 | | Name | Type | Mandatory | Description | Version | 318 | | ------------ | ------------ | ------------ | ------------ | ------------ | 319 | | key | `string` | true | 翻译文本的key值 | `v12.0.0` | 320 | | options | `INbTransOptions` | false | 翻译配置。具体配置见下方的[`INbTransOptions`](https://github.com/bigBear713/nb-trans/blob/main/projects/nb-trans/README.CN.md#inbtransoptions)定义 | `v12.0.0` | 321 | 322 | ##### Return 323 | | Type | Description | 324 | | ------------ | ------------ | 325 | | `string` | 翻译后的文本 | 326 | 327 | ##### Usage 328 | ```html 329 | 330 |
{{'title'|nbTrans}}
331 | 332 | 333 |
{{'title'|nbTrans:options}}
334 |
{{'helloWorld'|nbTrans:({prefix:'content'})}}
335 | ``` 336 | ```ts 337 | // v15.1.0新增 338 | // 在NgModule中引入 339 | @NgModule({ 340 | imports:[NbTransPipe], 341 | // ... 342 | }) 343 | export class XXXModule{} 344 | 345 | // 在standalone component中引入 346 | @Component({ 347 | standalone:true, 348 | imports:[NbTransPipe], 349 | // ... 350 | }) 351 | export class XXXComponent{} 352 | ``` 353 | 354 |
355 | 356 | --- 357 | 358 | ### Tokens 359 | 360 | #### NB_TRANS_DEFAULT_LANG 361 | ##### string 362 | ##### `v12.0.0` 363 | ###### 用于设置默认语言,初始化`NbTransService`实例时将自动加载该语言的文本内容。不设置时默认为`NbTransLang.ZH_CN`。一般只在AppModule设置一次 364 | 365 | ##### Usage 366 | ```ts 367 | providers: [ 368 | // ... 369 | { 370 | provide: NB_TRANS_DEFAULT_LANG, 371 | useValue: NbTransLang.ZH_CN, 372 | }, 373 | // ... 374 | ] 375 | ``` 376 | 377 |
378 | 379 | #### NB_TRANS_LOADER 380 | ##### { [key: string]: INbTransLoader } 381 | ##### `v12.0.0` 382 | ###### 翻译文本加载器。加载器支持急性加载和懒加载。一般只在AppModule设置一次 383 | - 急性加载:直接引入翻译文本内容,作为值赋给对应的语言。急性加载会增大项目初始化文件的体积. 384 | - 懒加载:通过`http.get()`或者`import()`等方式加载翻译文本文件。当翻译文本文件为`json`格式时,可使用`http.get()`加载。当翻译文本文件为`ts`格式时,可使用`import()`加载。 385 | 386 | ##### Usage 387 | ###### 急性加载 388 | ```ts 389 | providers: [ 390 | // ... 391 | { 392 | provide: NB_TRANS_LOADER, 393 | useValue: { 394 | [NbTransLang.ZH_CN]: zhCNTrans, 395 | [NbTransLang.EN]: enTrans, 396 | } 397 | } 398 | // ... 399 | ] 400 | ``` 401 | ###### 懒加载 402 | - 翻译文本文件为json格式 403 | ```ts 404 | providers: [ 405 | // ... 406 | { 407 | provide: NB_TRANS_LOADER, 408 | useFactory: (http: HttpClient) => ({ 409 | // dyn load and the content is a json file 410 | // the loader fn return value can be Observable/Promise type 411 | // [NbTransLang.EN]: () => http.get('./assets/localization/en/translations.json').toPromise(), 412 | [NbTransLang.EN]: () => http.get('./assets/localization/en/translations.json'), 413 | // [NbTransLang.ZH_CN]: () => http.get('./assets/localization/zh-CN/translations.json').toPromise(), 414 | [NbTransLang.ZH_CN]: () => http.get('./assets/localization/zh-CN/translations.json'), 415 | }), 416 | deps: [HttpClient] 417 | } 418 | // ... 419 | ] 420 | ``` 421 | - 翻译文本文件为ts格式 422 | ```ts 423 | providers: [ 424 | // ... 425 | { 426 | provide: NB_TRANS_LOADER, 427 | useValue: { 428 | [NbTransLang.EN]: () => import('./localization/en/translations').then(data => data.trans), 429 | [NbTransLang.ZH_CN]: () => import('./localization/zh-CN/translations').then(data => data.trans), 430 | } 431 | } 432 | // ... 433 | ] 434 | ``` 435 | 436 |
437 | 438 | #### NB_TRANS_MAX_RETRY 439 | ##### number 440 | ##### `v15.0.0` 441 | #### NB_TRANS_MAX_RETRY_TOKEN 442 | ##### number 443 | ##### `v12.0.0`, 从`v15.0.0`开始为`@deprecated` 444 | ###### 翻译文本加载失败时的最大重试次数,默认为5次。一般只在AppModule设置一次 445 | 446 | ##### Usage 447 | ```ts 448 | providers: [ 449 | // ... 450 | { 451 | provide: NB_TRANS_MAX_RETRY, 452 | useValue: 3 453 | }, 454 | // ... 455 | ] 456 | ``` 457 | 458 |
459 | 460 | #### NB_TRANS_PARAM_KEY_INVALID_WARNING 461 | ##### boolean 462 | ##### `v16.0.0` 463 | ###### 当 param key 不符合规则时,是否在 console 中打印警告信息。默认为 true。在生产环境下(调用`enableProdMode()`时,也将处于生产模式),将自动关闭打印警告信息的设置。 464 | 465 | ##### Usage 466 | ```ts 467 | providers: [ 468 | // ... 469 | { 470 | provide: NB_TRANS_PARAM_KEY_INVALID_WARNING, 471 | useValue: false 472 | }, 473 | // ... 474 | ] 475 | ``` 476 | 477 |
478 | 479 | --- 480 | 481 | ### Interfaces 482 | 483 | #### INbTransLoader 484 | ##### `v12.0.0` 485 | ###### 文本加载器 486 | | Property | Type | Mandatory | Description | Version | 487 | | ------------ | ------------ | ------------ | ------------ | ------------ | 488 | | [langKey: string] | `Object | (() => (Observable | Promise))` | false | key值为字符串类型,通常使用对应的语言的字符串值;value为含有文本的Object,或者返回含有文本的Object的Observable或者Promise | `v12.0.0` | 489 | 490 |
491 | 492 | #### INbTransOptions 493 | ##### `v12.0.0` 494 | ###### 翻译配置 495 | | Property | Type | Mandatory | Description | Version | 496 | | ------------ | ------------ | ------------ | ------------ | ------------ | 497 | | prefix | `string` | false | key值的前缀。根据key值获取对应文本时,会自动将该值追加在key值之前,形成一个新的key值,并以此来获取文本 | `v12.0.0` | 498 | | params | `INbTransParams` | false | 翻译文本中的参数。为key值为字符串,value值为字符串的对象 | `v12.0.0` | 499 | | returnKeyWhenEmpty | `boolean` | false | 当根据key值获取不到文本时,是否返回key值。默认为true。当显式设为false时,会返回空字符串 | `v12.0.0` | 500 | 501 |
502 | 503 | #### INbTransParams 504 | ##### `v12.0.0` 505 | #### 注意:param `key` 的命名规则 506 | - 自`v16.0.0`起: 507 | 1. 由 `字母,数字,_和$`组成; 508 | 2. `数字`不能是第一个字符; 509 | ###### 翻译文本中的参数 510 | | Property | Type | Mandatory | Description | Version | 511 | | ------------ | ------------ | ------------ | ------------ | ------------ | 512 | | [key: string] | `string` | false | key值为字符串类型,value值为字符串类型 | `v12.0.0` | 513 | 514 |
515 | 516 | #### INbTransChangeLang 517 | ##### `v12.0.0` 518 | ###### 切换语言的结果 519 | | Property | Type | Mandatory | Description | Version | 520 | | ------------ | ------------ | ------------ | ------------ | ------------ | 521 | | result | `boolean` | true | 切换语言的结果。切换成功时为true,否则为false | `v12.0.0` | 522 | | curLang | `string` | true | 当前语言。如果语言切换失败,则为切换前的语言;否则为切换后的语言 | `v12.0.0` | 523 | 524 |
525 | 526 | #### INbTransSentencePart 527 | ##### `v12.0.0` 528 | ###### 句子部分,可能为`string`或者`INbTransSentenceCompPart`类型。为`string`时,即该句子为文本;为`INbTransSentenceCompPart`时,即该句子中含有需要解析的组件。一般交给组件自己处理便可,可不用关心内部逻辑 529 | 530 |
531 | 532 | #### INbTransSentenceCompPart 533 | ##### `v12.0.0` 534 | ###### 句子中含有组件的部分 535 | | Property | Type | Mandatory | Description | Version | 536 | | ------------ | ------------ | ------------ | ------------ | ------------ | 537 | | index | `number` | true | 组件索引,用于匹配``组件的`components`输入属性中的组件 | `v12.0.0` | 538 | | content | `string` | true | 翻译文本 | `v12.0.0` | 539 | | list | `INbTransSentencePart[]` | false | 文本句子的解析部分 | `v12.0.0` | 540 | 541 |
542 | 543 | --- 544 | 545 | ### Enums 546 | #### NbTransLang 547 | ##### `v15.0.0` 548 | #### NbTransLangEnum 549 | ##### `v12.0.0`, 从`v15.0.0`开始为`@deprecated` 550 | ###### 常用语言枚举。除了默认语言未设置时的默认值外,组件以及服务中均未直接使用该枚举中的值,所以不强制要求使用该枚举。 551 | 552 |
553 | 554 | #### NbTransSentenceItem 555 | ##### `v15.0.0` 556 | #### NbTransSentenceItemEnum 557 | ##### `v12.0.0`, 从`v15.0.0`开始为`@deprecated` 558 | ###### 句子项类型枚举。在对句子内容进行解析时,会将句子分为`STR`,`COMP`和`MULTI_COMP`这3种类型 559 | 560 |
561 | 562 | --- 563 | 564 | ### 贡献 565 | > 欢迎提feature和PR,一起使该项目更好 566 | 567 | bigBear713 568 | 569 |
570 | 571 | --- 572 | 573 | ### License 574 | MIT 575 | --------------------------------------------------------------------------------