Skip to content

Commit bae651a

Browse files
committed
feat: add language pipe
1 parent 3856e47 commit bae651a

20 files changed

+1122
-38
lines changed

CHANGELOG.md

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Changelog
2+
3+
## 0.1.0 - 2023-02-19
4+
5+
### Added
6+
7+
* New `intlLanguage` pipe
8+
9+
## 0.0.2 - 2023-02-19
10+
11+
### Fixed
12+
13+
* Fix package metadata
14+
15+
## 0.0.1 - 2023-02-19
16+
17+
* Initial Release

README.md

+32-6
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ npm install angular-ecmascript-intl --save
1313
Import the `IntlModule`:
1414

1515
```typescript
16-
import { NgModule } from '@angular/core';
17-
import { IntlModule } from 'angular-ecmascript-intl';
16+
import {NgModule} from '@angular/core';
17+
import {IntlModule} from 'angular-ecmascript-intl';
1818

1919
@NgModule({
2020
imports: [
@@ -29,8 +29,8 @@ By default, the pipe will use the browser default locale. If you want to overrid
2929
injection token:
3030

3131
```typescript
32-
import { NgModule } from '@angular/core';
33-
import { INTL_LOCALES } from 'angular-ecmascript-intl';
32+
import {NgModule} from '@angular/core';
33+
import {INTL_LOCALES} from 'angular-ecmascript-intl';
3434

3535
@NgModule({
3636
providers: [
@@ -69,6 +69,25 @@ their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/G
6969

7070
With the `INTL_DATE_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.
7171

72+
### Language pipe
73+
74+
Use the language pipe like the following:
75+
76+
```
77+
{{'en-US' | intlLanguage: options}}
78+
```
79+
80+
The input date can be one of the following:
81+
82+
* string (must be a BCP 47 IETF language tag)
83+
* null
84+
* undefined
85+
86+
The options are the same as the options for `new Intl.DisplayNames()`. For a list of the options, see
87+
their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames).
88+
89+
With the `INTL_LANGUAGE_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.
90+
7291
## Background
7392

7493
Working with Angular's built-in pipes which support internationalization works fine when only supporting one locale.
@@ -79,9 +98,16 @@ to the application. This increases bundle size and load times.
7998
Modern browsers are fully capable of handling internationalization with the `Intl.*` browser APIs. There is no need for
8099
loading any locale date. This package re-implements some Angular built-in pipes such as `date` using these APIs.
81100

82-
## Roadmap
101+
## Feature Roadmap
83102

84-
* Language pipe
85103
* Country pipe
86104
* Number pipe(s)
87105
* Relative time
106+
107+
## Chore Roadmap
108+
109+
* Linting
110+
* Pull request verification
111+
* Automatic dependency updates
112+
* Automatic npm publishing
113+
* Automatic changelog generation

projects/angular-ecmascript-intl/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "angular-ecmascript-intl",
3-
"version": "0.0.2",
3+
"version": "0.1.0",
44
"peerDependencies": {
55
"@angular/common": "^15.1.0",
66
"@angular/core": "^15.1.0"

projects/angular-ecmascript-intl/src/lib/date/intl-date.pipe.spec.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { IntlDatePipe } from './intl-date.pipe';
2-
import { TestBed } from "@angular/core/testing";
3-
import { INTL_LOCALES } from "../locale";
4-
import { INTL_DATE_PIPE_DEFAULT_OPTIONS } from "./intl-date-pipe-default-options";
1+
import {IntlDatePipe} from './intl-date.pipe';
2+
import {TestBed} from "@angular/core/testing";
3+
import {INTL_LOCALES} from "../locale";
4+
import {INTL_DATE_PIPE_DEFAULT_OPTIONS} from "./intl-date-pipe-default-options";
55

66
describe('DatePipe', () => {
77
let testUnit: IntlDatePipe;
@@ -54,11 +54,11 @@ describe('DatePipe', () => {
5454
it('should handle missing Intl.DateTimeFormat browser API', () => {
5555
// @ts-expect-error
5656
spyOn(Intl, 'DateTimeFormat').and.returnValue(undefined);
57-
const consoleWarn = spyOn(console, 'warn');
57+
const consoleError = spyOn(console, 'error');
5858
const date = new Date('2023-02-19');
5959

6060
expect(testUnit.transform('2023-02-19')).toEqual(date.toString());
61-
expect(consoleWarn).toHaveBeenCalledTimes(1);
61+
expect(consoleError).toHaveBeenCalledTimes(1);
6262
});
6363
});
6464

projects/angular-ecmascript-intl/src/lib/date/intl-date.pipe.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Inject, Optional, Pipe, PipeTransform } from '@angular/core';
2-
import { INTL_LOCALES } from "../locale";
3-
import { INTL_DATE_PIPE_DEFAULT_OPTIONS } from "./intl-date-pipe-default-options";
1+
import {Inject, Optional, Pipe, PipeTransform} from '@angular/core';
2+
import {INTL_LOCALES} from "../locale";
3+
import {INTL_DATE_PIPE_DEFAULT_OPTIONS} from "./intl-date-pipe-default-options";
44

55
export type IntlDatePipeOptions = Partial<Intl.DateTimeFormatOptions>;
66

@@ -25,9 +25,9 @@ export class IntlDatePipe implements PipeTransform {
2525
}
2626

2727
try {
28-
return new Intl.DateTimeFormat(this.locale ?? undefined, { ...this.defaultOptions, ...options }).format(date);
28+
return new Intl.DateTimeFormat(this.locale ?? undefined, {...this.defaultOptions, ...options}).format(date);
2929
} catch (e) {
30-
console.warn('Error while parsing the date', e);
30+
console.error('Error while transforming the date', e);
3131
return date.toString();
3232
}
3333
}

projects/angular-ecmascript-intl/src/lib/intl.module.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { NgModule } from '@angular/core';
2-
import { IntlDatePipe } from "./date/intl-date.pipe";
1+
import {NgModule} from '@angular/core';
2+
import {IntlDatePipe} from "./date/intl-date.pipe";
3+
import {IntlLanguagePipe} from './language/intl-language.pipe';
34

45
@NgModule({
56
imports: [
67
IntlDatePipe,
8+
IntlLanguagePipe,
79
],
810
exports: [
911
IntlDatePipe,
12+
IntlLanguagePipe,
1013
],
1114
})
1215
export class IntlModule {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import {InjectionToken} from "@angular/core";
2+
import {IntlLanguagePipeOptions} from "./intl-language.pipe";
3+
4+
export const INTL_LANGUAGE_PIPE_DEFAULT_OPTIONS = new InjectionToken<IntlLanguagePipeOptions>('IntlLanguagePipeDefaultOptions');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import {IntlLanguagePipe} from './intl-language.pipe';
2+
import {TestBed} from "@angular/core/testing";
3+
import {INTL_LOCALES} from "../locale";
4+
import {INTL_LANGUAGE_PIPE_DEFAULT_OPTIONS} from "./intl-language-pipe-default-options";
5+
6+
describe('IntlLanguagePipe', () => {
7+
let testUnit: IntlLanguagePipe;
8+
9+
describe('parsing', () => {
10+
beforeEach(() => {
11+
testUnit = new IntlLanguagePipe('en-US');
12+
});
13+
14+
it('should create an instance', () => {
15+
expect(testUnit).toBeTruthy();
16+
});
17+
18+
it('should handle null values', () => {
19+
expect(testUnit.transform(null)).toBeNull();
20+
});
21+
22+
it('should handle undefined values', () => {
23+
expect(testUnit.transform(undefined)).toBeNull();
24+
});
25+
26+
it('should handle empty strings', () => {
27+
expect(testUnit.transform('')).toBeNull();
28+
});
29+
30+
it('should transform numbers', () => {
31+
expect(testUnit.transform('en-US')).toEqual('American English');
32+
});
33+
34+
it('should handle missing Intl.DisplayNames browser API', () => {
35+
// @ts-expect-error
36+
spyOn(Intl, 'DisplayNames').and.returnValue(undefined);
37+
const consoleError = spyOn(console, 'error');
38+
39+
expect(testUnit.transform('en-US')).toEqual('en-US');
40+
expect(consoleError).toHaveBeenCalledTimes(1);
41+
});
42+
});
43+
44+
describe('internationalization', () => {
45+
it('should respect the set locale', () => {
46+
TestBed.configureTestingModule({
47+
providers: [
48+
IntlLanguagePipe,
49+
{
50+
provide: INTL_LOCALES,
51+
useValue: 'de-DE',
52+
},
53+
],
54+
});
55+
testUnit = TestBed.inject(IntlLanguagePipe);
56+
57+
expect(testUnit.transform('de-AT')).toEqual('Österreichisches Deutsch');
58+
});
59+
60+
it('should fall back to the browser default locale', () => {
61+
TestBed.configureTestingModule({providers: [IntlLanguagePipe]});
62+
63+
const result1 = TestBed.inject(IntlLanguagePipe).transform('en-US');
64+
const result2 = new IntlLanguagePipe(navigator.language).transform('en-US');
65+
66+
expect(result1).toEqual(result2);
67+
});
68+
});
69+
70+
describe('options', () => {
71+
it('should respect the setting from default config', () => {
72+
TestBed.configureTestingModule({
73+
providers: [
74+
IntlLanguagePipe,
75+
{
76+
provide: INTL_LOCALES,
77+
useValue: 'de-DE',
78+
},
79+
{
80+
provide: INTL_LANGUAGE_PIPE_DEFAULT_OPTIONS,
81+
useValue: {
82+
languageDisplay: 'standard',
83+
},
84+
},
85+
],
86+
});
87+
testUnit = TestBed.inject(IntlLanguagePipe);
88+
89+
expect(testUnit.transform('de-AT')).toEqual('Deutsch (Österreich)');
90+
91+
});
92+
93+
it('should give the user options a higher priority', () => {
94+
TestBed.configureTestingModule({
95+
providers: [
96+
IntlLanguagePipe,
97+
{
98+
provide: INTL_LOCALES,
99+
useValue: 'de-DE',
100+
},
101+
{
102+
provide: INTL_LANGUAGE_PIPE_DEFAULT_OPTIONS,
103+
useValue: {
104+
languageDisplay: 'dialect',
105+
},
106+
},
107+
],
108+
});
109+
testUnit = TestBed.inject(IntlLanguagePipe);
110+
111+
expect(testUnit.transform('de-AT', {languageDisplay: 'standard'})).toEqual('Deutsch (Österreich)');
112+
});
113+
114+
it('should not override the type option', () => {
115+
TestBed.configureTestingModule({
116+
providers: [
117+
IntlLanguagePipe,
118+
{
119+
provide: INTL_LOCALES,
120+
useValue: 'de-DE',
121+
},
122+
{
123+
provide: INTL_LANGUAGE_PIPE_DEFAULT_OPTIONS,
124+
useValue: {
125+
type: 'region',
126+
},
127+
},
128+
],
129+
});
130+
testUnit = TestBed.inject(IntlLanguagePipe);
131+
132+
expect(testUnit.transform('de', {type: 'region'})).toEqual('Deutsch');
133+
});
134+
});
135+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {Inject, Optional, Pipe, PipeTransform} from '@angular/core';
2+
import {INTL_LOCALES} from "../locale";
3+
import {INTL_LANGUAGE_PIPE_DEFAULT_OPTIONS} from "./intl-language-pipe-default-options";
4+
5+
export type IntlLanguagePipeOptions = Partial<Intl.DisplayNamesOptions>;
6+
7+
@Pipe({
8+
name: 'intlLanguage',
9+
standalone: true,
10+
})
11+
export class IntlLanguagePipe implements PipeTransform {
12+
13+
constructor(@Optional() @Inject(INTL_LOCALES) readonly locale?: string | string[] | null,
14+
@Optional() @Inject(INTL_LANGUAGE_PIPE_DEFAULT_OPTIONS) readonly defaultOptions?: IntlLanguagePipeOptions | null) {
15+
}
16+
17+
transform(value: string | null | undefined, options?: IntlLanguagePipeOptions): string | null {
18+
if (!value) {
19+
return null;
20+
}
21+
22+
try {
23+
return new Intl.DisplayNames(this.locale ?? undefined, {
24+
...this.defaultOptions, ...options,
25+
type: 'language'
26+
}).of(value) ?? null;
27+
} catch (e) {
28+
console.error('Error while transforming the language', e);
29+
return value;
30+
}
31+
}
32+
33+
}

projects/angular-ecmascript-intl/src/public-api.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
* Public API Surface of angular-ecmascript-intl
33
*/
44

5-
export * from './lib/intl.module';
65
export * from './lib/date/intl-date.pipe';
6+
export * from './lib/date/intl-date-pipe-default-options';
7+
export * from './lib/intl.module';
8+
export * from './lib/language/intl-language.pipe';
9+
export * from './lib/language/intl-language-pipe-default-options';
710
export * from './lib/locale';

projects/angular-intl-demo/src/app/app.component.html

+3
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@
55
<mat-tab label="Date">
66
<app-date></app-date>
77
</mat-tab>
8+
<mat-tab label="Language">
9+
<app-language></app-language>
10+
</mat-tab>
811
</mat-tab-group>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:host ::ng-deep mat-tab-body {
2+
padding: 16px;
3+
}

projects/angular-intl-demo/src/app/app.module.ts

+17-15
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
1-
import { NgModule } from '@angular/core';
2-
import { BrowserModule } from '@angular/platform-browser';
1+
import {NgModule} from '@angular/core';
2+
import {BrowserModule} from '@angular/platform-browser';
33

4-
import { AppRoutingModule } from './app-routing.module';
5-
import { AppComponent } from './app.component';
6-
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
7-
import { MatTabsModule } from "@angular/material/tabs";
8-
import { IntlModule } from "projects/angular-ecmascript-intl/src/public-api";
9-
import { DateComponent } from './date/date.component';
10-
import { MatSelectModule } from "@angular/material/select";
11-
import { FormsModule } from "@angular/forms";
12-
import { MatSlideToggleModule } from "@angular/material/slide-toggle";
13-
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from "@angular/material/form-field";
14-
import { MarkdownModule } from "ngx-markdown";
15-
import { HttpClientModule } from "@angular/common/http";
4+
import {AppRoutingModule} from './app-routing.module';
5+
import {AppComponent} from './app.component';
6+
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
7+
import {MatTabsModule} from "@angular/material/tabs";
8+
import {IntlModule} from "projects/angular-ecmascript-intl/src/public-api";
9+
import {DateComponent} from './date/date.component';
10+
import {MatSelectModule} from "@angular/material/select";
11+
import {FormsModule} from "@angular/forms";
12+
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
13+
import {MAT_FORM_FIELD_DEFAULT_OPTIONS} from "@angular/material/form-field";
14+
import {MarkdownModule} from "ngx-markdown";
15+
import {HttpClientModule} from "@angular/common/http";
16+
import {LanguageComponent} from './language/language.component';
1617

1718
@NgModule({
1819
declarations: [
1920
AppComponent,
20-
DateComponent
21+
DateComponent,
22+
LanguageComponent,
2123
],
2224
imports: [
2325
BrowserModule,

0 commit comments

Comments
 (0)