惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
S
SegmentFault 最新的问题
博客园 - 三生石上(FineUI控件)
Hugging Face - Blog
Hugging Face - Blog
博客园 - Franky
云风的 BLOG
云风的 BLOG
I
InfoQ
小众软件
小众软件
Last Week in AI
Last Week in AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
大猫的无限游戏
大猫的无限游戏
A
About on SuperTechFans
Stack Overflow Blog
Stack Overflow Blog
The Cloudflare Blog
雷峰网
雷峰网
P
Proofpoint News Feed
腾讯CDC
H
Help Net Security
V
Visual Studio Blog
美团技术团队
F
Fortinet All Blogs
MongoDB | Blog
MongoDB | Blog
阮一峰的网络日志
阮一峰的网络日志

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
I love MJML — I just didn't want a whole templating engin...
Michal Král · 2026-05-27 · via DEV Community

I love MJML.

Responsive HTML email is one of the genuinely miserable corners of frontend — nested tables, <!--[if mso]> incantations, clients from 2007 that still get a vote. MJML takes all of that and lets me write semantic-ish components (<mj-section>, <mj-column>, <mj-button>) that compile down to email HTML that actually renders everywhere. It's one of those tools that makes a horrible problem boring, which is the highest compliment I can give a library.

So I was happy. Except for one thing.

The one thing I was missing

My templates needed two unglamorous things:

  1. Variables — drop a name, a URL, an order number into the email.
  2. Translations — the same template in en, cs, whatever.

MJML doesn't do either, and fair enough — it's a markup compiler, not a template engine. So like a lot of people, I reached for the template engine I already had and ran my .mjml files through Twig first, just to get this:

<mj-text>{{ 'welcome.hello'|trans({'%name%': name}) }}</mj-text>

And it worked. But look at what that actually is: I'm spinning up an entire templating engine — with loops, conditionals, filters, its own parsing pass — to do string interpolation and a dictionary lookup. Two compile steps (Twig → MJML) for what should be one. A second syntax in my files. And every time I touched a template I had to keep two mental models in sync.

I didn't want a template engine in my email pipeline. I wanted those two tiny things — and nothing else.

What I actually wanted

Co-located translations, vue-i18n style — the strings living right next to the markup that uses them — plus dead-simple variable access. Something like:

<mjml>
  <i18n type="json">
    {
      "en": { "hello": "Hello {name}!", "cta": "Shop now" },
      "cs": { "hello": "Ahoj {name}!", "cta": "Nakupovat" }
    }
  </i18n>

  <mj-body>
    <mj-section>
      <mj-column>
        <mj-text>{{ i18n('hello', { name: get('firstName') }) }}</mj-text>
        <mj-button href="{{ get('url') }}">{{ i18n('cta') }}</mj-button>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

No second template language. No second compile. Just i18n() and get().

So I built it: @checkthiscloud/mjml-i18n.

Using it

It's a single MJML preprocessor:

import mjml from 'mjml';
import { createI18nPreprocessor } from '@checkthiscloud/mjml-i18n';

const preprocessor = createI18nPreprocessor({
  locale: 'cs',
  vars: { firstName: 'Ada', url: 'https://example.com' },
});

const { html } = await mjml(template, { preprocessors: [preprocessor] });

That's the whole integration. One preprocessor in, fully resolved email HTML out.

How it works (and what it deliberately doesn't do)

The trick is that it runs as an MJML preprocessor — it resolves the {{ … }} markers on the raw XML, before MJML parses it. Two nice consequences:

  • Markers work anywhere, including inside attributes (href="{{ get('url') }}"), not just text nodes.
  • It composes with MJML instead of fighting it.

Inside each {{ … }} is a single function-call expression, evaluated by a tiny allowlist (built on jsep). Only literals, object arguments, and the registered functions (i18n, get) are allowed. You can nest them — i18n('hello', { name: get('firstName') }) — and that's it.

What it is not is a template engine. No loops, no conditionals, no arbitrary expressions, no eval. If a marker isn't a recognized call, it's left untouched rather than executed. That constraint is the whole point: I wanted the two things I was missing, not a Turing-complete language living in my emails.

Translations fall back gracefully (missing key → the key, missing variable → an obvious placeholder), so a typo degrades instead of exploding mid-render.

Status

It's young — 0.x, MIT, built for MJML v5 — but small and focused, and it does exactly what it says. Params are currently plain {name} substitution (no ICU plurals yet, though the seam is there if that day comes). If you live in MJML and you've been bolting a template engine onto it just for translations, give it a spin and tell me where it breaks.

npm install @checkthiscloud/mjml-i18n mjml

Sometimes the feature you want isn't "more" — it's exactly two small things and nothing else.