nestjs-i18n Translation handlebars templates not working - node.js

Handlebars template not translating by nestjs-i18n
app.module.ts
#Global()
#Module({
imports: [
I18nModule.forRoot({
fallbackLanguage: 'en',
loaderOptions: {
path: path.join(__dirname, '/i18n/'),
watch: true,
},
resolvers: [
{ use: HeaderResolver, options: ['lang'] },
AcceptLanguageResolver,
],
}),
],
})
export class AppModule {}
mail.module.ts
doc
#Module({
imports: [
ConfigModule.forRoot(),
MailerModule.forRootAsync({
inject: [I18nService],
useFactory: (i18n: I18nService) => ({
transport: {
host: process.env.MAILER_HOST,
port: +process.env.MAILER_PORT,
ignoreTLS: true,
secure: true,
auth: {
user: process.env.MAILER_USER,
pass: process.env.MAILER_PASS,
},
},
defaults: {
from: '"No Reply" <no-reply#localhost>',
},
preview: true,
template: {
dir: path.join(__dirname, '../resources/mail/templates/'),
adapter: new HandlebarsAdapter({ t: i18n.hbsHelper }),
options: {
strict: true,
},
},
}),
}),
],
providers: [MailService],
exports: [MailService],
})
export class MailModule {}
src/i18n/fr/common.json
"HELLO": "Bonjour",
src/i18n/en/common.json
"HELLO": "Hello",
src/resources/mail/templates/test.hbs
<!doctype html>
<html>
<body>
<h1>{{ t 'common.HELLO' }}</h1>
</body>
</html>
call api endpoint with curl
curl -X POST http://localhost:8009/api/message -H "lang: fr"
in email preview i see
<!doctype html>
<html>
<body>
<h1>Hello</h1>
</body>
</html>
instead of Bonjour
Translations in another places (f.e. validation) working ok
What I'm doing wrong?

Same issue. As I can see in src/services/i18n.service.ts, property i18nLang is required in option.data.root. I think it means that we should provide property i18nLang in object, which we pass to template. In my case, I get lang value from I18nContext from controller.
F.E. I pass object in tis form
context: {
user,
i18nLang,
},

According to my experience I18nService doesnt have access to data from Resolvers. Have you tried with I18nContext or getI18nContextFromRequest? When I used them Resolvers were able to set the language.
I did another check and saw that Stas Pyatnicyn's solution works definitely.
I think he mentions this line in the hbsHelper function
const lang = options.lookupProperty(options.data.root, 'i18nLang');
But I couldnt follow up what exactly lookupProperty() does. If he can explain it, it would be very much appreciated.
Its really weird that there is no documentation about this and thanks him a lot for finding out the solution.

Related

How to wrap Vite build in IIFE and still have all the dependencies bundled into a single file?

I'm building a chrome extension using Vite as my build tool. The main problem is during minification and mangling there are a lot of global variables created. After injecting my script to the page they conflict with already defined variables on window object.
I imagine the perfect solution would be to have my entire script wrapped in IIFE. I tried using esbuild.format = 'iife'. The resulting build is in fact wrapped in IIFE, however all the imports are not inlined. Instead resulting script is like 15 lines long with a bunch of require statements, which obviously does not work in the browser.
This is my config file:
export default defineConfig({
plugins: [
vue(),
],
esbuild: {
format: 'iife',
},
build: {
emptyOutDir: false,
rollupOptions: {
input: resolve(__dirname, './src/web/index.ts'),
output: {
dir: resolve(__dirname, './dist'),
entryFileNames: 'web.js',
assetFileNames: 'style.css',
},
},
},
resolve: {
alias: {
'#': resolve(__dirname, './src'),
},
},
});
I'm currently using this hack so to say to wrap my build in IIFE (for this I removed the esbuild.format option).
Hey I am doing the exact same thing! And I also noticed the unminified variables and functions can clash with random code in a webpage.
From what I researched myself on this topic, you shouldn't change esbuild build options with Vite as that will prevent Rollup from transforming the output. Instead, you should use format: 'iife' in the rollupOptions of your vite.config. However, in my case (and yours I believe), I have to output multiple bundles since the extension code can't share modules amongst each other. Which will crash when you set the format to 'iife' due to:
Invalid value for option "output.inlineDynamicImports" - multiple inputs are not supported when "output.inlineDynamicImports" is true.
The only solution in my case seems to be to either use multiple vite.configs (I already have two) for each of my bundle with single input entry point and format as 'iife'. Or, as you did, just write the self-invoking function yourself with some hacky script. Seems though there aren't any perfect solutions as of now.
EDIT: Okay, got it working. This is my vite.config.ts (the project):
import { defineConfig } from 'vite'
import { svelte } from '#sveltejs/vite-plugin-svelte'
import tsconfigPaths from 'vite-tsconfig-paths'
import path from 'path'
/** #type {import('vite').UserConfig} */
export default defineConfig({
plugins: [svelte({}), tsconfigPaths()],
build: {
minify: false,
rollupOptions: {
output: {
chunkFileNames: '[name].js',
entryFileNames: '[name].js'
},
input: {
inject: path.resolve('./src/inject.ts'),
proxy: path.resolve('./src/proxy.ts'),
'pop-up': path.resolve('./pop-up.html')
},
plugins: [
{
name: 'wrap-in-iife',
generateBundle(outputOptions, bundle) {
Object.keys(bundle).forEach((fileName) => {
const file = bundle[fileName]
if (fileName.slice(-3) === '.js' && 'code' in file) {
file.code = `(() => {\n${file.code}})()`
}
})
}
}
]
}
}
})
Okay, I made it working with this config:
export default defineConfig({
plugins: [
vue(),
],
build: {
emptyOutDir: false,
rollupOptions: {
input: resolve(__dirname, './src/web/index.ts'),
output: {
format: 'iife',
dir: resolve(__dirname, './dist'),
entryFileNames: 'web.js',
assetFileNames: 'style.css',
},
},
},
resolve: {
alias: {
'#': resolve(__dirname, './src'),
},
},
});
They key part is format: 'iife' inside build.rollupOptions.output.

Use handlebars template in nest js application

I want to use handle bars template in my nest js application:
<!--confirmation.hbs-->
<p>Hello template</p>
This file is located in src/mail/templates/confirmation.hbs. Also i try to send this template as email:
//mail service
#Injectable()
export class EmailService {
constructor(private readonly mailerService: MailerService) {}
public example(): void {
this.mailerService
.sendMail({
to: 'mail', // list of receivers
from: 'test#nestjs.com', // sender address
subject: 'Testing Nest MailerModule ✔', // Subject line
template: './confirmation',
})
.then((r) => {
console.log(r, 'email is sent');
})
.catch((e) => {
console.log(e, 'error sending email');
});
}
}
My app.module.ts looks:
#Module({
imports: [
MailerModule.forRoot({
transport: {
service: 'Gmail',
auth: {
user: '---secret',
pass: '---secret',
},
},
defaults: {
from: '"No Reply" <no-reply#localhost>',
},
template: {
dir: __dirname + '/templates',
adapter: new HandlebarsAdapter(),
options: {
strict: true,
},
},
}),
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5433,
username: '-',
password: '-',
database: '-',
entities: [RegisterEntity],
synchronize: true,
}),
AuthenticationModule,
],
controllers: [AppController, AuthenticationController],
providers: [AppService, AuthenticationService],
})
This is my main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix(CONSTANTS.GLOBAL_PREFIX);
await app.listen(3000);
}
bootstrap();
This is my nest-cli.hbs
{
"collection": "#nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"assets": [
"mail/templates/**/*.hbs"
],
"watchAssets": true
}
}
The email is sent if don't send a template, so the code is working. Trying to send an email template like is my code above i get this error: TypeError: Cannot destructure property 'templateName' of 'precompile(...)' as it is undefined. Question: Why i get this issue and how to get rid of it?
Your files are located inside src/mail/templates/.
But in your module you have dir: __dirname + '/templates',.
Here __dirname returns app.module.ts folder location path which is src/.
change
dir: __dirname + '/templates',
to
dir: __dirname + '/mail/templates',
Since this bug is not yet released #743 (to this date), I rolled back to previous version:
npm i --save #nestjs-modules/mailer#1.6.0 --force
For me,this is a template file path issue.
Review your template.dir in your MailerModule config, and compare it to your project "dist" directory.
if your template file path is "dist/templates/template.hbs"
then your template.dir should be ${process.cwd()}/templates
else if your dist directory is "dist/src/templates/template.hbs" ,which is depend on your compile configs.
then your template.dir config should be join(__dirname, 'templates')

NestJs not reading environmental variables

I followed the the Nest documentation to create the config but it's not working
app.module.ts
#Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRoot(config),
AuthModule,
UsersModule,
MailModule,
CloudinaryModule,
],
controllers: [AppController],
providers: [AppService],
})
.env file is on the src folder
mail.module.ts
#Module({
imports: [
MailerModule.forRoot({
transport: {
service: 'Gmail',
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS,
},
},
}),
],
But when I run the app its undefined my key value pairs are also there.
The problem is ConfigModule's env variables are only available at run time but not on the nestjs initial state.
To allow getting the .env after nestjs initialised, you can use async config to in MailerModule.
mail.config.ts
export class MailerConfig implements MailerOptionsFactory {
createMailerOptions(): MailerOptions | Promise<MailerOptions> {
console.log(process.env.MAIL_USER); // should have value
return {
transport: {
service: 'Gmail',
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS,
},
},
};
}
}
mail.module.ts
console.log(process.env.MAIL_USER); // undefined
#Module({
imports: [
MailerModule.forRootAsync({
useClass: MailerConfig,
}),
],
})
export class MailModule {}
you can use useFactory as well without the need of class, here I want to console.log the .env for you to check with so i used config class.

How to tear down MikroOrm in NestJS

I've recently converted my AppModule to a dynamic module so that I'm able to provide different configurations to MikroOrm depending on context (E2E tests, etc) and it currently looks like this:
#Module({
imports: [
MikroOrmModule.forFeature({
entities: [Todo],
}),
],
providers: [TodoService],
controllers: [AppController, TodosController],
})
export class AppModule {
static register(options?: {
mikroOrmOptions?: MikroOrmModuleOptions;
}): DynamicModule {
return {
module: AppModule,
imports: [
MikroOrmModule.forRoot({
entities: [Todo],
type: 'postgresql',
host: process.env.DB_HOST,
port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 5432,
user: process.env.DB_USER,
password: process.env.DB_PASS,
dbName: process.env.DB_DB,
...options?.mikroOrmOptions,
}),
],
};
}
}
Now I'm trying to ensure graceful shutdown of the app by disconnecting from the database, but not sure where to place a life-cycle hook in this case. It doesn't seem to be possible to have a dynamic module with life-cycle hooks, so I'm thinking of developing a separate provider that injects the orm and write the hook on that.
What would be the correct approach? Thanks.
Edit:
I came up with the following solution. Would appreciate someone indicating if this is the best way:
import { MikroORM } from 'mikro-orm';
...
#Module({
imports: [
MikroOrmModule.forFeature({
entities: [Todo],
}),
],
providers: [TodoService],
controllers: [AppController, TodosController],
})
export class AppModule implements OnModuleDestroy {
static register(options?: {
mikroOrmOptions?: MikroOrmModuleOptions;
}): DynamicModule {
return {
module: AppModule,
imports: [
MikroOrmModule.forRoot({
entities: [Todo],
type: 'postgresql',
host: process.env.DB_HOST,
port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 5432,
user: process.env.DB_USER,
password: process.env.DB_PASS,
dbName: process.env.DB_DB,
...options?.mikroOrmOptions,
}),
],
};
}
constructor(private orm: MikroORM) {}
async onModuleDestroy(): Promise<void> {
await this.orm.close();
}
}
As discussed in the issues, I would go with the way nestjs/typeorm is using, so using onApplicationShutdown hook.
Also linking the issue here for possible future readers:
https://github.com/dario1985/nestjs-mikro-orm/issues/10

Angular Universal not rendering route

I'm having a problem with Angular Universal, although all guides are different (the official one seems outdated aswell) I've managed to run node.js server with server side rendering.
There's still a huge problem which I can't solve, because I actually have no idea on what's going on
This is the app.module.ts
#NgModule({
declarations: [
AppComponent
],
imports: [
HttpClientModule,
BrowserModule.withServerTransition({
appId: 'ta-un-certificate'
}),
RouterModule.forRoot([{
path: '', loadChildren: './page/page.module#PageModule'
}], {
enableTracing: false,
useHash: false
}),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
})
],
providers: [
SeoService,
DataService, {
provide: HTTP_INTERCEPTORS,
useClass: HttpErrorInterceptor,
multi: true
}],
bootstrap: [
AppComponent
]
})
It simply loads another module, PageModule with its components and stuff
#NgModule({
imports: [
CommonModule,
TranslateModule,
RouterModule.forChild([{
path: ':name/:id', component: PageComponent
}, {
path: '', pathMatch: 'full', component: RedirectComponent
}])
],
declarations: [
RedirectComponent,
PageComponent,
BannerComponent,
BodyComponent,
FooterComponent
]
})
export class PageModule {
}
For the server part, I made another module, app.server.module.ts which is the one used by node.js
#NgModule({
imports: [
AppModule,
ServerModule,
ModuleMapLoaderModule,
ServerTransferStateModule
],
providers: [
SeoService
],
bootstrap: [AppComponent],
})
export class AppServerModule {
}
The problem is that if I try to call a route from node.js server, eg. http://localhost:4000/foo/bar, the node.js server console prints out a huge error, starting with this:
Error: Uncaught (in promise): ReferenceError: navigator is not defined
[...]
(it's really huge, if u need something please ask)
And page doesn't get rendered, as from cURL I get only <app-root><router-outlet></router-outlet></app-root> inside html body.
I think I've checked so many guides that I've completely lost the right way to do it, but cloning Angular Universal Starter seems doing what I'm expecting from Universal
Searching on compiled server.js script, the one executed by node, it seemed like there was an error inside Translator. So I focused searching for issues between html rendering and Translation pipe, but then I've just found a navigator.language.split inside a service (app wasn't built by me). Moved that control inside a isPlatformServer block solved my issue.
This was the breaking part of code
private _language = navigator.language.split('-')[0];
constructor(private _http: HttpClient) {
}
Which I edited as following
private _language;
constructor(#Inject(PLATFORM_ID) private platformId,
private _http: HttpClient) {
if (isPlatformServer(this.platformId)) {
this._language = 'en';
} else {
this._language = navigator.language.split('-')[0];
}
}
Fixed the issue

Resources