Merge pull request #6052 from abpframework/refactor/locale

Moved the registerLocale to core/locale secondary endpoint
pull/6096/head
Bunyamin Coskuner 5 years ago committed by GitHub
commit e2e46967e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -193,44 +193,122 @@ import { Component } from '@angular/core';
export class AppComponent {}
```
## Mapping of Culture Name to Angular Locale File Name
## Registering a New Locale
Since ABP has more than one language, Angular locale files loads lazily using [Webpack's import function](https://webpack.js.org/api/module-methods/#import-1) to avoid increasing the bundle size and register to Angular core using the [`registerLocaleData`](https://angular.io/api/common/registerLocaleData) function. The chunks to be included in the bundle are specified by the [Webpack's magic comments](https://webpack.js.org/api/module-methods/#magic-comments) as hard-coded. Therefore a `registerLocale` function that returns Webpack `import` function must be passed to `CoreModule`.
### registerLocaleFn
A `registerLocaleFn` is a higher order function that accepts `cultureNameLocaleFileMap` object and `errorHandlerFn` function as params and returns Webpack import function. A `registerLocale` function must be passed to the `forRoot` of the `CoreModule` as shown below:
```js
// app.module.ts
import { registerLocale } from '@abp/ng.core/locale';
// if you have commercial license and the language management module, add the below import
// import { registerLocale } from '@volo/abp.ng.language-management/locale';
@NgModule({
imports: [
// ...
CoreModule.forRoot({
// ...other options,
registerLocaleFn: registerLocale(
// you can pass the cultureNameLocaleFileMap and errorHandlerFn as optionally
{
cultureNameLocaleFileMap: { 'pt-BR': 'pt' },
errorHandlerFn: ({ resolve, reject, locale, error }) => {
// the error can be handled here
},
},
)
}),
//...
]
```
### Mapping of Culture Name to Angular Locale File Name
Some of the culture names defined in .NET do not match Angular locales. In such cases, the Angular app throws an error like below at runtime:
![locale-error](./images/locale-error.png)
If you see an error like this, you should pass the `cultureNameLocaleFileMap` property like below to CoreModule's forRoot static method.
If you see an error like this, you should pass the `cultureNameLocaleFileMap` property like below to the `registerLocale` function.
```js
// app.module.ts
import { registerLocale } from '@abp/ng.core/locale';
// if you have commercial license and the language management module, add the below import
// import { registerLocale } from '@volo/abp.ng.language-management/locale';
@NgModule({
imports: [
// other imports
CoreModule.forRoot({
// other options
cultureNameLocaleFileMap: {
"DotnetCultureName": "AngularLocaleFileName",
"pt-BR": "pt" // example
}
})
// ...
CoreModule.forRoot({
// ...other options,
registerLocaleFn: registerLocale(
{
cultureNameLocaleFileMap: {
"DotnetCultureName": "AngularLocaleFileName",
"pt-BR": "pt" // example
},
},
)
}),
//...
```
See [all locale files in Angular](https://github.com/angular/angular/tree/master/packages/common/locales).
## Adding new culture
### Adding a New Culture
Add the below code to the `app.module.ts` by replacing `your-locale` placeholder with a correct locale name.
```js
//app.module.ts
import { storeLocaleData } from '@abp/ng.core';
import { storeLocaleData } from '@abp/ng.core/locale';
import(
/* webpackChunkName: "_locale-your-locale-js"*/
/* webpackMode: "eager" */
'@angular/common/locales/your-locale.js'
).then(m => storeLocaleData(m.default, 'your-locale'));
```
...or a custom `registerLocale` function can be passed to the `CoreModule`:
```js
// register-locale.ts
import { differentLocales } from '@abp/ng.core';
export function registerLocale(locale: string) {
return import(
/* webpackChunkName: "_locale-[request]"*/
/* webpackInclude: /[/\\](en|fr).js/ */
/* webpackExclude: /[/\\]global|extra/ */
`@angular/common/locales/${differentLocales[locale] || locale}.js`
)
}
// app.module.ts
import { registerLocale } from './register-locale';
@NgModule({
imports: [
// ...
CoreModule.forRoot({
// ...other options,
registerLocaleFn: registerLocale
}),
//...
]
```
## See Also
* [Localization in ASP.NET Core](../../Localization.md)

@ -1,5 +1,6 @@
import { AccountConfigModule } from '@abp/ng.account/config';
import { CoreModule } from '@abp/ng.core';
import { registerLocale } from '@abp/ng.core/locale';
import { IdentityConfigModule } from '@abp/ng.identity/config';
import { SettingManagementConfigModule } from '@abp/ng.setting-management/config';
import { TenantManagementConfigModule } from '@abp/ng.tenant-management/config';
@ -28,6 +29,7 @@ const INSPECTION_TOOLS = [
AppRoutingModule,
CoreModule.forRoot({
environment,
registerLocaleFn: registerLocale(),
sendNullsAsQueryParam: false,
skipGetAppConfiguration: false,
}),

@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/core/locale",
"lib": {
"entryFile": "src/public-api.ts"
}
}

@ -0,0 +1 @@
export * from './utils/register-locale';

@ -0,0 +1,66 @@
import { differentLocales } from '@abp/ng.core';
import { registerLocaleData } from '@angular/common';
import { isDevMode } from '@angular/core';
export interface LocaleErrorHandlerData {
resolve: any;
reject: any;
error: any;
locale: string;
}
let localeMap = {};
export interface RegisterLocaleData {
cultureNameLocaleFileMap?: Record<string, string>;
errorHandlerFn?: (data: LocaleErrorHandlerData) => any;
}
export function registerLocale(
{
cultureNameLocaleFileMap = {},
errorHandlerFn = defaultLocalErrorHandlerFn,
} = {} as RegisterLocaleData,
) {
return (locale: string): Promise<any> => {
localeMap = { ...differentLocales, ...cultureNameLocaleFileMap };
return new Promise((resolve, reject) => {
return import(
/* webpackChunkName: "_locale-[request]"*/
/* webpackInclude: /[/\\](ar|cs|en|fr|pt|tr|ru|hu|sl|zh-Hans|zh-Hant).js/ */
/* webpackExclude: /[/\\]global|extra/ */
`@angular/common/locales/${localeMap[locale] || locale}.js`
)
.then(resolve)
.catch(error => {
errorHandlerFn({
resolve,
reject,
error,
locale,
});
});
});
};
}
const extraLocales = {};
export function storeLocaleData(data: any, localeId: string) {
extraLocales[localeId] = data;
}
export async function defaultLocalErrorHandlerFn({ locale, resolve }: LocaleErrorHandlerData) {
if (extraLocales[locale]) {
resolve({ default: extraLocales[localeMap[locale] || locale] });
return;
}
if (isDevMode) {
console.error(
`Cannot find the ${locale} locale file. You can check how can add new culture at https://docs.abp.io/en/abp/latest/UI/Angular/Localization#adding-a-new-culture`,
);
}
resolve();
}

@ -1,7 +1,7 @@
// Different locales from .NET
// Key is .NET locale, value is Angular locale
export default {
export const differentLocales = {
aa: 'en',
'aa-DJ': 'en',
'aa-ER': 'en',

@ -1,12 +0,0 @@
export const includedLocales = [
'tr',
'ar',
'cs',
'en',
'fr',
'pt',
'ru',
'sl',
'zh-Hans',
'zh-Hant',
];

@ -1,2 +1 @@
export * from './different-locales';
export * from './included-locales';

@ -1,16 +1,16 @@
import { EventEmitter, Type } from '@angular/core';
import { Router } from '@angular/router';
import { NgxsStoragePluginOptions } from '@ngxs/storage-plugin';
import { Subject } from 'rxjs';
import { eLayoutType } from '../enums/common';
import { Config } from './config';
import { NgxsStoragePluginOptions } from '@ngxs/storage-plugin';
export namespace ABP {
export interface Root {
environment: Partial<Config.Environment>;
registerLocaleFn: (locale: string) => Promise<any>;
skipGetAppConfiguration?: boolean;
sendNullsAsQueryParam?: boolean;
cultureNameLocaleFileMap?: Dictionary<string>;
ngxsStoragePluginOptions?: NgxsStoragePluginOptions & { key?: string[] };
}

@ -1,5 +1,5 @@
import { LOCALE_ID, Provider } from '@angular/core';
import localesMapping from '../constants/different-locales';
import { differentLocales } from '../constants/different-locales';
import { LocalizationService } from '../services/localization.service';
export class LocaleId extends String {
@ -9,7 +9,7 @@ export class LocaleId extends String {
toString(): string {
const { currentLang } = this.localizationService;
return localesMapping[currentLang] || currentLang;
return differentLocales[currentLang] || currentLang;
}
valueOf(): string {

@ -1,14 +1,15 @@
import { registerLocaleData } from '@angular/common';
import { Injectable, Injector, NgZone, Optional, SkipSelf } from '@angular/core';
import { ActivatedRouteSnapshot, Router } from '@angular/router';
import { Actions, ofActionSuccessful, Store } from '@ngxs/store';
import { noop, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { SetLanguage } from '../actions/session.actions';
import { ABP } from '../models/common';
import { Config } from '../models/config';
import { ConfigState } from '../states/config.state';
import { registerLocale } from '../utils/initial-utils';
import { createLocalizer, createLocalizerWithFallback } from '../utils/localization-utils';
import { CORE_OPTIONS } from '../tokens/options.token';
import { createLocalizer, createLocalizerWithFallback } from '../utils/localization-utils';
type ShouldReuseRoute = (future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot) => boolean;
@ -52,7 +53,11 @@ export class LocalizationService {
router.routeReuseStrategy.shouldReuseRoute = () => false;
router.navigated = false;
return registerLocale(locale, this.injector).then(() => {
const { registerLocaleFn }: ABP.Root = this.injector.get(CORE_OPTIONS);
return registerLocaleFn(locale).then(module => {
if (module?.default) registerLocaleData(module.default);
this.ngZone.run(async () => {
await router.navigateByUrl(router.url).catch(noop);
router.routeReuseStrategy.shouldReuseRoute = shouldReuseRoute;

@ -1,9 +1,8 @@
import { Injectable, NgZone } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ReplaceableComponents } from '../models/replaceable-components';
import { BehaviorSubject, Observable } from 'rxjs';
import { noop } from '../utils/common-utils';
import { map, filter } from 'rxjs/operators';
import { InternalStore } from '../utils/internal-store-utils';
import { reloadRoute } from '../utils/route-utils';

@ -226,17 +226,15 @@ export class ConfigState {
}),
),
switchMap(configuration => {
let lang = configuration.localization.currentCulture.cultureName;
if (this.store.selectSnapshot(SessionState.getLanguage)) return of(null);
let lang = configuration.localization.currentCulture.cultureName;
if (lang.includes(';')) {
lang = lang.split(';')[0];
}
document.documentElement.setAttribute('lang', lang);
return this.store.selectSnapshot(SessionState.getLanguage)
? of(null)
: dispatch(new SetLanguage(lang, false));
return dispatch(new SetLanguage(lang, false));
}),
catchError((err: HttpErrorResponse) => {
dispatch(new RestOccurError(err));

@ -6,94 +6,6 @@ import { Config } from '../models/config';
import { ConfigStateService } from '../services/config-state.service';
import { ConfigState } from '../states';
const CONFIG_STATE_DATA = {
environment: {
production: false,
application: {
name: 'MyProjectName',
},
oAuthConfig: {
issuer: 'https://localhost:44305',
},
apis: {
default: {
url: 'https://localhost:44305',
},
other: {
url: 'https://localhost:44306',
},
},
localization: {
defaultResourceName: 'MyProjectName',
},
},
requirements: {
layouts: [null, null, null],
},
localization: {
values: {
MyProjectName: {
"'{0}' and '{1}' do not match.": "'{0}' and '{1}' do not match.",
},
AbpIdentity: {
Identity: 'identity',
},
},
languages: [
{
cultureName: 'cs',
uiCultureName: 'cs',
displayName: 'Čeština',
flagIcon: null,
},
],
currentCulture: {
displayName: 'English',
englishName: 'English',
threeLetterIsoLanguageName: 'eng',
twoLetterIsoLanguageName: 'en',
isRightToLeft: false,
cultureName: 'en',
name: 'en',
nativeName: 'English',
dateTimeFormat: {
calendarAlgorithmType: 'SolarCalendar',
dateTimeFormatLong: 'dddd, MMMM d, yyyy',
shortDatePattern: 'M/d/yyyy',
fullDateTimePattern: 'dddd, MMMM d, yyyy h:mm:ss tt',
dateSeparator: '/',
shortTimePattern: 'h:mm tt',
longTimePattern: 'h:mm:ss tt',
},
},
defaultResourceName: null,
},
auth: {
policies: {
'AbpIdentity.Roles': true,
},
grantedPolicies: {
'Abp.Identity': false,
},
},
setting: {
values: {
'Abp.Localization.DefaultLanguage': 'en',
},
},
currentUser: {
isAuthenticated: false,
id: null,
tenantId: null,
userName: null,
email: null,
roles: [],
} as ApplicationConfiguration.CurrentUser,
features: {
values: {},
},
} as Config.State;
describe('ConfigStateService', () => {
let service: ConfigStateService;
let spectator: SpectatorService<ConfigStateService>;

@ -6,6 +6,7 @@ const options: ABP.Root = {
environment: {
production: false,
},
registerLocaleFn: () => Promise.resolve(),
};
const event = new InitState();

@ -8,7 +8,7 @@ import { Config } from '../models/config';
import { ApplicationConfigurationService, ConfigStateService } from '../services';
import { ConfigState } from '../states';
export const CONFIG_STATE_DATA = {
export const CONFIG_STATE_DATA = ({
environment: {
production: false,
application: {
@ -98,7 +98,8 @@ export const CONFIG_STATE_DATA = {
'Chat.Enable': 'True',
},
},
} as Config.State;
registerLocaleFn: () => Promise.resolve(),
} as any) as Config.State;
describe('ConfigState', () => {
let spectator: SpectatorService<ConfigStateService>;

@ -22,7 +22,13 @@ describe('InitialUtils', () => {
component: DummyComponent,
mocks: [Store, OAuthService],
providers: [
{ provide: CORE_OPTIONS, useValue: { environment: { oAuthConfig: { issuer: 'test' } } } },
{
provide: CORE_OPTIONS,
useValue: {
environment: { oAuthConfig: { issuer: 'test' } },
registerLocaleFn: () => Promise.resolve(),
},
},
],
});
@ -76,7 +82,7 @@ describe('InitialUtils', () => {
const store = spectator.inject(Store);
store.selectSnapshot.andCallFake(selector => selector({ SessionState: { language: 'tr' } }));
injectorSpy.mockReturnValueOnce(store);
injectorSpy.mockReturnValueOnce({ cultureNameLocaleFileMap: {} });
injectorSpy.mockReturnValueOnce({ registerLocaleFn: () => Promise.resolve() });
expect(typeof localeInitializer(injector)).toBe('function');
expect(await localeInitializer(injector)()).toBe('resolved');
});

@ -1,8 +1,8 @@
import { Component, LOCALE_ID } from '@angular/core';
import { createRoutingFactory, SpectatorHost, SpectatorRouting } from '@ngneat/spectator/jest';
import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest';
import { differentLocales } from '../constants/different-locales';
import { LocaleId } from '../providers';
import { LocalizationService } from '../services';
import { LocaleProvider, LocaleId } from '../providers';
import localesMapping from '../constants/different-locales';
@Component({ selector: 'abp-dummy', template: '' })
export class DummyComponent {}
@ -28,10 +28,10 @@ describe('LocaleProvider', () => {
spectator = createComponent();
const localizationService = spectator.inject(LocalizationService);
expect(spectator.inject(LOCALE_ID).valueOf()).toBe(localesMapping['en-US'] || 'en-US');
expect(spectator.inject(LOCALE_ID).valueOf()).toBe(differentLocales['en-US'] || 'en-US');
(localizationService as any).currentLang = 'tr';
expect(spectator.inject(LOCALE_ID).valueOf()).toBe(localesMapping['tr'] || 'tr');
expect(spectator.inject(LOCALE_ID).valueOf()).toBe(differentLocales['tr'] || 'tr');
});
});
});

@ -16,7 +16,10 @@ describe('LocalizationService', () => {
mocks: [Store, Router],
providers: [
{ provide: Actions, useValue: new Subject() },
{ provide: CORE_OPTIONS, useValue: { cultureNameLocaleFileMap: {} } },
{
provide: CORE_OPTIONS,
useValue: { registerLocaleFn: () => Promise.resolve(), cultureNameLocaleFileMap: {} },
},
],
});

@ -1,3 +1,2 @@
export * from './list.token';
export * from './locale-error-handler.token';
export * from './options.token';

@ -1,14 +0,0 @@
import { InjectionToken, Injector } from '@angular/core';
export interface LocaleErrorHandlerData {
resolve: any;
reject: any;
error: any;
locale: string;
storedLocales: Record<string, any>;
injector: Injector;
}
export const LOCALE_ERROR_HANDLER = new InjectionToken<(data: LocaleErrorHandlerData) => any>(
'LOCALE_ERROR_HANDLER',
);

@ -1,12 +1,10 @@
import { InjectionToken } from '@angular/core';
import differentLocales from '../constants/different-locales';
import { ABP } from '../models/common';
export const CORE_OPTIONS = new InjectionToken<ABP.Root>('CORE_OPTIONS');
export function coreOptionsFactory({ cultureNameLocaleFileMap = {}, ...options }: ABP.Root) {
export function coreOptionsFactory({ ...options }: ABP.Root) {
return {
...options,
cultureNameLocaleFileMap: { ...differentLocales, ...cultureNameLocaleFileMap },
} as ABP.Root;
}

@ -1,5 +1,5 @@
import { registerLocaleData } from '@angular/common';
import { Injector, isDevMode } from '@angular/core';
import { Injector } from '@angular/core';
import { Store } from '@ngxs/store';
import { OAuthService } from 'angular-oauth2-oidc';
import { tap } from 'rxjs/operators';
@ -8,7 +8,6 @@ import { ABP } from '../models/common';
import { AuthService } from '../services/auth.service';
import { ConfigState } from '../states/config.state';
import { clearOAuthStorage } from '../strategies/auth-flow.strategy';
import { LocaleErrorHandlerData, LOCALE_ERROR_HANDLER } from '../tokens/locale-error-handler.token';
import { CORE_OPTIONS } from '../tokens/options.token';
import { getRemoteEnv } from './environment-utils';
import { parseTenantFromUrl } from './multi-tenancy-utils';
@ -43,68 +42,18 @@ export function checkAccessToken(store: Store, injector: Injector) {
export function localeInitializer(injector: Injector) {
const fn = () => {
const store: Store = injector.get(Store);
const { registerLocaleFn }: ABP.Root = injector.get(CORE_OPTIONS);
const lang = store.selectSnapshot(state => state.SessionState.language) || 'en';
return new Promise((resolve, reject) => {
registerLocale(lang, injector).then(() => resolve('resolved'), reject);
registerLocaleFn(lang).then(module => {
if (module?.default) registerLocaleData(module.default);
return resolve('resolved');
}, reject);
});
};
return fn;
}
export function registerLocale(locale: string, injector: Injector): Promise<any> {
const { cultureNameLocaleFileMap } = injector.get(CORE_OPTIONS, {} as ABP.Root);
const errorHandlerFn = injector.get(LOCALE_ERROR_HANDLER, defaultLocalErrorHandlerFn);
return new Promise((resolve, reject) => {
return import(
/* webpackChunkName: "_locale-[request]"*/
/* webpackInclude: /[/\\](ar|cs|en|fr|pt|tr|ru|hu|sl|zh-Hans|zh-Hant).js/ */
/* webpackExclude: /[/\\]global|extra/ */
`@angular/common/locales/${cultureNameLocaleFileMap[locale] || locale}.js`
)
.then(module => {
registerLocaleData(module.default, locale);
resolve(module.default);
})
.catch(error => {
errorHandlerFn({
resolve,
reject,
error,
injector,
locale,
storedLocales: { ...extraLocales },
});
});
});
}
const extraLocales = {};
export function storeLocaleData(data: any, localeId: string) {
extraLocales[localeId] = data;
}
async function defaultLocalErrorHandlerFn({
locale,
storedLocales,
resolve,
injector,
}: LocaleErrorHandlerData) {
if (storedLocales[locale]) {
registerLocaleData(storedLocales[locale], locale);
resolve();
return;
}
if (isDevMode) {
console.error(
`Cannot find the ${locale} locale file. You can check how can add new culture at https://docs.abp.io/en/abp/latest/UI/Angular/Localization#adding-new-culture`,
);
}
resolve();
}

@ -16,6 +16,7 @@
"types": ["jest"],
"paths": {
"@abp/ng.core": ["packages/core/src/public-api.ts"],
"@abp/ng.core/locale": ["packages/core/locale/src/public-api.ts"],
"@abp/ng.theme.shared": ["packages/theme-shared/src/public-api.ts"],
"@abp/ng.theme.shared/extensions": ["packages/theme-shared/extensions/src/public-api.ts"],
"@abp/ng.components/tree": ["packages/components/tree/src/public-api.ts"],

File diff suppressed because it is too large Load Diff

@ -1,5 +1,6 @@
import { AccountConfigModule } from '@abp/ng.account/config';
import { CoreModule } from '@abp/ng.core';
import { registerLocale } from '@abp/ng.core/locale';
import { IdentityConfigModule } from '@abp/ng.identity/config';
import { SettingManagementConfigModule } from '@abp/ng.setting-management/config';
import { TenantManagementConfigModule } from '@abp/ng.tenant-management/config';
@ -21,6 +22,7 @@ import { APP_ROUTE_PROVIDER } from './route.provider';
AppRoutingModule,
CoreModule.forRoot({
environment,
registerLocaleFn: registerLocale(),
}),
ThemeSharedModule.forRoot(),
AccountConfigModule.forRoot(),

@ -1,5 +1,6 @@
import { AccountConfigModule } from '@abp/ng.account/config';
import { CoreModule } from '@abp/ng.core';
import { registerLocale } from '@abp/ng.core/locale';
import { IdentityConfigModule } from '@abp/ng.identity/config';
import { SettingManagementConfigModule } from '@abp/ng.setting-management/config';
import { TenantManagementConfigModule } from '@abp/ng.tenant-management/config';
@ -22,6 +23,7 @@ import { ThemeBasicModule } from '@abp/ng.theme.basic';
AppRoutingModule,
CoreModule.forRoot({
environment,
registerLocaleFn: registerLocale(),
sendNullsAsQueryParam: false,
skipGetAppConfiguration: false,
}),

Loading…
Cancel
Save