Skip to content

feat: add country pipe #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,32 @@ their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/G

With the `INTL_LANGUAGE_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.

### Country pipe

Use the country pipe like the following:

```
{{'US' | intlCountry: options}}
```

The input can be one of the following:

* string (must be two-letter ISO 639-1 language code or a three-letter ISO 639-2 language code)
* null
* undefined

The options are the same as the options for `new Intl.DisplayNames()`. For a list of the options, see
their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames).

With the `INTL_COUNTRY_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.

## Background

For more context, see the following [GitHub issue](https://github.com/angular/angular/issues/49143)

## Feature Roadmap

* Performance: Prepare Intl.* object with default options, only construct new object when necessary
* Country pipe
* Relative time
* Migration Schematics for usages of Angular pipes

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {InjectionToken} from "@angular/core";

export const INTL_COUNTRY_PIPE_DEFAULT_OPTIONS = new InjectionToken<Partial<Intl.DisplayNamesOptions>>('IntlCountryPipeDefaultOptions');
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {IntlCountryPipe} from './intl-country.pipe';
import {TestBed} from "@angular/core/testing";
import {INTL_LOCALES} from "../locale";
import {INTL_COUNTRY_PIPE_DEFAULT_OPTIONS} from "./intl-country-pipe-default-options";

describe('IntlCountryPipe', () => {
let testUnit: IntlCountryPipe;

describe('parsing', () => {
beforeEach(() => {
testUnit = new IntlCountryPipe('en-US');
});

it('should create an instance', () => {
expect(testUnit).toBeTruthy();
});

it('should handle null values', () => {
expect(testUnit.transform(null)).toBeNull();
});

it('should handle undefined values', () => {
expect(testUnit.transform(undefined)).toBeNull();
});

it('should handle empty strings', () => {
expect(testUnit.transform('')).toBeNull();
});

it('should transform numbers', () => {
expect(testUnit.transform('US')).toEqual('United States');
});

it('should handle missing Intl.DisplayNames browser API', () => {
// @ts-expect-error Intl APIs are not expected to be undefined
spyOn(Intl, 'DisplayNames').and.returnValue(undefined);
const consoleError = spyOn(console, 'error');

expect(testUnit.transform('US')).toBeNull();
expect(consoleError).toHaveBeenCalledTimes(1);
});
});

describe('internationalization', () => {
it('should respect the set locale', () => {
TestBed.configureTestingModule({
providers: [
IntlCountryPipe,
{
provide: INTL_LOCALES,
useValue: 'de-DE',
},
],
});
testUnit = TestBed.inject(IntlCountryPipe);

expect(testUnit.transform('AT')).toEqual('Österreich');
});

it('should fall back to the browser default locale', () => {
TestBed.configureTestingModule({providers: [IntlCountryPipe]});

const result1 = TestBed.inject(IntlCountryPipe).transform('US');
const result2 = new IntlCountryPipe(navigator.language).transform('US');

expect(result1).toEqual(result2);
});
});

describe('options', () => {
it('should not override the type option', () => {
TestBed.configureTestingModule({
providers: [
IntlCountryPipe,
{
provide: INTL_LOCALES,
useValue: 'de-DE',
},
{
provide: INTL_COUNTRY_PIPE_DEFAULT_OPTIONS,
useValue: {
type: 'language',
},
},
],
});
testUnit = TestBed.inject(IntlCountryPipe);

expect(testUnit.transform('DE', {type: 'language'})).toEqual('Deutschland');
});
});

it('should respect locale option', () => {
TestBed.configureTestingModule({
providers: [
IntlCountryPipe,
{
provide: INTL_LOCALES,
useValue: 'en-US',
},
],
});
testUnit = TestBed.inject(IntlCountryPipe);

expect(testUnit.transform('US', {locale: 'de-DE'})).toEqual('Vereinigte Staaten');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {Inject, Optional, Pipe, PipeTransform} from '@angular/core';
import {INTL_LOCALES} from "../locale";
import {INTL_COUNTRY_PIPE_DEFAULT_OPTIONS} from "./intl-country-pipe-default-options";
import {IntlPipeOptions} from "../intl-pipe-options";

export type IntlCountryPipeOptions = Partial<Intl.DisplayNamesOptions> & IntlPipeOptions;

@Pipe({
name: 'intlCountry',
standalone: true,
})
export class IntlCountryPipe implements PipeTransform {

constructor(@Optional() @Inject(INTL_LOCALES) readonly locale?: string | string[] | null,
@Optional() @Inject(INTL_COUNTRY_PIPE_DEFAULT_OPTIONS) readonly defaultOptions?: Partial<Intl.DisplayNamesOptions> | null) {
}

transform(value: string | null | undefined, options?: IntlCountryPipeOptions): string | null {
if (!value) {
return null;
}

const {locale, ...intlOptions} = options ?? {};

try {
return new Intl.DisplayNames(locale ?? this.locale ?? undefined, {
...this.defaultOptions, ...intlOptions,
type: 'region',
}).of(value) ?? null;
} catch (e) {
console.error('Error while transforming the country', e);
return null;
}
}

}
3 changes: 3 additions & 0 deletions projects/angular-ecmascript-intl/src/lib/intl.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {IntlLanguagePipe} from './language/intl-language.pipe';
import {IntlDecimalPipe} from "./decimal/intl-decimal.pipe";
import {IntlPercentPipe} from "./percent/intl-percent.pipe";
import {IntlCurrencyPipe} from "./currency/intl-currency.pipe";
import {IntlCountryPipe} from "./country/intl-country.pipe";

@NgModule({
imports: [
Expand All @@ -12,13 +13,15 @@ import {IntlCurrencyPipe} from "./currency/intl-currency.pipe";
IntlDecimalPipe,
IntlPercentPipe,
IntlCurrencyPipe,
IntlCountryPipe,
],
exports: [
IntlDatePipe,
IntlLanguagePipe,
IntlDecimalPipe,
IntlPercentPipe,
IntlCurrencyPipe,
IntlCountryPipe,
],
})
export class IntlModule {
Expand Down
14 changes: 7 additions & 7 deletions projects/angular-ecmascript-intl/src/lib/utils/number-utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export const getNumericValue = (value: string | number) => {
if (typeof value === 'string') {
if (isNaN(Number(value) - parseFloat(value))) {
throw new Error(`${value} is not a number!`);
}
export const getNumericValue = (value: string | number): number => {
if (typeof value === 'number') {
return value;
}

return Number(value);
if (isNaN(Number(value) - parseFloat(value))) {
throw new Error(`${value} is not a number!`);
}

return value;
return Number(value);
}
2 changes: 2 additions & 0 deletions projects/angular-ecmascript-intl/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Public API Surface of angular-ecmascript-intl
*/

export * from './lib/country/intl-country.pipe';
export * from './lib/country/intl-country-pipe-default-options';
export * from './lib/currency/intl-currency.pipe';
export * from './lib/currency/intl-currency-pipe-default-options';
export * from './lib/date/intl-date.pipe';
Expand Down
11 changes: 11 additions & 0 deletions projects/angular-intl-demo/src/app/pipes/country/countries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const countries = [
'AT',
'CA',
'CH',
'DE',
'GB',
'KR',
'SE',
'UA',
'US',
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div class="fields-container">
<mat-form-field>
<mat-label>Country to transform</mat-label>
<mat-select [(ngModel)]="selectedCountry">
<mat-option *ngFor="let country of countries" [value]="country">{{country}}</mat-option>
</mat-select>
</mat-form-field>

<mat-form-field>
<mat-label>Locale</mat-label>
<mat-select [(ngModel)]="locale">
<mat-option [value]="undefined">Browser default</mat-option>
<mat-option *ngFor="let language of languages" [value]="language">{{language}}</mat-option>
</mat-select>
</mat-form-field>
</div>

<p>{{selectedCountry | intlCountry: {locale} }}</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.fields-container {
display: flex;
gap: 16px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 16px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {Component} from '@angular/core';
import {languages} from "../../languages";
import {countries} from "./countries";

@Component({
selector: 'app-country',
templateUrl: './country.component.html',
styleUrls: ['./country.component.scss'],
})
export class CountryComponent {
languages = languages;
countries = countries;
selectedCountry = 'DE';
locale?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {DecimalComponent} from "./decimal/decimal.component";
import {PercentComponent} from "./percent/percent.component";
import {CurrencyComponent} from "./currency/currency.component";
import {LanguageComponent} from "./language/language.component";
import {CountryComponent} from "./country/country.component";

const routes: Routes = [
{
Expand All @@ -32,6 +33,10 @@ const routes: Routes = [
path: 'language',
component: LanguageComponent,
},
{
path: 'country',
component: CountryComponent,
},
{
path: '',
redirectTo: 'date',
Expand Down
2 changes: 2 additions & 0 deletions projects/angular-intl-demo/src/app/pipes/pipes.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
routerLinkActive>Currency</a>
<a #languageActive="routerLinkActive" [active]="languageActive.isActive" mat-tab-link routerLink="language"
routerLinkActive>Language</a>
<a #countryActive="routerLinkActive" [active]="countryActive.isActive" mat-tab-link routerLink="country"
routerLinkActive>Country</a>
</nav>
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>

Expand Down
2 changes: 2 additions & 0 deletions projects/angular-intl-demo/src/app/pipes/pipes.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {MatSelectModule} from "@angular/material/select";
import {FormsModule} from "@angular/forms";
import {MatInputModule} from "@angular/material/input";
import {PipesRoutingModule} from "./pipes-routing.module";
import {CountryComponent} from "./country/country.component";

@NgModule({
declarations: [
Expand All @@ -21,6 +22,7 @@ import {PipesRoutingModule} from "./pipes-routing.module";
PercentComponent,
CurrencyComponent,
PipesComponent,
CountryComponent,
],
imports: [
CommonModule,
Expand Down