ソースを参照

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
 generated: true
 ---
+
 <!-- 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 GenerationInfo from '@site/src/components/GenerationInfo';
 import MemberDescription from '@site/src/components/MemberDescription';
 
-
 ## EmailEventHandler
 
 <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
 to specify which event to respond to.
 
-*Example*
+_Example_
 
 ```ts
 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
@@ -39,22 +40,36 @@ also to locate the directory of the email template files. So in the example abov
 
 ## 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
-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
@@ -115,87 +130,75 @@ import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
 import { quoteRequestedHandler } from './plugins/quote-plugin';
 
 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">
 
 ### 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
 
-<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.
 Multiple filter functions may be defined.
+
 ### 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.
 
 The recipient can be a plain email address: `'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>'`
+
 ### 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.
+
 ### 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
 and subject line for interpolation.
+
 ### 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
 setTemplateVars() method.
+
 ### 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
 setTemplateVars() method.
+
 ### 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".
+
 ### 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
 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.
 - 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>,
-  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
   `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
 
-<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
 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
 errors may result if using the DefaultJobQueuePlugin with certain DBs such as MySQL/MariaDB.
 
-*Example*
+_Example_
 
 ```ts
 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
 
-<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
 templates for channels or languageCodes other than the default.
+
 ### 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.
 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
 added to the `event` as the `data` property.
 
-*Example*
+_Example_
 
 ```ts
 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
 
-<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
 from this handler, which is useful when developing the email templates.
 
-
 </div>

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

@@ -1,22 +1,23 @@
 ---
-title: "TemplateLoader"
+title: 'TemplateLoader'
 isDefaultIndex: false
 generated: true
 ---
+
 <!-- 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 GenerationInfo from '@site/src/components/GenerationInfo';
 import MemberDescription from '@site/src/components/MemberDescription';
 
-
 ## TemplateLoader
 
 <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
-and return the template as a string.
+and returns the template as a string.
 
-*Example*
+_Example_
 
 ```ts
 import { EmailPlugin, TemplateLoader } from '@vendure/email-plugin';
@@ -46,20 +47,19 @@ interface TemplateLoader {
 
 ### 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
+
 ### loadPartials
 
-<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`}   />
+<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`} />
 
 Load partials and return their contents.
 This method is only called during initialization, i.e. during server startup.
 
-
 </div>
 
-
 ## FileBasedTemplateLoader
 
 <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[]>;
 }
 ```
-* 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">
 
 ### constructor
 
-<MemberInfo kind="method" type={`(templatePath: string) => FileBasedTemplateLoader`}   />
-
+<MemberInfo kind="method" type={`(templatePath: string) => FileBasedTemplateLoader`} />
 
 ### 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
 
-<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`}   />
-
-
-
+<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`} />
 
 </div>

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

@@ -47,20 +47,34 @@ import {
  * ## 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.
+ * 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
  * ```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
@@ -102,7 +116,7 @@ import {
  *             of the quote you recently requested:
  *         </mj-text>
  *
- *         <--! your custom email layout goes here -->
+ *         <!-- your custom email layout goes here -->
  *     </mj-column>
  * </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 { EmailSender } from './sender/email-sender';
 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', () => {
     let eventBus: EventBus;
@@ -1003,6 +1010,47 @@ describe('EmailPlugin', () => {
             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 {