瀏覽代碼

docs(email-plugin): Update deprecated code and add customTemplateLoader example and tests (#3641)

Housein Abo Shaar 6 月之前
父節點
當前提交
1846b029f1

+ 101 - 96
docs/docs/reference/core-plugins/email-plugin/email-event-handler.md

@@ -1,14 +1,15 @@
 ---
 ---
-title: "EmailEventHandler"
+title: 'EmailEventHandler'
 isDefaultIndex: false
 isDefaultIndex: false
 generated: true
 generated: true
 ---
 ---
+
 <!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
 <!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
 import MemberInfo from '@site/src/components/MemberInfo';
 import MemberInfo from '@site/src/components/MemberInfo';
 import GenerationInfo from '@site/src/components/GenerationInfo';
 import GenerationInfo from '@site/src/components/GenerationInfo';
 import MemberDescription from '@site/src/components/MemberDescription';
 import MemberDescription from '@site/src/components/MemberDescription';
 
 
-
 ## EmailEventHandler
 ## EmailEventHandler
 
 
 <GenerationInfo sourceFile="packages/email-plugin/src/handler/event-handler.ts" sourceLine="136" packageName="@vendure/email-plugin" />
 <GenerationInfo sourceFile="packages/email-plugin/src/handler/event-handler.ts" sourceLine="136" packageName="@vendure/email-plugin" />
@@ -18,16 +19,16 @@ The EmailEventHandler defines how the EmailPlugin will respond to a given event.
 A handler is created by creating a new <a href='/reference/core-plugins/email-plugin/email-event-listener#emaileventlistener'>EmailEventListener</a> and calling the `.on()` method
 A handler is created by creating a new <a href='/reference/core-plugins/email-plugin/email-event-listener#emaileventlistener'>EmailEventListener</a> and calling the `.on()` method
 to specify which event to respond to.
 to specify which event to respond to.
 
 
-*Example*
+_Example_
 
 
 ```ts
 ```ts
 const confirmationHandler = new EmailEventListener('order-confirmation')
 const confirmationHandler = new EmailEventListener('order-confirmation')
-  .on(OrderStateTransitionEvent)
-  .filter(event => event.toState === 'PaymentSettled')
-  .setRecipient(event => event.order.customer.emailAddress)
-  .setFrom('{{ fromAddress }}')
-  .setSubject(`Order confirmation for #{{ order.code }}`)
-  .setTemplateVars(event => ({ order: event.order }));
+    .on(OrderStateTransitionEvent)
+    .filter(event => event.toState === 'PaymentSettled')
+    .setRecipient(event => event.order.customer.emailAddress)
+    .setFrom('{{ fromAddress }}')
+    .setSubject(`Order confirmation for #{{ order.code }}`)
+    .setTemplateVars(event => ({ order: event.order }));
 ```
 ```
 
 
 This example creates a handler which listens for the `OrderStateTransitionEvent` and if the Order has
 This example creates a handler which listens for the `OrderStateTransitionEvent` and if the Order has
@@ -39,22 +40,36 @@ also to locate the directory of the email template files. So in the example abov
 
 
 ## Handling other languages
 ## Handling other languages
 
 
-By default, the handler will respond to all events on all channels and use the same subject ("Order confirmation for #12345" above)
-and body template. Where the server is intended to support multiple languages, the `.addTemplate()` method may be used
-to define the subject and body template for specific language and channel combinations.
+By default, a handler will respond to all events on all channels and use the same subject ("Order confirmation for #12345" above)
+and body template.
+
+Since v2.0 the `.addTemplate()` method has been **deprecated**. To serve different templates—for example, based on the current
+`languageCode`—implement a custom <a href='/reference/core-plugins/email-plugin/template-loader#templateloader'>TemplateLoader</a> and pass it to `EmailPlugin.init({ templateLoader: new MyTemplateLoader() })`.
 
 
-The language is determined by looking at the `languageCode` property of the event's `ctx` (<a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>) object.
+The language is typically determined by the `languageCode` property of the event's `ctx` (<a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>) object, so the
+`loadTemplate()` method can use that to locate the correct template file.
 
 
-*Example*
+_Example_
 
 
 ```ts
 ```ts
-const extendedConfirmationHandler = confirmationHandler
-  .addTemplate({
-    channelCode: 'default',
-    languageCode: LanguageCode.de,
-    templateFile: 'body.de.hbs',
-    subject: 'Bestellbestätigung für #{{ order.code }}',
-  })
+import { EmailPlugin, TemplateLoader } from '@vendure/email-plugin';
+import { readFileSync } from 'fs';
+import path from 'path';
+
+class CustomLanguageAwareTemplateLoader implements TemplateLoader {
+    constructor(private templateDir: string) {}
+
+    async loadTemplate(_injector, ctx, { type, templateName }) {
+        // e.g. returns the content of "body.de.hbs" or "body.en.hbs" depending on ctx.languageCode
+        const filePath = path.join(this.templateDir, type, `${templateName}.${ctx.languageCode}.hbs`);
+        return readFileSync(filePath, 'utf-8');
+    }
+}
+
+EmailPlugin.init({
+    templateLoader: new CustomLanguageAwareTemplateLoader(path.join(__dirname, '../static/email/templates')),
+    handlers: defaultEmailHandlers,
+});
 ```
 ```
 
 
 ## Defining a custom handler
 ## Defining a custom handler
@@ -115,87 +130,75 @@ import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
 import { quoteRequestedHandler } from './plugins/quote-plugin';
 import { quoteRequestedHandler } from './plugins/quote-plugin';
 
 
 const config: VendureConfig = {
 const config: VendureConfig = {
-  // Add an instance of the plugin to the plugins array
-  plugins: [
-    EmailPlugin.init({
-      handler: [...defaultEmailHandlers, quoteRequestedHandler],
-      // ... etc
-    }),
-  ],
+    // Add an instance of the plugin to the plugins array
+    plugins: [
+        EmailPlugin.init({
+            handler: [...defaultEmailHandlers, quoteRequestedHandler],
+            // ... etc
+        }),
+    ],
 };
 };
 ```
 ```
 
 
-```ts title="Signature"
-class EmailEventHandler<T extends string = string, Event extends EventWithContext = EventWithContext> {
-    constructor(listener: EmailEventListener<T>, event: Type<Event>)
-    filter(filterFn: (event: Event) => boolean) => EmailEventHandler<T, Event>;
-    setRecipient(setRecipientFn: (event: Event) => string) => EmailEventHandler<T, Event>;
-    setLanguageCode(setLanguageCodeFn: (event: Event) => LanguageCode | undefined) => EmailEventHandler<T, Event>;
-    setTemplateVars(templateVarsFn: SetTemplateVarsFn<Event>) => EmailEventHandler<T, Event>;
-    setSubject(defaultSubject: string | SetSubjectFn<Event>) => EmailEventHandler<T, Event>;
-    setFrom(from: string) => EmailEventHandler<T, Event>;
-    setOptionalAddressFields(optionalAddressFieldsFn: SetOptionalAddressFieldsFn<Event>) => ;
-    setMetadata(optionalSetMetadataFn: SetMetadataFn<Event>) => ;
-    setAttachments(setAttachmentsFn: SetAttachmentsFn<Event>) => ;
-    addTemplate(config: EmailTemplateConfig) => EmailEventHandler<T, Event>;
-    loadData(loadDataFn: LoadDataFn<Event, R>) => EmailEventHandlerWithAsyncData<R, T, Event, EventWithAsyncData<Event, R>>;
-    setMockEvent(event: Omit<Event, 'ctx' | 'data'>) => EmailEventHandler<T, Event>;
-}
-```
-
 <div className="members-wrapper">
 <div className="members-wrapper">
 
 
 ### constructor
 ### constructor
 
 
-<MemberInfo kind="method" type={`(listener: <a href='/reference/core-plugins/email-plugin/email-event-listener#emaileventlistener'>EmailEventListener</a>&#60;T&#62;, event: Type&#60;Event&#62;) => EmailEventHandler`}   />
-
+<MemberInfo kind="method" type={`(listener: <a href='/reference/core-plugins/email-plugin/email-event-listener#emaileventlistener'>EmailEventListener</a>&#60;T&#62;, event: Type&#60;Event&#62;) => EmailEventHandler`} />
 
 
 ### filter
 ### filter
 
 
-<MemberInfo kind="method" type={`(filterFn: (event: Event) =&#62; boolean) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}   />
+<MemberInfo kind="method" type={`(filterFn: (event: Event) =&#62; boolean) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} />
 
 
 Defines a predicate function which is used to determine whether the event will trigger an email.
 Defines a predicate function which is used to determine whether the event will trigger an email.
 Multiple filter functions may be defined.
 Multiple filter functions may be defined.
+
 ### setRecipient
 ### setRecipient
 
 
-<MemberInfo kind="method" type={`(setRecipientFn: (event: Event) =&#62; string) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}   />
+<MemberInfo kind="method" type={`(setRecipientFn: (event: Event) =&#62; string) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} />
 
 
 A function which defines how the recipient email address should be extracted from the incoming event.
 A function which defines how the recipient email address should be extracted from the incoming event.
 
 
 The recipient can be a plain email address: `'foobar@example.com'`
 The recipient can be a plain email address: `'foobar@example.com'`
 Or with a formatted name (includes unicode support): `'Ноде Майлер <foobar@example.com>'`
 Or with a formatted name (includes unicode support): `'Ноде Майлер <foobar@example.com>'`
 Or a comma-separated list of addresses: `'foobar@example.com, "Ноде Майлер" <bar@example.com>'`
 Or a comma-separated list of addresses: `'foobar@example.com, "Ноде Майлер" <bar@example.com>'`
+
 ### setLanguageCode
 ### setLanguageCode
 
 
-<MemberInfo kind="method" type={`(setLanguageCodeFn: (event: Event) =&#62; <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a> | undefined) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}  since="1.8.0"  />
+<MemberInfo kind="method" type={`(setLanguageCodeFn: (event: Event) =&#62; <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a> | undefined) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} since="1.8.0" />
 
 
 A function which allows to override the language of the email. If not defined, the language from the context will be used.
 A function which allows to override the language of the email. If not defined, the language from the context will be used.
+
 ### setTemplateVars
 ### setTemplateVars
 
 
-<MemberInfo kind="method" type={`(templateVarsFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#settemplatevarsfn'>SetTemplateVarsFn</a>&#60;Event&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}   />
+<MemberInfo kind="method" type={`(templateVarsFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#settemplatevarsfn'>SetTemplateVarsFn</a>&#60;Event&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} />
 
 
 A function which returns an object hash of variables which will be made available to the Handlebars template
 A function which returns an object hash of variables which will be made available to the Handlebars template
 and subject line for interpolation.
 and subject line for interpolation.
+
 ### setSubject
 ### setSubject
 
 
-<MemberInfo kind="method" type={`(defaultSubject: string | <a href='/reference/core-plugins/email-plugin/email-plugin-types#setsubjectfn'>SetSubjectFn</a>&#60;Event&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}   />
+<MemberInfo kind="method" type={`(defaultSubject: string | <a href='/reference/core-plugins/email-plugin/email-plugin-types#setsubjectfn'>SetSubjectFn</a>&#60;Event&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} />
 
 
 Sets the default subject of the email. The subject string may use Handlebars variables defined by the
 Sets the default subject of the email. The subject string may use Handlebars variables defined by the
 setTemplateVars() method.
 setTemplateVars() method.
+
 ### setFrom
 ### setFrom
 
 
-<MemberInfo kind="method" type={`(from: string) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}   />
+<MemberInfo kind="method" type={`(from: string) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} />
 
 
 Sets the default from field of the email. The from string may use Handlebars variables defined by the
 Sets the default from field of the email. The from string may use Handlebars variables defined by the
 setTemplateVars() method.
 setTemplateVars() method.
+
 ### setOptionalAddressFields
 ### setOptionalAddressFields
 
 
-<MemberInfo kind="method" type={`(optionalAddressFieldsFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#setoptionaladdressfieldsfn'>SetOptionalAddressFieldsFn</a>&#60;Event&#62;) => `}  since="1.1.0"  />
+<MemberInfo kind="method" type={`(optionalAddressFieldsFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#setoptionaladdressfieldsfn'>SetOptionalAddressFieldsFn</a>&#60;Event&#62;) => `} since="1.1.0" />
 
 
 A function which allows <a href='/reference/core-plugins/email-plugin/email-plugin-types#optionaladdressfields'>OptionalAddressFields</a> to be specified such as "cc" and "bcc".
 A function which allows <a href='/reference/core-plugins/email-plugin/email-plugin-types#optionaladdressfields'>OptionalAddressFields</a> to be specified such as "cc" and "bcc".
+
 ### setMetadata
 ### setMetadata
 
 
-<MemberInfo kind="method" type={`(optionalSetMetadataFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#setmetadatafn'>SetMetadataFn</a>&#60;Event&#62;) => `}  since="3.1.0"  />
+<MemberInfo kind="method" type={`(optionalSetMetadataFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#setmetadatafn'>SetMetadataFn</a>&#60;Event&#62;) => `} since="3.1.0" />
 
 
 A function which allows <a href='/reference/core-plugins/email-plugin/email-plugin-types#emailmetadata'>EmailMetadata</a> to be specified for the email. This can be used
 A function which allows <a href='/reference/core-plugins/email-plugin/email-plugin-types#emailmetadata'>EmailMetadata</a> to be specified for the email. This can be used
 to store arbitrary data about the email which can be used for tracking or other purposes.
 to store arbitrary data about the email which can be used for tracking or other purposes.
@@ -204,23 +207,22 @@ It will be exposed in the <a href='/reference/core-plugins/email-plugin/email-se
 
 
 - An <a href='/reference/typescript-api/events/event-types#orderstatetransitionevent'>OrderStateTransitionEvent</a> occurs, and the EmailEventListener starts processing it.
 - An <a href='/reference/typescript-api/events/event-types#orderstatetransitionevent'>OrderStateTransitionEvent</a> occurs, and the EmailEventListener starts processing it.
 - The EmailEventHandler attaches metadata to the email:
 - The EmailEventHandler attaches metadata to the email:
-   ```ts
-   new EmailEventListener(EventType.ORDER_CONFIRMATION)
-     .on(OrderStateTransitionEvent)
-     .setMetadata(event => ({
-       type: EventType.ORDER_CONFIRMATION,
-       orderId: event.order.id,
-     }));
-  ```
+    ```ts
+    new EmailEventListener(EventType.ORDER_CONFIRMATION).on(OrderStateTransitionEvent).setMetadata(event => ({
+        type: EventType.ORDER_CONFIRMATION,
+        orderId: event.order.id,
+    }));
+    ```
 - Then, the EmailPlugin tries to send the email and publishes <a href='/reference/core-plugins/email-plugin/email-send-event#emailsendevent'>EmailSendEvent</a>,
 - Then, the EmailPlugin tries to send the email and publishes <a href='/reference/core-plugins/email-plugin/email-send-event#emailsendevent'>EmailSendEvent</a>,
-  passing ctx, emailDetails, error or success, and this metadata.
+  passing `ctx`, emailDetails, error or success, and this metadata.
 - In another part of the server, we have an eventBus that subscribes to EmailSendEvent. We can use
 - In another part of the server, we have an eventBus that subscribes to EmailSendEvent. We can use
   `metadata.type` and `metadata.orderId` to identify the related order. For example, we can indicate on the
   `metadata.type` and `metadata.orderId` to identify the related order. For example, we can indicate on the
-   order that the email was successfully sent, or in case of an error, send a notification confirming
-   the order in another available way.
+  order that the email was successfully sent, or in case of an error, send a notification confirming
+  the order in another available way.
+
 ### setAttachments
 ### setAttachments
 
 
-<MemberInfo kind="method" type={`(setAttachmentsFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#setattachmentsfn'>SetAttachmentsFn</a>&#60;Event&#62;) => `}   />
+<MemberInfo kind="method" type={`(setAttachmentsFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#setattachmentsfn'>SetAttachmentsFn</a>&#60;Event&#62;) => `} />
 
 
 Defines one or more files to be attached to the email. An attachment can be specified
 Defines one or more files to be attached to the email. An attachment can be specified
 as either a `path` (to a file or URL) or as `content` which can be a string, Buffer or Stream.
 as either a `path` (to a file or URL) or as `content` which can be a string, Buffer or Stream.
@@ -230,31 +232,34 @@ into the job queue. For this reason the total size of all attachments passed as
 **less than ~50k**. If the attachments are greater than that limit, a warning will be logged and
 **less than ~50k**. If the attachments are greater than that limit, a warning will be logged and
 errors may result if using the DefaultJobQueuePlugin with certain DBs such as MySQL/MariaDB.
 errors may result if using the DefaultJobQueuePlugin with certain DBs such as MySQL/MariaDB.
 
 
-*Example*
+_Example_
 
 
 ```ts
 ```ts
 const testAttachmentHandler = new EmailEventListener('activate-voucher')
 const testAttachmentHandler = new EmailEventListener('activate-voucher')
-  .on(ActivateVoucherEvent)
-  // ... omitted some steps for brevity
-  .setAttachments(async (event) => {
-    const { imageUrl, voucherCode } = await getVoucherDataForUser(event.user.id);
-    return [
-      {
-        filename: `voucher-${voucherCode}.jpg`,
-        path: imageUrl,
-      },
-    ];
-  });
+    .on(ActivateVoucherEvent)
+    // ... omitted some steps for brevity
+    .setAttachments(async event => {
+        const { imageUrl, voucherCode } = await getVoucherDataForUser(event.user.id);
+        return [
+            {
+                filename: `voucher-${voucherCode}.jpg`,
+                path: imageUrl,
+            },
+        ];
+    });
 ```
 ```
+
 ### addTemplate
 ### addTemplate
 
 
-<MemberInfo kind="method" type={`(config: EmailTemplateConfig) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}   />
+<MemberInfo kind="method" deprecated="Define a custom TemplateLoader on plugin initalization to define templates based on the RequestContext.
+E.g. `EmailPlugin.init({ templateLoader: new CustomTemplateLoader() })`" type={`(config: EmailTemplateConfig) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} />
 
 
 Add configuration for another template other than the default `"body.hbs"`. Use this method to define specific
 Add configuration for another template other than the default `"body.hbs"`. Use this method to define specific
 templates for channels or languageCodes other than the default.
 templates for channels or languageCodes other than the default.
+
 ### loadData
 ### loadData
 
 
-<MemberInfo kind="method" type={`(loadDataFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#loaddatafn'>LoadDataFn</a>&#60;Event, R&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler-with-async-data#emaileventhandlerwithasyncdata'>EmailEventHandlerWithAsyncData</a>&#60;R, T, Event, <a href='/reference/core-plugins/email-plugin/email-plugin-types#eventwithasyncdata'>EventWithAsyncData</a>&#60;Event, R&#62;&#62;`}   />
+<MemberInfo kind="method" type={`(loadDataFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#loaddatafn'>LoadDataFn</a>&#60;Event, R&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler-with-async-data#emaileventhandlerwithasyncdata'>EmailEventHandlerWithAsyncData</a>&#60;R, T, Event, <a href='/reference/core-plugins/email-plugin/email-plugin-types#eventwithasyncdata'>EventWithAsyncData</a>&#60;Event, R&#62;&#62;`} />
 
 
 Allows data to be loaded asynchronously which can then be used as template variables.
 Allows data to be loaded asynchronously which can then be used as template variables.
 The `loadDataFn` has access to the event, the TypeORM `Connection` object, and an
 The `loadDataFn` has access to the event, the TypeORM `Connection` object, and an
@@ -262,28 +267,28 @@ The `loadDataFn` has access to the event, the TypeORM `Connection` object, and a
 by the <a href='/reference/typescript-api/plugin/plugin-common-module#plugincommonmodule'>PluginCommonModule</a>. The return value of the `loadDataFn` will be
 by the <a href='/reference/typescript-api/plugin/plugin-common-module#plugincommonmodule'>PluginCommonModule</a>. The return value of the `loadDataFn` will be
 added to the `event` as the `data` property.
 added to the `event` as the `data` property.
 
 
-*Example*
+_Example_
 
 
 ```ts
 ```ts
 new EmailEventListener('order-confirmation')
 new EmailEventListener('order-confirmation')
-  .on(OrderStateTransitionEvent)
-  .filter(event => event.toState === 'PaymentSettled' && !!event.order.customer)
-  .loadData(({ event, injector }) => {
-    const orderService = injector.get(OrderService);
-    return orderService.getOrderPayments(event.order.id);
-  })
-  .setTemplateVars(event => ({
-    order: event.order,
-    payments: event.data,
-  }))
-  // ...
+    .on(OrderStateTransitionEvent)
+    .filter(event => event.toState === 'PaymentSettled' && !!event.order.customer)
+    .loadData(({ event, injector }) => {
+        const orderService = injector.get(OrderService);
+        return orderService.getOrderPayments(event.order.id);
+    })
+    .setTemplateVars(event => ({
+        order: event.order,
+        payments: event.data,
+    }));
+// ...
 ```
 ```
+
 ### setMockEvent
 ### setMockEvent
 
 
-<MemberInfo kind="method" type={`(event: Omit&#60;Event, 'ctx' | 'data'&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}   />
+<MemberInfo kind="method" type={`(event: Omit&#60;Event, 'ctx' | 'data'&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} />
 
 
 Optionally define a mock Event which is used by the dev mode mailbox app for generating mock emails
 Optionally define a mock Event which is used by the dev mode mailbox app for generating mock emails
 from this handler, which is useful when developing the email templates.
 from this handler, which is useful when developing the email templates.
 
 
-
 </div>
 </div>

+ 12 - 18
docs/docs/reference/core-plugins/email-plugin/template-loader.md

@@ -1,22 +1,23 @@
 ---
 ---
-title: "TemplateLoader"
+title: 'TemplateLoader'
 isDefaultIndex: false
 isDefaultIndex: false
 generated: true
 generated: true
 ---
 ---
+
 <!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
 <!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
 import MemberInfo from '@site/src/components/MemberInfo';
 import MemberInfo from '@site/src/components/MemberInfo';
 import GenerationInfo from '@site/src/components/GenerationInfo';
 import GenerationInfo from '@site/src/components/GenerationInfo';
 import MemberDescription from '@site/src/components/MemberDescription';
 import MemberDescription from '@site/src/components/MemberDescription';
 
 
-
 ## TemplateLoader
 ## TemplateLoader
 
 
 <GenerationInfo sourceFile="packages/email-plugin/src/template-loader/template-loader.ts" sourceLine="32" packageName="@vendure/email-plugin" />
 <GenerationInfo sourceFile="packages/email-plugin/src/template-loader/template-loader.ts" sourceLine="32" packageName="@vendure/email-plugin" />
 
 
 Loads email templates based on the given request context, type and template name
 Loads email templates based on the given request context, type and template name
-and return the template as a string.
+and returns the template as a string.
 
 
-*Example*
+_Example_
 
 
 ```ts
 ```ts
 import { EmailPlugin, TemplateLoader } from '@vendure/email-plugin';
 import { EmailPlugin, TemplateLoader } from '@vendure/email-plugin';
@@ -46,20 +47,19 @@ interface TemplateLoader {
 
 
 ### loadTemplate
 ### loadTemplate
 
 
-<MemberInfo kind="method" type={`(injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, input: <a href='/reference/core-plugins/email-plugin/email-plugin-types#loadtemplateinput'>LoadTemplateInput</a>) => Promise&#60;string&#62;`}   />
+<MemberInfo kind="method" type={`(injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, input: <a href='/reference/core-plugins/email-plugin/email-plugin-types#loadtemplateinput'>LoadTemplateInput</a>) => Promise&#60;string&#62;`} />
 
 
 Load template and return it's content as a string
 Load template and return it's content as a string
+
 ### loadPartials
 ### loadPartials
 
 
-<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`}   />
+<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`} />
 
 
 Load partials and return their contents.
 Load partials and return their contents.
 This method is only called during initialization, i.e. during server startup.
 This method is only called during initialization, i.e. during server startup.
 
 
-
 </div>
 </div>
 
 
-
 ## FileBasedTemplateLoader
 ## FileBasedTemplateLoader
 
 
 <GenerationInfo sourceFile="packages/email-plugin/src/template-loader/file-based-template-loader.ts" sourceLine="17" packageName="@vendure/email-plugin" />
 <GenerationInfo sourceFile="packages/email-plugin/src/template-loader/file-based-template-loader.ts" sourceLine="17" packageName="@vendure/email-plugin" />
@@ -74,27 +74,21 @@ class FileBasedTemplateLoader implements TemplateLoader {
     loadPartials() => Promise<Partial[]>;
     loadPartials() => Promise<Partial[]>;
 }
 }
 ```
 ```
-* Implements: <code><a href='/reference/core-plugins/email-plugin/template-loader#templateloader'>TemplateLoader</a></code>
-
 
 
+- Implements: <code><a href='/reference/core-plugins/email-plugin/template-loader#templateloader'>TemplateLoader</a></code>
 
 
 <div className="members-wrapper">
 <div className="members-wrapper">
 
 
 ### constructor
 ### constructor
 
 
-<MemberInfo kind="method" type={`(templatePath: string) => FileBasedTemplateLoader`}   />
-
+<MemberInfo kind="method" type={`(templatePath: string) => FileBasedTemplateLoader`} />
 
 
 ### loadTemplate
 ### loadTemplate
 
 
-<MemberInfo kind="method" type={`(_injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, _ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, { type, templateName }: <a href='/reference/core-plugins/email-plugin/email-plugin-types#loadtemplateinput'>LoadTemplateInput</a>) => Promise&#60;string&#62;`}   />
-
+<MemberInfo kind="method" type={`(_injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, _ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, { type, templateName }: <a href='/reference/core-plugins/email-plugin/email-plugin-types#loadtemplateinput'>LoadTemplateInput</a>) => Promise&#60;string&#62;`} />
 
 
 ### loadPartials
 ### loadPartials
 
 
-<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`}   />
-
-
-
+<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`} />
 
 
 </div>
 </div>

+ 25 - 11
packages/email-plugin/src/handler/event-handler.ts

@@ -47,20 +47,34 @@ import {
  * ## Handling other languages
  * ## Handling other languages
  *
  *
  * By default, the handler will respond to all events on all channels and use the same subject ("Order confirmation for #12345" above)
  * By default, the handler will respond to all events on all channels and use the same subject ("Order confirmation for #12345" above)
- * and body template. Where the server is intended to support multiple languages, the `.addTemplate()` method may be used
- * to define the subject and body template for specific language and channel combinations.
+ * and body template.
  *
  *
- * The language is determined by looking at the `languageCode` property of the event's `ctx` ({@link RequestContext}) object.
+ * Since v2.0 the `.addTemplate()` method has been **deprecated**. To serve different templates — for example, based on the current
+ * `languageCode` — implement a custom {@link TemplateLoader} and pass it to `EmailPlugin.init({ templateLoader: new MyTemplateLoader() })`.
+ *
+ * The language is typically determined by the `languageCode` property of the event's `ctx` ({@link RequestContext}) object, so the
+ * `loadTemplate()` method can use that to locate the correct template file.
  *
  *
  * @example
  * @example
  * ```ts
  * ```ts
- * const extendedConfirmationHandler = confirmationHandler
- *   .addTemplate({
- *     channelCode: 'default',
- *     languageCode: LanguageCode.de,
- *     templateFile: 'body.de.hbs',
- *     subject: 'Bestellbestätigung für #{{ order.code }}',
- *   })
+ * import { EmailPlugin, TemplateLoader } from '\@vendure/email-plugin';
+ * import { readFileSync } from 'fs';
+ * import path from 'path';
+ *
+ * class CustomLanguageAwareTemplateLoader implements TemplateLoader {
+ *   constructor(private templateDir: string) {}
+ *
+ *   async loadTemplate(_injector, ctx, { type, templateName }) {
+ *     // e.g. returns the content of "body.de.hbs" or "body.en.hbs" depending on ctx.languageCode
+ *     const filePath = path.join(this.templateDir, type, `${templateName}.${ctx.languageCode}.hbs`);
+ *     return readFileSync(filePath, 'utf-8');
+ *   }
+ * }
+ *
+ * EmailPlugin.init({
+ *   templateLoader: new CustomLanguageAwareTemplateLoader(path.join(__dirname, '../static/email/templates')),
+ *   handlers: defaultEmailHandlers,
+ * });
  * ```
  * ```
  *
  *
  * ## Defining a custom handler
  * ## Defining a custom handler
@@ -102,7 +116,7 @@ import {
  *             of the quote you recently requested:
  *             of the quote you recently requested:
  *         </mj-text>
  *         </mj-text>
  *
  *
- *         <--! your custom email layout goes here -->
+ *         <!-- your custom email layout goes here -->
  *     </mj-column>
  *     </mj-column>
  * </mj-section>
  * </mj-section>
  *
  *

+ 49 - 1
packages/email-plugin/src/plugin.spec.ts

@@ -28,7 +28,14 @@ import { EmailEventHandler } from './handler/event-handler';
 import { EmailPlugin } from './plugin';
 import { EmailPlugin } from './plugin';
 import { EmailSender } from './sender/email-sender';
 import { EmailSender } from './sender/email-sender';
 import { FileBasedTemplateLoader } from './template-loader/file-based-template-loader';
 import { FileBasedTemplateLoader } from './template-loader/file-based-template-loader';
-import { EmailDetails, EmailPluginOptions, EmailTransportOptions } from './types';
+import { TemplateLoader } from './template-loader/template-loader';
+import {
+    EmailDetails,
+    Partial as EmailPartial,
+    EmailPluginOptions,
+    EmailTransportOptions,
+    LoadTemplateInput,
+} from './types';
 
 
 describe('EmailPlugin', () => {
 describe('EmailPlugin', () => {
     let eventBus: EventBus;
     let eventBus: EventBus;
@@ -1003,6 +1010,47 @@ describe('EmailPlugin', () => {
             expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
             expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
         });
         });
     });
     });
+    // Only in case of custom template loader - part of the jsDoc - not used in the core code
+    describe('CustomLanguageAwareTemplateLoader example', () => {
+        it('loads language-specific template correctly', async () => {
+            class CustomLanguageAwareTemplateLoader implements TemplateLoader {
+                constructor(private templateDir: string) {}
+
+                async loadTemplate(
+                    _injector: Injector,
+                    context: RequestContext,
+                    { type, templateName }: LoadTemplateInput,
+                ) {
+                    const filePath = path.join(
+                        this.templateDir,
+                        type,
+                        `${templateName}.${context.languageCode}.hbs`,
+                    );
+                    return readFileSync(filePath, 'utf-8');
+                }
+
+                async loadPartials(): Promise<EmailPartial[]> {
+                    return [];
+                }
+            }
+
+            const templatePath = path.join(__dirname, '../test-templates');
+            const loader = new CustomLanguageAwareTemplateLoader(templatePath);
+
+            const requestContext = RequestContext.deserialize({
+                _channel: { code: DEFAULT_CHANNEL_CODE },
+                _languageCode: LanguageCode.de,
+            } as any);
+
+            const result = await loader.loadTemplate({} as Injector, requestContext, {
+                type: 'test',
+                templateName: 'body',
+                templateVars: {},
+            });
+
+            expect(result).toContain('German body');
+        });
+    });
 });
 });
 
 
 class FakeCustomSender implements EmailSender {
 class FakeCustomSender implements EmailSender {