diff --git a/bun.lockb b/bun.lockb index 8d62ecf..434254b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/cli/actions/email/compile.ts b/cli/actions/email/compile.ts index f37fd30..904ebb3 100644 --- a/cli/actions/email/compile.ts +++ b/cli/actions/email/compile.ts @@ -1,7 +1,11 @@ +import config from '@/config.js' import type { GlobalOptions, Installer } from '@/types.js' import { action, supabaseProjectRef } from '@/utils.js' +import interpose from '@/utils/interpose.js' import { Option } from 'commander' import { log } from 'console' +import { getProperty } from 'dot-prop' +import { compile } from 'ejs' import { readFile, writeFile } from 'fs/promises' import { glob } from 'glob' import type { Root } from 'hast' @@ -14,6 +18,7 @@ import rehypeMinify from 'rehype-preset-minify' import rehypeStringify from 'rehype-stringify' const directory = resolve('supabase/emails') +const i18n = resolve('supabase/i18n') const supabaseConfigMap: Record = { 'confirmation': 'mailer_templates_confirmation_content', @@ -25,12 +30,49 @@ const supabaseConfigMap: Record = { } type Options = GlobalOptions & { + minify: boolean deploy: boolean } +type Translations = Record> + +function ejsTranslate(translations: Translations) { + return (key: string) => { + const languages = Object.keys(translations) + const branches = languages.map((lang, index) => { + if (index === 0) { + return `{{ if eq .Data.${config.i18n.attribute} "${lang}" }}` + } else { + return `{{ else if eq .Data.${config.i18n.attribute} "${lang}" }}` + } + }).concat('{{ else }}', '{{ end }}') + + const result = interpose(branches, ({ cursor, last }) => { + const _default = getProperty( + translations[config.i18n.default], + key, + '!!MISSING TRANSLATION KEY!!', + ) + + if (last) { + return _default + } + + return getProperty( + translations[languages[cursor - 1]], + key, + _default, + ) + }) + + return result.join('\n') + } +} + const installer: Installer = program => { program.command('email:compile') .description('Compile and optionally deploy email templates') + .option('--no-minify', 'Disable minifying of the result files.') .addOption( new Option('--deploy', 'Enable deployment to linked Supabase project. (Implies --linked)') .implies({ linked: true }), @@ -38,6 +80,13 @@ const installer: Installer = program => { .action(action(async ({ opts }) => { const projectRef = opts.deploy ? await supabaseProjectRef() : null + const languages = await glob('*.js', { cwd: i18n }) + const languageMap: Translations = {} + for await (const language of languages) { + const { default: data } = await import(`${i18n}/${language}`) + languageMap[basename(language, '.js')] = data + } + const files = await glob('*.mjml', { cwd: directory }) files.sort() @@ -50,8 +99,16 @@ const installer: Installer = program => { log(`Processing file ${file} (${++i}/${files.length}) …`) const abs = join(directory, file) - const template = await readFile(abs) - const converted = convert(template.toString('utf8'), { + const contents = await readFile(abs) + const template = compile(contents.toString('utf8'), { + async: true, + beautify: false, + cache: true, + }) + const compiled = await template({ + __: ejsTranslate(languageMap), + }) + const converted = convertMjml(compiled, { filePath: abs, }) @@ -69,7 +126,7 @@ const installer: Installer = program => { const result = await rehype() .use(inlineImages) - .use(rehypeMinify) + .use(opts.minify ? rehypeMinify : null) .use(rehypeStringify) .process(html) @@ -113,7 +170,7 @@ type ConvertResults = { error: null } & MJMLParseResults -function convert(input: string, options?: MJMLParsingOptions): ConvertResults { +function convertMjml(input: string, options?: MJMLParsingOptions): ConvertResults { try { const results = mjml2html(input, options) return { diff --git a/cli/config.ts b/cli/config.ts index 38cfab6..dbc5fe6 100644 --- a/cli/config.ts +++ b/cli/config.ts @@ -37,10 +37,14 @@ async function load(file?: string): Promise { } } -const defaults: Config = { +export const defaults: Config = { typeFiles: [ 'db.ts', ], + i18n: { + attribute: 'language', + default: 'en', + }, } const configFile = await selectFile() diff --git a/cli/types.ts b/cli/types.ts index 2cc3674..10c94a4 100644 --- a/cli/types.ts +++ b/cli/types.ts @@ -3,6 +3,10 @@ import { z } from 'zod' export type Config = { typeFiles: string[] + i18n: { + attribute: string + default: string + } } export const globalOptionsSchema = z.object({ diff --git a/cli/utils/interpose.ts b/cli/utils/interpose.ts new file mode 100644 index 0000000..012f131 --- /dev/null +++ b/cli/utils/interpose.ts @@ -0,0 +1,48 @@ +type Args = { + /** + * The current index of the overall iteration. + */ + index: number + + /** + * The current index of the interpose iteration. + */ + cursor: number + + /** + * Whether this is the first interpose. + */ + first: boolean + + /** + * Whether this is the last interpose. + */ + last: boolean +} + +/** + * Interpose items to an array. + * + * @param arr The source array to modify. + * @param producer A function that returns the items to interpose. + * @returns The interposed array. + */ +export default function interpose(arr: T[], producer: (args: Args) => T) { + let cursor = 0 + + const result = [] + const max = arr.length * 2 - 1 + + for (let index = 0; index < max; index++) { + const first = index == 1 + const last = index === max - 2 + + if (index % 2 === 0) { + result.push(arr[cursor++]) + } else { + result.push(producer({index, cursor, first, last})) + } + } + + return result +} diff --git a/eslint.config.js b/eslint.config.js index 09ee24c..844b592 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,7 +6,7 @@ const config = [ { ignores: [ '*.d.ts', - 'supabase/', + 'supabase/functions', '.venv', ], }, diff --git a/package.json b/package.json index b53655b..9419be6 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@inquirer/select": "^2.4.7", "commander": "^12.1.0", "dayjs": "^1.11.12", + "dot-prop": "^9.0.0", "dotenv": "^16.4.5", "glob": "^11.0.0", "hast-util-select": "^6.0.2", diff --git a/supabase/emails/confirmation.mjml b/supabase/emails/confirmation.mjml index 1e3a50d..b2e5c02 100644 --- a/supabase/emails/confirmation.mjml +++ b/supabase/emails/confirmation.mjml @@ -12,13 +12,15 @@ - Hello World - - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + + <%- __("confirmation.title") %> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + <%- __("confirmation.text1") %> + + + <%- __("confirmation.text2") %> @@ -38,7 +40,7 @@ - Copyright (c) [NAME] [YEAR] + Copyright (c) act coding GbR {{ now.UTC.Year }} diff --git a/supabase/i18n/de.js b/supabase/i18n/de.js new file mode 100644 index 0000000..3df2f78 --- /dev/null +++ b/supabase/i18n/de.js @@ -0,0 +1,9 @@ +export default { + confirmation: { + title: 'Konto aktivieren', + text1: `Es wurde ein neues Konto mit dieser E-Mail-Adresse erstellt. + Verwende den unten stehenden Link, um die Erstellung des Kontos zu bestätigen.`, + text2: `Wenn du kein Konto bei uns registriert haben, kannst du diese E-Mail einfach ignorieren. + Prüfe aber zur Sicherheit die Integrität deiner E-Mail-Adresse.`, + }, +} diff --git a/supabase/i18n/en.js b/supabase/i18n/en.js new file mode 100644 index 0000000..b7db2ba --- /dev/null +++ b/supabase/i18n/en.js @@ -0,0 +1,9 @@ +export default { + confirmation: { + title: 'Active account', + text1: `A new account has been created using this email address. + Use the link below to confirm the account creation.`, + text2: `If you did not register an account with us, you can simply ignore + this email. Be sure to verify your email's account integrity.`, + }, +}