Skip to content

Commit

Permalink
feat: add angular locale module
Browse files Browse the repository at this point in the history
  • Loading branch information
draconisNoctis committed Aug 27, 2019
1 parent 90612a4 commit 00dc32e
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 32 deletions.
67 changes: 67 additions & 0 deletions src/angular/angular-locale.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import 'zone.js';
import 'zone.js/dist/zone-node';

import { Controller, Get, Inject, Next, Request, Response } from '@nestjs/common';
import express from 'express';

import { AngularController } from './angular.controller';
import { ANGULAR_LOCALE_OPTIONS, AngularLocaleOptions } from './tokens';


@Controller('/')
export class AngularLocaleController {

protected readonly router = express.Router();

constructor(@Inject(ANGULAR_LOCALE_OPTIONS) protected readonly options : AngularLocaleOptions) {
this.init();
}

protected init() {
for(const [ path, options ] of this.options.apps) {
const controller = new AngularController(this.options.mode, options, this.options.nonceFactory);

this.router.use(path, controller.handle.bind(controller));
}

this.router.get('*', (req: express.Request, res: express.Response, next: express.NextFunction) => {
const requestedLocale = req.path.replace(/^\/([a-z]{2,3}).*/, '$1').toLowerCase();
if(this.options.locales.includes(requestedLocale)) {
return next();
}
for(const locale in parseAcceptLanguageHeader(req.get('accept-language') || '')) {
if(this.options.locales.includes(locale)) {
return res.redirect(`/${locale}`);
}
}
res.redirect(`/${this.options.defaultLocale}`);
})
}

@Get('*')
handle(@Request() req: express.Request, @Response() res: express.Response, @Next() next: express.NextFunction) {
if(this.options.skip && this.options.skip(req)) {
return next();
}
this.router(req, res, next);
}
}

function parseAcceptLanguageHeader(value: string) : string[] {
const result : { locale: string, quality: number }[] = [];

for(const part of value.trim().split(/\s*,\s*/)) {
const [ _ = null, locale = null, quality = null ] = /([^;]+)(?:;q=((?:\d+\.)\d+))?/.exec(part) || [];
if(!_ || locale === '*') continue;
result.push({
locale: locale!.split(/[-_]/).shift()!.toLowerCase(),
quality: quality ? +quality : 1
})
}

return result
.sort((a, b) => b.quality - a.quality)
.map(({ locale }) => locale)
.filter((c, i, a) => a.indexOf(c) === i);
}

6 changes: 4 additions & 2 deletions src/angular/angular-root.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Controller, Inject } from '@nestjs/common';
import { ANGULAR_OPTIONS, AngularOptions } from './tokens';
import 'zone.js';
import 'zone.js/dist/zone-node';

import { Controller, Inject } from '@nestjs/common';

import { AngularController } from './angular.controller';
import { ANGULAR_OPTIONS, AngularOptions } from './tokens';


@Controller('/')
Expand Down
91 changes: 63 additions & 28 deletions src/angular/angular.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,23 @@ import { MiddlewareConsumer, ModuleMetadata } from '@nestjs/common/interfaces';
import cookieParser from 'cookie-parser';

import { AngularRootController } from './angular-root.controller';
import { ANGULAR_OPTIONS, AngularOptions } from './tokens';
import { ANGULAR_OPTIONS, AngularOptions, AngularLocaleOptions, ANGULAR_LOCALE_OPTIONS } from './tokens';
import { enableProdMode } from '@angular/core';
import { AngularLocaleController } from './angular-locale.controller';

enableProdMode();

export interface AngularOptionsFactory {
createAngularOptions() : Promise<AngularOptions> | AngularOptions;
}
export interface AngularLocaleOptionsFactory {
createAngularOptions() : Promise<AngularLocaleOptions> | AngularLocaleOptions;
}

export interface AngularAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
useExisting?: Type<AngularOptionsFactory>;
useClass?: Type<AngularOptionsFactory>;
useFactory?: (...args : any[]) => Promise<AngularOptions> | AngularOptions,
export interface AngularAsyncOptions<F = AngularOptionsFactory, T = AngularOptions> extends Pick<ModuleMetadata, 'imports'> {
useExisting?: Type<F>;
useClass?: Type<F>;
useFactory?: (...args : any[]) => Promise<T> | T,
inject?: any[];
}

Expand All @@ -42,43 +46,74 @@ export class AngularModule implements NestModule {
return {
module: AngularModule,
providers: [
...this.createAsyncProviders(options, ANGULAR_OPTIONS),
...createAsyncProviders(options, ANGULAR_OPTIONS),
],
exports: [ ANGULAR_OPTIONS ]
};
}

configure(consumer : MiddlewareConsumer) {
consumer.apply(cookieParser()).forRoutes('/');
}
}

protected static createAsyncProviders(options: AngularAsyncOptions, token : any) {
if(options.useExisting || options.useFactory) {
return [ this.createAsyncOptionsProvider(options, token) ];
@Module({
controllers: [
AngularLocaleController
]
})
export class AngularLocaleModule implements NestModule {
static forRoot(options : AngularLocaleOptions) : DynamicModule {
return {
module: AngularLocaleModule,
providers: [
{ provide: ANGULAR_LOCALE_OPTIONS, useValue: options }
],
exports: [ ANGULAR_LOCALE_OPTIONS ]
}
}

return [
this.createAsyncOptionsProvider(options, token),
{
provide: options.useClass!,
useClass: options.useClass!
}
];
static forRootAsync(options : AngularAsyncOptions<AngularLocaleOptionsFactory, AngularLocaleOptions>) : DynamicModule {
return {
module: AngularLocaleModule,
providers: [
...createAsyncProviders(options, ANGULAR_LOCALE_OPTIONS),
],
exports: [ ANGULAR_LOCALE_OPTIONS ]
};
}

configure(consumer : MiddlewareConsumer) {
consumer.apply(cookieParser()).forRoutes('/');
}
}

protected static createAsyncOptionsProvider(options : AngularAsyncOptions, token : any) : Provider {
if(options.useFactory) {
return {
provide: token,
useFactory: options.useFactory,
inject: options.inject
};
function createAsyncProviders<F, T>(options: AngularAsyncOptions<F, T>, token : any) {
if(options.useExisting || options.useFactory) {
return [ createAsyncOptionsProvider(options, token) ];
}

return [
createAsyncOptionsProvider(options, token),
{
provide: options.useClass!,
useClass: options.useClass!
}
];
}

function createAsyncOptionsProvider<F, T>(options : AngularAsyncOptions<F, T>, token : any) : Provider {
if(options.useFactory) {
return {
provide: token,
useFactory: async (factory : AngularOptionsFactory) => await factory.createAngularOptions(),
inject: [ options.useExisting || options.useClass ]
useFactory: options.useFactory,
inject: options.inject
};
}

configure(consumer : MiddlewareConsumer) {
consumer.apply(cookieParser()).forRoutes('/');
}
return {
provide: token,
useFactory: async (factory : AngularOptionsFactory) => await factory.createAngularOptions(),
inject: [ options.useExisting || options.useClass ]
};
}
1 change: 1 addition & 0 deletions src/angular/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './angular-root.controller';
export * from './angular-locale.controller';
export * from './angular.controller';
export * from './angular.module';
export * from './tokens';
15 changes: 13 additions & 2 deletions src/angular/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Request, Response } from 'express';

export const ANGULAR_OPTIONS = 'ANGULAR_OPTIONS';
export const ANGULAR_LOCALE_OPTIONS = 'ANGULAR_LOCALE_OPTIONS';

export interface IAngularAppOptions {
www: string;
Expand All @@ -9,8 +10,18 @@ export interface IAngularAppOptions {
skip?: (req : Request) => boolean;
}

export interface AngularOptions extends IAngularAppOptions {
export interface AngularBaseOptions {
mode: 'ssr' | 'proxy';
apps: [ string|RegExp, IAngularAppOptions ][];
nonceFactory?(request : Request, response : Response) : string;
}

export interface AngularOptions extends IAngularAppOptions, AngularBaseOptions {
apps: [ string|RegExp, IAngularAppOptions ][];
}

export interface AngularLocaleOptions extends AngularBaseOptions {
apps: [ string|RegExp, IAngularAppOptions ][];
skip?: (req : Request) => boolean;
locales: string[];
defaultLocale: string;
}

0 comments on commit 00dc32e

Please sign in to comment.