Skip to content

Commit

Permalink
feat(admin-ui): Currencies respect UI language setting
Browse files Browse the repository at this point in the history
Relates to #568
  • Loading branch information
michaelbromley committed Jan 13, 2021
1 parent dd0e73a commit 5530782
Show file tree
Hide file tree
Showing 34 changed files with 307 additions and 117 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@
<tbody>
<tr *ngFor="let row of variantsPreview$ | async">
<td>{{ row.name }}</td>
<td>{{ row.price / 100 | currency: currentChannel?.currencyCode }}</td>
<td>{{ row.price | localeCurrency: currentChannel?.currencyCode }}</td>
<td>
<ng-template [ngIf]="selectedChannel" [ngIfElse]="noChannelSelected">
{{ row.pricePreview / 100 | currency: selectedChannel?.currencyCode }}
{{ row.pricePreview | localeCurrency: selectedChannel?.currencyCode }}
</ng-template>
<ng-template #noChannelSelected> - </ng-template>
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
(ngModelChange)="onFormChanged(variantFormValues[variant.id])"
></vdr-currency-input>
</clr-input-container>
<span *ngIf="variantFormValues[variant.id].existing">{{ variantFormValues[variant.id].price / 100 | currency: currencyCode }}</span>
<span *ngIf="variantFormValues[variant.id].existing">{{ variantFormValues[variant.id].price | localeCurrency: currencyCode }}</span>
</td>
<td>
<clr-input-container *ngIf="!variantFormValues[variant.id].existing">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
<div *ngIf="!priceIncludesTax" class="value">
{{
'catalog.price-with-tax-in-default-zone'
| translate: { price: grossPrice$ | async | currency: currencyCode, rate: taxRate$ | async }
| translate: { price: grossPrice$ | async | localeCurrency: currencyCode, rate: taxRate$ | async }
}}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class VariantPriceDetailComponent implements OnInit, OnChanges {

this.grossPrice$ = combineLatest(this.taxRate$, this.priceChange$).pipe(
map(([taxRate, price]) => {
return Math.round(price * ((100 + taxRate) / 100)) / 100;
return Math.round(price * ((100 + taxRate) / 100));
}),
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/admin-ui/src/lib/core/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export * from './shared/dynamic-form-inputs/select-form-input/select-form-input.
export * from './shared/dynamic-form-inputs/text-form-input/text-form-input.component';
export * from './shared/pipes/asset-preview.pipe';
export * from './shared/pipes/channel-label.pipe';
export * from './shared/pipes/currency-name.pipe';
export * from './shared/pipes/locale-currency-name.pipe';
export * from './shared/pipes/custom-field-label.pipe';
export * from './shared/pipes/duration.pipe';
export * from './shared/pipes/file-size.pipe';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<vdr-affixed-input [prefix]="currencyCode | currencyName: 'symbol'">
<vdr-affixed-input
[prefix]="prefix$ | async | localeCurrencyName: 'symbol'"
[suffix]="suffix$ | async | localeCurrencyName: 'symbol'"
>
<input
type="number"
step="0.01"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Component } from '@angular/core';
import { Component, Injectable } from '@angular/core';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { Type } from '@vendure/common/lib/shared-types';
import { of } from 'rxjs';

import { CurrencyNamePipe } from '../../pipes/currency-name.pipe';
import { LanguageCode } from '../../../common/generated-types';
import { DataService } from '../../../data/providers/data.service';
import { LocaleCurrencyNamePipe } from '../../pipes/locale-currency-name.pipe';
import { AffixedInputComponent } from '../affixed-input/affixed-input.component';

import { CurrencyInputComponent } from './currency-input.component';
Expand All @@ -13,12 +16,13 @@ describe('CurrencyInputComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [FormsModule],
providers: [{ provide: DataService, useClass: MockDataService }],
declarations: [
TestControlValueAccessorComponent,
TestSimpleComponent,
CurrencyInputComponent,
AffixedInputComponent,
CurrencyNamePipe,
LocaleCurrencyNamePipe,
],
}).compileComponents();
}));
Expand Down Expand Up @@ -66,11 +70,35 @@ describe('CurrencyInputComponent', () => {
expect(fixture.componentInstance.price).toBe(157);
}));

describe('currencyCode display', () => {
it('displays currency code in correct position (prefix)', fakeAsync(() => {
MockDataService.language = LanguageCode.en;
const fixture = createAndRunChangeDetection(TestSimpleComponent, 4299, 'GBP');
const prefix = fixture.debugElement.query(By.css('.prefix'));
const suffix = fixture.debugElement.query(By.css('.suffix'));
expect(prefix.nativeElement.innerHTML).toBe('£');
expect(suffix).toBeNull();
}));

it('displays currency code in correct position (suffix)', fakeAsync(() => {
MockDataService.language = LanguageCode.fr;
const fixture = createAndRunChangeDetection(TestSimpleComponent, 4299, 'GBP');
const prefix = fixture.debugElement.query(By.css('.prefix'));
const suffix = fixture.debugElement.query(By.css('.suffix'));
expect(prefix).toBeNull();
expect(suffix.nativeElement.innerHTML).toBe('£GB');
}));
});

function createAndRunChangeDetection<T extends TestControlValueAccessorComponent | TestSimpleComponent>(
component: Type<T>,
priceValue = 123,
currencyCode: string = '',
): ComponentFixture<T> {
const fixture = TestBed.createComponent(component);
if (fixture.componentInstance instanceof TestSimpleComponent && currencyCode) {
fixture.componentInstance.currencyCode = currencyCode;
}
fixture.componentInstance.price = priceValue;
fixture.detectChanges();
tick();
Expand All @@ -96,9 +124,30 @@ class TestControlValueAccessorComponent {
@Component({
selector: 'vdr-test-component',
template: `
<vdr-currency-input [value]="price"></vdr-currency-input>
<vdr-currency-input [value]="price" [currencyCode]="currencyCode"></vdr-currency-input>
`,
})
class TestSimpleComponent {
currencyCode = '';
price = 123;
}

@Injectable()
class MockDataService {
static language: LanguageCode = LanguageCode.en;
client = {
uiState() {
return {
mapStream(mapFn: any) {
return of(
mapFn({
uiState: {
language: MockDataService.language,
},
}),
);
},
};
},
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import {
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { DataService } from '../../../data/providers/data.service';

/**
* A form input control which displays currency in decimal format, whilst working
Expand All @@ -17,16 +30,40 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
},
],
})
export class CurrencyInputComponent implements ControlValueAccessor, OnChanges {
export class CurrencyInputComponent implements ControlValueAccessor, OnInit, OnChanges {
@Input() disabled = false;
@Input() readonly = false;
@Input() value: number;
@Input() currencyCode = '';
@Output() valueChange = new EventEmitter();
prefix$: Observable<string>;
suffix$: Observable<string>;
onChange: (val: any) => void;
onTouch: () => void;
_decimalValue: string;

constructor(private dataService: DataService, private changeDetectorRef: ChangeDetectorRef) {}

ngOnInit() {
const languageCode$ = this.dataService.client.uiState().mapStream(data => data.uiState.language);
const shouldPrefix$ = languageCode$.pipe(
map(languageCode => {
if (!this.currencyCode) {
return '';
}
const locale = languageCode.replace(/_/g, '-');
const localised = new Intl.NumberFormat(locale, {
style: 'currency',
currency: this.currencyCode,
currencyDisplay: 'symbol',
}).format(undefined as any);
return localised.indexOf('NaN') > 0;
}),
);
this.prefix$ = shouldPrefix$.pipe(map(shouldPrefix => (shouldPrefix ? this.currencyCode : '')));
this.suffix$ = shouldPrefix$.pipe(map(shouldPrefix => (shouldPrefix ? '' : this.currencyCode)));
}

ngOnChanges(changes: SimpleChanges) {
if ('value' in changes) {
this.writeValue(changes['value'].currentValue);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CurrencyNamePipe } from './currency-name.pipe';
import { LocaleCurrencyNamePipe } from './locale-currency-name.pipe';

describe('CurrencyNamePipe', () => {
const pipe = new CurrencyNamePipe();
describe('LocaleCurrencyNamePipe', () => {
const pipe = new LocaleCurrencyNamePipe();
it('full output', () => {
expect(pipe.transform('usd')).toBe('US dollars ($)');
expect(pipe.transform('gbp')).toBe('British pounds (£)');
Expand All @@ -20,6 +20,11 @@ describe('CurrencyNamePipe', () => {
expect(pipe.transform('CNY', 'symbol')).toBe('CN¥');
});

it('uses locale', () => {
expect(pipe.transform('usd', 'symbol', 'fr')).toBe('$US');
expect(pipe.transform('usd', 'name', 'de')).toBe('US-Dollar');
});

it('returns code for unknown codes', () => {
expect(pipe.transform('zzz')).toBe('ZZZ (ZZZ)');
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ChangeDetectorRef, OnDestroy, Optional, Pipe, PipeTransform } from '@angular/core';
import { Subscription } from 'rxjs';

import { DataService } from '../../data/providers/data.service';

/**
* Displays a human-readable name for a given ISO 4217 currency code.
*/
@Pipe({
name: 'localeCurrencyName',
pure: false,
})
export class LocaleCurrencyNamePipe implements PipeTransform, OnDestroy {
private locale: string;
private readonly subscription: Subscription;

constructor(
@Optional() private dataService?: DataService,
@Optional() changeDetectorRef?: ChangeDetectorRef,
) {
if (this.dataService && changeDetectorRef) {
this.subscription = this.dataService.client
.uiState()
.mapStream(data => data.uiState.language)
.subscribe(languageCode => {
this.locale = languageCode.replace(/_/g, '-');
changeDetectorRef.markForCheck();
});
}
}

ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}

transform(value: any, display: 'full' | 'symbol' | 'name' = 'full', locale?: unknown): any {
if (value == null || value === '') {
return '';
}
if (typeof value !== 'string') {
return `Invalid currencyCode "${value as any}"`;
}
let name = '';
let symbol = '';
const activeLocale = typeof locale === 'string' ? locale : this.locale ?? 'en';

if (display === 'full' || display === 'name') {
name = new Intl.NumberFormat(activeLocale, {
style: 'currency',
currency: value,
currencyDisplay: 'name',
})
.format(undefined as any)
.replace(/\s*NaN\s*/, '');
}
if (display === 'full' || display === 'symbol') {
symbol = new Intl.NumberFormat(activeLocale, {
style: 'currency',
currency: value,
currencyDisplay: 'symbol',
})
.format(undefined as any)
.replace(/\s*NaN\s*/, '');
}
return display === 'full' ? `${name} (${symbol})` : display === 'name' ? name : symbol;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { CurrencyCode, LanguageCode } from '../../common/generated-types';

import { LocaleCurrencyPipe } from './locale-currency.pipe';

describe('LocaleCurrencyPipe', () => {
it('GBP in English', () => {
const pipe = new LocaleCurrencyPipe();
expect(pipe.transform(1, CurrencyCode.GBP, LanguageCode.en)).toBe('£0.01');
expect(pipe.transform(123, CurrencyCode.GBP, LanguageCode.en)).toBe('£1.23');
expect(pipe.transform(4200000, CurrencyCode.GBP, LanguageCode.en)).toBe('£42,000.00');
});

it('EUR in German', () => {
const pipe = new LocaleCurrencyPipe();
expect(pipe.transform(1, CurrencyCode.EUR, LanguageCode.de)).toBe('0,01 €');
expect(pipe.transform(123, CurrencyCode.EUR, LanguageCode.de)).toBe('1,23 €');
expect(pipe.transform(4200000, CurrencyCode.EUR, LanguageCode.de)).toBe('42.000,00 €');
});
});
Loading

0 comments on commit 5530782

Please sign in to comment.