@seliseblocks/mailcraft 0.2.16 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/DOCS.md +70 -67
- package/README.md +8 -8
- package/README.md.txt +8 -8
- package/dist/mailcraft-editor.bundle.js +84 -76
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/examples/templates/activate-your-account.html +1 -1
- package/examples/templates/back-in-stock.html +4 -4
- package/examples/templates/cart-left-behind.html +3 -3
- package/examples/templates/community-giveaway.html +3 -3
- package/examples/templates/frontend-futures-invite.html +3 -3
- package/examples/templates/give-25-get-25.html +2 -2
- package/examples/templates/invoice-paid.html +3 -3
- package/examples/templates/meet-nova-launch.html +7 -7
- package/examples/templates/mega-weekend-sale.html +1 -1
- package/examples/templates/order-confirmed.html +6 -6
- package/examples/templates/rate-your-headphones.html +3 -3
- package/examples/templates/reset-your-password.html +1 -1
- package/examples/templates/thankyou-promo-code.html +3 -3
- package/examples/templates/the-sunday-brief.html +4 -4
- package/examples/templates/welcome-to-your-workspace.html +4 -4
- package/examples/templates/your-password-was-changed.html +2 -2
- package/examples/templates/your-signin-code.html +1 -1
- package/examples/vanilla.html +63 -63
- package/package.json +1 -1
- package/src/core/binder.js +10 -0
- package/src/core/blocks.js +2 -2
- package/src/core/editor-core.js +195 -6
- package/src/core/export.js +311 -22
- package/src/core/i18n/ar.js +2 -0
- package/src/core/i18n/bg.js +2 -0
- package/src/core/i18n/bn.js +2 -0
- package/src/core/i18n/ca.js +2 -0
- package/src/core/i18n/cs.js +2 -0
- package/src/core/i18n/da.js +2 -0
- package/src/core/i18n/de-CH.js +2 -0
- package/src/core/i18n/de.js +2 -0
- package/src/core/i18n/dz.js +2 -0
- package/src/core/i18n/el.js +2 -0
- package/src/core/i18n/en.js +2 -0
- package/src/core/i18n/es.js +2 -0
- package/src/core/i18n/et.js +2 -0
- package/src/core/i18n/fi.js +2 -0
- package/src/core/i18n/fr.js +2 -0
- package/src/core/i18n/hr.js +2 -0
- package/src/core/i18n/hu.js +2 -0
- package/src/core/i18n/it.js +2 -0
- package/src/core/i18n/lt.js +2 -0
- package/src/core/i18n/lv.js +2 -0
- package/src/core/i18n/nb.js +2 -0
- package/src/core/i18n/nl.js +2 -0
- package/src/core/i18n/pl.js +2 -0
- package/src/core/i18n/pt.js +2 -0
- package/src/core/i18n/ro.js +2 -0
- package/src/core/i18n/ru.js +2 -0
- package/src/core/i18n/sk.js +2 -0
- package/src/core/i18n/sl.js +2 -0
- package/src/core/i18n/sv.js +2 -0
- package/src/core/i18n/tr.js +2 -0
- package/src/core/i18n/uk.js +2 -0
- package/src/core/import-html.js +390 -20
- package/src/core/layout-style.js +25 -0
- package/src/core/sanitize.js +4 -1
- package/src/core/theme.js +22 -1
- package/src/mailcraft-editor.js +8 -0
- package/src/render/block-body.js +129 -17
- package/src/render/canvas.js +35 -6
- package/src/render/fields.js +19 -0
- package/src/render/screenshot.js +26 -3
package/src/core/export.js
CHANGED
|
@@ -87,6 +87,36 @@ export function msoHarden(html) {
|
|
|
87
87
|
if (!/line-height:\s*[\d.]+px/.test(out)) return 'style="' + out + '"';
|
|
88
88
|
return 'style="' + out + (out.trim().endsWith(';') ? '' : ';') + 'mso-line-height-rule:exactly;"';
|
|
89
89
|
})
|
|
90
|
+
/*
|
|
91
|
+
* `mso-padding-alt` on the button cell. Word applies a cell's CSS padding
|
|
92
|
+
* inconsistently and ignores it on the anchor entirely, so the pill can
|
|
93
|
+
* shrink to bare text there; `mso-padding-alt` is the declaration it does
|
|
94
|
+
* honour, restating the same two values. Keyed on the exact shape the
|
|
95
|
+
* button renderer writes (block-body.js): a centred cell carrying a
|
|
96
|
+
* two-value padding AND a border-radius. Nothing else in the exporter
|
|
97
|
+
* produces that pair on a <td>.
|
|
98
|
+
*/
|
|
99
|
+
.replace(/<td\b([^>]*)>/g, (m0, attrs) => {
|
|
100
|
+
// The whole tag, not just the style: the renderer writes align="center"
|
|
101
|
+
// AFTER the style attribute.
|
|
102
|
+
if (/mso-padding-alt/.test(attrs) || !/\balign="center"/.test(attrs) || !/\bborder-radius:/.test(attrs)) return m0;
|
|
103
|
+
const pm = attrs.match(/\bpadding:\s*(\d+(?:\.\d+)?px)\s+(\d+(?:\.\d+)?px)\s*;/);
|
|
104
|
+
if (!pm) return m0;
|
|
105
|
+
return '<td' + attrs.replace(/style="([^"]*)"/, (s0, css) => 'style="' + css + (css.trim().endsWith(';') ? '' : ';') + 'mso-padding-alt:' + pm[1] + ' ' + pm[2] + ';"') + '>';
|
|
106
|
+
})
|
|
107
|
+
/*
|
|
108
|
+
* `-ms-interpolation-mode:bicubic` on every image. Outlook's (and old
|
|
109
|
+
* IE's) default is nearest-neighbour, which turns a scaled photo into
|
|
110
|
+
* visible stair-steps; bicubic is the one switch that fixes it. Not a
|
|
111
|
+
* real CSS property, so CSSOM drops it on the canvas -- another string
|
|
112
|
+
* pass job, like the rest of this function.
|
|
113
|
+
*/
|
|
114
|
+
.replace(/<img\b([^>]*)>/g, (m0, attrs) => {
|
|
115
|
+
if (/interpolation-mode/.test(attrs)) return m0;
|
|
116
|
+
return /style="/.test(attrs)
|
|
117
|
+
? '<img' + attrs.replace(/style="([^"]*)"/, (s0, css) => 'style="' + css + (!css.trim() || css.trim().endsWith(';') ? '' : ';') + '-ms-interpolation-mode:bicubic;"') + '>'
|
|
118
|
+
: '<img' + attrs + ' style="-ms-interpolation-mode:bicubic;">';
|
|
119
|
+
})
|
|
90
120
|
.replace(/<table\b([^>]*)>/g, (m0, attrs) => {
|
|
91
121
|
if (/mso-table-lspace/.test(attrs)) return m0;
|
|
92
122
|
const spacing = 'mso-table-lspace:0pt;mso-table-rspace:0pt;';
|
|
@@ -194,6 +224,9 @@ export function buildHtml(state, root, boxCss, opts) {
|
|
|
194
224
|
* per block with no deduplication.
|
|
195
225
|
*/
|
|
196
226
|
const need = { stack: false, twoUp: false, reverse: false, hideM: false, hideD: false };
|
|
227
|
+
// Set by the first row that emits a `v:rect`; decides whether the VML
|
|
228
|
+
// namespace is declared on <html> (see the msoHead comment below).
|
|
229
|
+
let usedVml = false;
|
|
197
230
|
/**
|
|
198
231
|
* One decision per row, made once: what goes on the row box (`<tr>`, or the
|
|
199
232
|
* flex/grid wrapper) and what goes on each cell.
|
|
@@ -254,15 +287,84 @@ export function buildHtml(state, root, boxCss, opts) {
|
|
|
254
287
|
if (b.type === 'condition' || b.type === 'loop') return logic.emit.get(b.id) || '';
|
|
255
288
|
return '<div' + marker(b) + visClass(b.props) + ' style="' + boxCss(b.props) + '">' + grab(b.id) + '</div>';
|
|
256
289
|
}).filter(Boolean).join('\n ') || ' ';
|
|
257
|
-
|
|
290
|
+
/*
|
|
291
|
+
* A COLUMN's own background image -- the photo behind one half of a
|
|
292
|
+
* two-up row, which until now only the whole row could carry.
|
|
293
|
+
*
|
|
294
|
+
* Everything the row's image does (longhands not the shorthand, a solid
|
|
295
|
+
* rgba tint rather than a gradient layer, VML for Word) it does here for
|
|
296
|
+
* the same reasons, spelled out at the row's `bgUrl` below. What differs
|
|
297
|
+
* is where it lands: the painted element is the wrapper <div>, never the
|
|
298
|
+
* <td>, because the cell also carries the gutter padding and an image on
|
|
299
|
+
* it would run under the gap between the columns. That also rules out
|
|
300
|
+
* the `background=`/`bgcolor=` attribute pair -- those exist only on the
|
|
301
|
+
* cell -- so a column image is CSS plus VML, exactly like the row's
|
|
302
|
+
* `boxed` branch.
|
|
303
|
+
*/
|
|
304
|
+
const colPadPx = (c) => (c.padY || 0) + 'px ' + (c.padX || 0) + 'px';
|
|
305
|
+
/** The column's own pixel width, for `v:rect` (which has no percentages). Content width less the row's side padding, split by span, less the gutter this cell gives up. */
|
|
306
|
+
const colPxOf = (c) => {
|
|
307
|
+
const padL = rp.pl ?? rp.px ?? 0;
|
|
308
|
+
const padR = rp.pr ?? rp.px ?? 0;
|
|
309
|
+
const innerPx = Math.max(20, (Number(t.width) || 620) - padL - padR);
|
|
310
|
+
return Math.max(20, Math.round(innerPx * ((c.span || 100) / 100)) - (rp.gap || 0));
|
|
311
|
+
};
|
|
312
|
+
/*
|
|
313
|
+
* One column's content inside its own paint.
|
|
314
|
+
*
|
|
315
|
+
* Used by all three layouts. The table path passes `allowVml` because a
|
|
316
|
+
* `<td>`'s pixel width is well defined there; a grid column is an equal
|
|
317
|
+
* fraction and a flex column can wrap, so neither offers the fixed
|
|
318
|
+
* number `v:rect` demands and both stay CSS-only -- Word still gets the
|
|
319
|
+
* ghost-cell table those layouts already emit.
|
|
320
|
+
*
|
|
321
|
+
* Before this was shared, the flex and grid branches called `colInner`
|
|
322
|
+
* raw: the canvas painted a column's background, border, radius and
|
|
323
|
+
* padding (render/canvas.js builds the same `host` wrapper for every
|
|
324
|
+
* layout) and the export dropped all four, so a two-card flex row was
|
|
325
|
+
* designed as cards and arrived as plain text.
|
|
326
|
+
*/
|
|
327
|
+
const colPainted = (c, allowVml) => {
|
|
258
328
|
// Column-level styling (bg/radius/inner padding) renders as a wrapper
|
|
259
329
|
// <div> inside the cell so the gutter padding stays unpainted --
|
|
260
330
|
// mirrors render/canvas.js's `host` wrapper.
|
|
261
|
-
const
|
|
262
|
-
|
|
331
|
+
const cUrl = c.bgImage ? cssUrl(c.bgImage) : '';
|
|
332
|
+
const cPaint = cUrl
|
|
333
|
+
? 'background-color:' + (c.bg || 'transparent') + ';'
|
|
334
|
+
+ 'background-image:url(' + attrEsc(cUrl) + ');'
|
|
335
|
+
+ 'background-size:' + (c.bgSize || 'cover') + ';'
|
|
336
|
+
+ 'background-position:' + (c.bgPos || 'center') + ';'
|
|
337
|
+
+ 'background-repeat:' + (c.bgRepeat || 'no-repeat') + ';'
|
|
338
|
+
: 'background:' + (c.bg || 'transparent') + ';';
|
|
339
|
+
const cOv = cUrl && c.overlay ? c.overlay / 100 : 0;
|
|
340
|
+
// The tint carries the padding for the same reason the row's does: the
|
|
341
|
+
// band a reader sees is the padded box, so a tint sized to the content
|
|
342
|
+
// would leave an untinted ring of bare photo around it.
|
|
343
|
+
const cBody = cOv
|
|
344
|
+
? '<div style="background-color:rgba(20,22,24,' + cOv + ');padding:' + colPadPx(c) + ';">\n ' + colInner(c) + '\n </div>'
|
|
345
|
+
: colInner(c);
|
|
346
|
+
/*
|
|
347
|
+
* Word gets the column photo through VML -- but only where the ROW is
|
|
348
|
+
* not already painting one. Nesting a `v:rect` inside another shape's
|
|
349
|
+
* `v:textbox` is where Word's layout stops being predictable, and a
|
|
350
|
+
* row image plus a column image is a design that has already decided
|
|
351
|
+
* which one it wants on top. Every other client renders both.
|
|
352
|
+
*/
|
|
353
|
+
let cPainted = cBody;
|
|
354
|
+
if (cUrl && allowVml && !rp.bgImage) {
|
|
355
|
+
usedVml = true;
|
|
356
|
+
const h = Math.max(120, (c.padY || 0) * 2 + Math.max(1, c.blocks.length) * 90);
|
|
357
|
+
cPainted = '<!--[if gte mso 9]><v:rect fill="true" stroke="false" style="width:' + colPxOf(c) + 'px;height:' + h + 'px;">'
|
|
358
|
+
+ '<v:fill type="frame" src="' + attrEsc(cUrl) + '" color="' + (c.bg || '#ffffff') + '" />'
|
|
359
|
+
+ '<v:textbox inset="0,0,0,0" style="mso-fit-shape-to-text:true"><![endif]-->'
|
|
360
|
+
+ '\n ' + cBody
|
|
361
|
+
+ '\n <!--[if gte mso 9]></v:textbox></v:rect><![endif]-->';
|
|
362
|
+
}
|
|
363
|
+
return (c.bg || c.bgImage || c.border || c.radius || c.padY || c.padX)
|
|
364
|
+
? '<div style="' + cPaint + (c.border ? 'border:' + c.border + 'px ' + (c.borderStyle || 'solid') + ' ' + (c.lineColor || '#e2e2e5') + ';' : '') + 'border-radius:' + (c.radius || 0) + 'px;padding:' + (cOv ? '0px 0px' : colPadPx(c)) + '">\n ' + cPainted + '\n </div>'
|
|
263
365
|
: colInner(c);
|
|
264
|
-
|
|
265
|
-
|
|
366
|
+
};
|
|
367
|
+
const cells = r.cols.map((c) => '<td' + (plan.cell ? ' class="' + plan.cell + '"' : '') + ' width="' + c.span + '%" valign="' + rp.valign + '" style="padding:0 ' + Math.round(rp.gap / 2) + 'px;">\n ' + colPainted(c, true) + '\n </td>').join('\n ');
|
|
266
368
|
// The CSS-layout rows reach the same behaviour through their wrapper: one
|
|
267
369
|
// class on the flex/grid container, so the markup stays exactly as it was
|
|
268
370
|
// for every wide client.
|
|
@@ -271,20 +373,79 @@ export function buildHtml(state, root, boxCss, opts) {
|
|
|
271
373
|
const mcr = markers && rp.layout && rp.layout !== 'columns'
|
|
272
374
|
? ' data-mcr="' + attrEsc(JSON.stringify({ layout: rp.layout, flexDir: rp.flexDir || 'row', justify: rp.justify || 'flex-start', alignItems: rp.alignItems || 'stretch', wrap: rp.wrap !== false, gridCols: rp.gridCols || 2, gap: rp.gap || 0, spans: r.cols.map((c) => c.span) })) + '"'
|
|
273
375
|
: '';
|
|
376
|
+
/*
|
|
377
|
+
* Flex and grid rows ship as the div they are -- and Word, which knows
|
|
378
|
+
* neither, sees block children and stacks them. The ghost cells fix that
|
|
379
|
+
* without duplicating any content: each child is wrapped in an MSO-only
|
|
380
|
+
* <td>, so Word lays the same children out in a table row while every
|
|
381
|
+
* other client skips the conditional comments and gets the flex/grid.
|
|
382
|
+
* Grid breaks into a new ghost <tr> every `gridCols` children; a
|
|
383
|
+
* column-direction flex stacks (one child per ghost row), which is what
|
|
384
|
+
* it does everywhere else too. Comments are inert to the importer, so
|
|
385
|
+
* the round trip through `data-mcr` is unchanged.
|
|
386
|
+
*/
|
|
387
|
+
const ghostCells = (children, widths, perRow) => {
|
|
388
|
+
let out = '<!--[if mso]><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><![endif]-->';
|
|
389
|
+
children.forEach((h, i) => {
|
|
390
|
+
if (i > 0 && i % perRow === 0) out += '<!--[if mso]></tr><tr><![endif]-->';
|
|
391
|
+
out += '<!--[if mso]><td width="' + widths[i] + '%" valign="top"><![endif]-->' + h + '<!--[if mso]></td><![endif]-->';
|
|
392
|
+
});
|
|
393
|
+
return out + '<!--[if mso]></tr></table><![endif]-->';
|
|
394
|
+
};
|
|
395
|
+
const gridN = Math.max(1, rp.gridCols || 2);
|
|
396
|
+
const stacksVertically = /column/.test(rp.flexDir || 'row');
|
|
397
|
+
const spanSum = r.cols.reduce((a, c) => a + (c.span || 0), 0) || 100;
|
|
274
398
|
const cssBody = rp.layout === 'grid'
|
|
275
|
-
? '<div' + mcr + stackWrap + ' style="display:grid;grid-template-columns:repeat(' +
|
|
276
|
-
|
|
399
|
+
? '<div' + mcr + stackWrap + ' style="display:grid;grid-template-columns:repeat(' + gridN + ',minmax(0,1fr));gap:' + rp.gap + 'px">\n '
|
|
400
|
+
+ ghostCells(r.cols.map((c) => '<div>' + colPainted(c, false) + '</div>'), r.cols.map(() => Math.floor(100 / gridN)), gridN)
|
|
401
|
+
+ '\n </div>'
|
|
402
|
+
: '<div' + mcr + stackWrap + ' style="display:flex;flex-direction:' + (rp.flexDir || 'row') + ';justify-content:' + (rp.justify || 'flex-start') + ';align-items:' + (rp.alignItems || 'stretch') + ';flex-wrap:' + (rp.wrap ? 'wrap' : 'nowrap') + ';gap:' + rp.gap + 'px">\n '
|
|
403
|
+
+ ghostCells(r.cols.map((c) => '<div style="flex:' + c.span + ' 1 auto;min-width:0">' + colPainted(c, false) + '</div>'),
|
|
404
|
+
r.cols.map((c) => (stacksVertically ? 100 : Math.round(((c.span || 0) / spanSum) * 100))), stacksVertically ? 1 : r.cols.length)
|
|
405
|
+
+ '\n </div>';
|
|
277
406
|
const body = rp.layout && rp.layout !== 'columns'
|
|
278
407
|
? cssBody
|
|
279
408
|
// The `<tr>` is what becomes the flex container for two-up and reverse;
|
|
280
409
|
// in the default one-up stack it carries no class at all, exactly as before.
|
|
281
410
|
: '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr' + (plan.row ? ' class="' + plan.row + '"' : '') + '>\n ' + cells + '\n </tr></table>';
|
|
282
|
-
|
|
283
|
-
|
|
411
|
+
/*
|
|
412
|
+
* A row's background image.
|
|
413
|
+
*
|
|
414
|
+
* Emitted as LONGHANDS, never the `background` shorthand, and never as a
|
|
415
|
+
* layered value. The old shape put the tint in front of the photo in one
|
|
416
|
+
* declaration --
|
|
417
|
+
* background-image:linear-gradient(rgba(20,22,24,.46),...),url("...")
|
|
418
|
+
* -- and outlook.com's sanitiser (which is what New Outlook for Windows
|
|
419
|
+
* and Outlook on the web both run) drops a declaration it cannot fully
|
|
420
|
+
* parse rather than salvaging the layers it understands. A comma list
|
|
421
|
+
* opening with `linear-gradient(...)` is exactly that, so the tint took
|
|
422
|
+
* the photo down with it and the row fell back to flat colour. The tint
|
|
423
|
+
* is now a solid rgba wrapper (`tinted` below), which every client that
|
|
424
|
+
* paints the image at all also paints.
|
|
425
|
+
*
|
|
426
|
+
* `background=` and `bgcolor=` restate the same two values as HTML
|
|
427
|
+
* attributes. They cost nothing, they outlive sanitisers that drop CSS
|
|
428
|
+
* wholesale, and the importer already reads `background` back
|
|
429
|
+
* (import-html.js `bgImageOf`). They can only ride the `<td>`: in the
|
|
430
|
+
* boxed branch the painted element is a `<div>`, where the attributes do
|
|
431
|
+
* not exist, so that branch stays CSS-only.
|
|
432
|
+
*
|
|
433
|
+
* The URL is unquoted. `cssUrl` percent-encodes quotes, parens, spaces
|
|
434
|
+
* and backslashes, so there is nothing left for a bare `url(...)` to trip
|
|
435
|
+
* over, and one less thing for a sanitiser to re-serialise wrongly.
|
|
436
|
+
*/
|
|
437
|
+
const bgUrl = rp.bgImage ? cssUrl(rp.bgImage) : '';
|
|
438
|
+
const bgColor = rp.bg || t.contentBg || 'transparent';
|
|
439
|
+
const tdBg = bgUrl
|
|
440
|
+
? 'background-color:' + bgColor + ';'
|
|
441
|
+
+ 'background-image:url(' + attrEsc(bgUrl) + ');'
|
|
442
|
+
+ 'background-size:' + (rp.bgSize || 'cover') + ';'
|
|
443
|
+
+ 'background-position:' + (rp.bgPos || 'center') + ';'
|
|
444
|
+
+ 'background-repeat:' + (rp.bgRepeat || 'no-repeat') + ';'
|
|
284
445
|
// Falls all the way through to `transparent`: a row inherits the
|
|
285
446
|
// content column's background, and that column may itself be unpainted
|
|
286
447
|
// now that it can be -- 'background:;' is not a declaration.
|
|
287
|
-
: 'background:' +
|
|
448
|
+
: 'background:' + bgColor + ';';
|
|
288
449
|
const tdBorder = rowBorderCss(rp)
|
|
289
450
|
+ (rp.radius ? 'border-radius:' + rp.radius + 'px;' : '')
|
|
290
451
|
+ (rp.shadow ? 'box-shadow:' + rp.shadow + ';' : '');
|
|
@@ -296,13 +457,61 @@ export function buildHtml(state, root, boxCss, opts) {
|
|
|
296
457
|
// the margin sits outside the painted box, exactly as the canvas draws
|
|
297
458
|
// it. Rows using neither keep the old markup byte for byte.
|
|
298
459
|
const boxed = rp.mt || rp.mr || rp.mb || rp.ml || rp.my || (rp.maxW && rp.maxW < 100);
|
|
460
|
+
/*
|
|
461
|
+
* The tint, as its own box. It has to carry the row's padding: the band a
|
|
462
|
+
* reader sees is the padded box, so a tint sized to the content alone
|
|
463
|
+
* would leave an untinted ring of bare photo around it. The painted
|
|
464
|
+
* element gives its padding up in exchange (`ownPad` below).
|
|
465
|
+
*
|
|
466
|
+
* `rgba()` is understood by every client that renders the image in the
|
|
467
|
+
* first place. Classic Outlook understands neither the CSS background nor
|
|
468
|
+
* rgba, and gets the photo through VML with no tint over it -- the one
|
|
469
|
+
* place the two paths differ.
|
|
470
|
+
*/
|
|
471
|
+
const ov = bgUrl && rp.overlay ? rp.overlay / 100 : 0;
|
|
472
|
+
const ownPad = ov ? 'padding:0;' : 'padding:' + rowPad(rp) + ';';
|
|
473
|
+
const tinted = ov
|
|
474
|
+
? '<div style="background-color:rgba(20,22,24,' + ov + ');padding:' + rowPad(rp) + ';">\n ' + body + '\n </div>'
|
|
475
|
+
: body;
|
|
476
|
+
/*
|
|
477
|
+
* VML, for the one engine that has never read a CSS background: Word,
|
|
478
|
+
* behind Outlook 2007-2021 and Classic Outlook for Microsoft 365. New
|
|
479
|
+
* Outlook skips conditional comments entirely, so none of this reaches
|
|
480
|
+
* the client this fix was written for -- it is the Classic half of the
|
|
481
|
+
* same feature, and it is free to carry.
|
|
482
|
+
*
|
|
483
|
+
* `v:rect` needs pixel dimensions, and a row's height is content-driven.
|
|
484
|
+
* The height here is therefore an ESTIMATE (padding plus a nominal 90px a
|
|
485
|
+
* block), which `mso-fit-shape-to-text` then treats as a floor rather
|
|
486
|
+
* than a clip: Word grows the shape to whatever the content actually
|
|
487
|
+
* needs. A row whose content is shorter than the estimate paints a taller
|
|
488
|
+
* photo band in Classic Outlook than elsewhere; that is the residual cost
|
|
489
|
+
* of not asking the author for a height.
|
|
490
|
+
*/
|
|
491
|
+
const vmlWidth = boxed && rp.maxW && rp.maxW < 100 ? Math.round(t.width * (rp.maxW / 100)) : t.width;
|
|
492
|
+
const vmlOpen = () => {
|
|
493
|
+
usedVml = true;
|
|
494
|
+
const padT = rp.pt ?? rp.py ?? 0;
|
|
495
|
+
const padB = rp.pb ?? rp.py ?? 0;
|
|
496
|
+
const blockCount = r.cols.reduce((a, c) => a + c.blocks.length, 0);
|
|
497
|
+
const h = Math.max(120, padT + padB + Math.max(1, blockCount) * 90);
|
|
498
|
+
return '<!--[if gte mso 9]><v:rect fill="true" stroke="false" style="width:' + vmlWidth + 'px;height:' + h + 'px;">'
|
|
499
|
+
+ '<v:fill type="frame" src="' + attrEsc(bgUrl) + '" color="' + (bgColor === 'transparent' ? '#ffffff' : bgColor) + '" />'
|
|
500
|
+
+ '<v:textbox inset="0,0,0,0" style="mso-fit-shape-to-text:true"><![endif]-->';
|
|
501
|
+
};
|
|
502
|
+
const painted = bgUrl
|
|
503
|
+
? vmlOpen() + '\n ' + tinted + '\n <!--[if gte mso 9]></v:textbox></v:rect><![endif]-->'
|
|
504
|
+
: tinted;
|
|
299
505
|
if (boxed) {
|
|
300
506
|
const cap = rp.maxW && rp.maxW < 100 ? 'max-width:' + rp.maxW + '%;' : '';
|
|
301
507
|
// centerEmpty only when capped: an uncapped box spans the cell anyway,
|
|
302
508
|
// and `auto` there would only read back as a lost slider value.
|
|
303
|
-
return ' <tr>\n <td style="padding:0;">\n <div style="' + cap + 'margin:' + rowMargin(rp, !!cap) + ';
|
|
509
|
+
return ' <tr>\n <td style="padding:0;">\n <div style="' + cap + 'margin:' + rowMargin(rp, !!cap) + ';' + ownPad + tdBg + tdBorder + '">\n ' + painted + '\n </div>\n </td>\n </tr>';
|
|
304
510
|
}
|
|
305
|
-
|
|
511
|
+
const tdAttrs = bgUrl
|
|
512
|
+
? ' background="' + attrEsc(bgUrl) + '"' + (bgColor === 'transparent' ? '' : ' bgcolor="' + bgColor + '"')
|
|
513
|
+
: '';
|
|
514
|
+
return ' <tr>\n <td' + tdAttrs + ' style="' + ownPad + tdBg + tdBorder + '">\n ' + painted + '\n </td>\n </tr>';
|
|
306
515
|
}).join('\n') + (logic.tail ? '\n ' + logic.tail : '');
|
|
307
516
|
// The page section -- the full-width area the content column sits on. Its
|
|
308
517
|
// padding is the band a mail client shows around the template, and `radius`
|
|
@@ -343,7 +552,36 @@ export function buildHtml(state, root, boxCss, opts) {
|
|
|
343
552
|
*/
|
|
344
553
|
const ghostOpen = '<!--[if mso]><table role="presentation" width="' + t.width + '" cellpadding="0" cellspacing="0" border="0"><tr><td><![endif]-->';
|
|
345
554
|
const ghostClose = '<!--[if mso]></td></tr></table><![endif]-->';
|
|
346
|
-
|
|
555
|
+
/*
|
|
556
|
+
* The content column's own paint. A background image here follows exactly
|
|
557
|
+
* the rules a row's does (see `tdBg` above): separate longhands, never the
|
|
558
|
+
* shorthand and never a layered value, an unquoted url, and the same two
|
|
559
|
+
* values repeated as `background=`/`bgcolor=` attributes for anything that
|
|
560
|
+
* drops CSS wholesale.
|
|
561
|
+
*
|
|
562
|
+
* This is the content column and not the page for one reason: it is already
|
|
563
|
+
* a <table>, the one element every client paints a background on. A page
|
|
564
|
+
* background has to ride <body>, and Gmail discards the body element
|
|
565
|
+
* outright -- so a page-level image is simply absent there, while this one
|
|
566
|
+
* renders everywhere `background-image` renders at all.
|
|
567
|
+
*
|
|
568
|
+
* No VML: `v:rect` needs a pixel height, and the content column's height is
|
|
569
|
+
* the whole email's. Classic Outlook gets `contentBg` flat, which is the
|
|
570
|
+
* same deal it has always had here.
|
|
571
|
+
*/
|
|
572
|
+
const contentBgUrl = t.contentBgImage ? cssUrl(t.contentBgImage) : '';
|
|
573
|
+
const contentColor = t.contentBg || 'transparent';
|
|
574
|
+
const contentPaint = contentBgUrl
|
|
575
|
+
? 'background-color:' + contentColor + ';'
|
|
576
|
+
+ 'background-image:url(' + attrEsc(contentBgUrl) + ');'
|
|
577
|
+
+ 'background-size:' + (t.contentBgSize || 'cover') + ';'
|
|
578
|
+
+ 'background-position:' + (t.contentBgPos || 'center') + ';'
|
|
579
|
+
+ 'background-repeat:' + (t.contentBgRepeat || 'no-repeat') + ';'
|
|
580
|
+
: 'background:' + contentColor + ';';
|
|
581
|
+
const contentAttrs = contentBgUrl
|
|
582
|
+
? ' background="' + attrEsc(contentBgUrl) + '"' + (contentColor === 'transparent' ? '' : ' bgcolor="' + contentColor + '"')
|
|
583
|
+
: '';
|
|
584
|
+
const shell = '<table role="presentation"' + contentAttrs + ' width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:' + t.width + 'px;' + contentPaint + contentShape + '">\n' + rows + '\n </table>';
|
|
347
585
|
/*
|
|
348
586
|
* The one embedded stylesheet in the document, and the only place the
|
|
349
587
|
* exporter is not inline-styled -- a media query cannot be expressed
|
|
@@ -397,13 +635,17 @@ export function buildHtml(state, root, boxCss, opts) {
|
|
|
397
635
|
+ ' img { max-width:100% !important; height:auto !important; }\n'
|
|
398
636
|
+ '}\n</style>';
|
|
399
637
|
/*
|
|
400
|
-
* `xmlns:o` earns its place; `xmlns:v`
|
|
401
|
-
* what makes `<o:OfficeDocumentSettings>` parse, and
|
|
402
|
-
* a live bug fix rather than a legacy one: on a
|
|
403
|
-
* Outlook renders at 120dpi and scales the whole
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
638
|
+
* `xmlns:o` always earns its place; `xmlns:v` earns it conditionally. The
|
|
639
|
+
* Office namespace is what makes `<o:OfficeDocumentSettings>` parse, and
|
|
640
|
+
* `PixelsPerInch` 96 is a live bug fix rather than a legacy one: on a
|
|
641
|
+
* high-DPI Windows display Outlook renders at 120dpi and scales the whole
|
|
642
|
+
* template about 25% larger than authored.
|
|
643
|
+
*
|
|
644
|
+
* VML's namespace ships only when a row actually emitted a `v:rect` for its
|
|
645
|
+
* background image. The old rule here -- "declaring a namespace for markup
|
|
646
|
+
* that never appears is noise in every other client" -- is unchanged; it is
|
|
647
|
+
* just that the markup now sometimes appears, so the namespace follows it
|
|
648
|
+
* rather than being absent on principle or present on every send.
|
|
407
649
|
*/
|
|
408
650
|
const msoHead = '\n<!--[if mso]>\n<xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml>\n<![endif]-->';
|
|
409
651
|
// `text-size-adjust` at 100%, never `none`: both stop a mobile client
|
|
@@ -429,6 +671,53 @@ export function buildHtml(state, root, boxCss, opts) {
|
|
|
429
671
|
return '<a' + attrs + ' style="color:' + t.link + ';">';
|
|
430
672
|
});
|
|
431
673
|
};
|
|
432
|
-
|
|
433
|
-
|
|
674
|
+
/*
|
|
675
|
+
* The preview line. Hidden by every trick the clients between them need
|
|
676
|
+
* (display:none for most, mso-hide for Word, zero size/opacity for the ones
|
|
677
|
+
* that ignore display), and padded out with zero-width joiners so a client
|
|
678
|
+
* that shows ~90 characters does not run on into the body copy after a
|
|
679
|
+
* short preheader. Escaped: it is author text in a markup context.
|
|
680
|
+
*/
|
|
681
|
+
const preText = String(t.preheader || '').trim();
|
|
682
|
+
const preheader = preText
|
|
683
|
+
? '\n<div style="display:none;font-size:1px;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;mso-hide:all;visibility:hidden;">'
|
|
684
|
+
+ preText.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
685
|
+
+ '͏‌ '.repeat(40) + '</div>'
|
|
686
|
+
: '';
|
|
687
|
+
const docDir = t.dir === 'rtl' ? ' dir="rtl"' : '';
|
|
688
|
+
// `bgcolor` beside the CSS on both page wrappers, as the rows already carry:
|
|
689
|
+
// the attribute is what survives a client that drops the style attribute.
|
|
690
|
+
/*
|
|
691
|
+
* The page's paint, on <body> AND the full-width wrapper table. Same rules
|
|
692
|
+
* as every other background here: longhands, unquoted url, the two values
|
|
693
|
+
* repeated as attributes. The wrapper table is what makes an image render
|
|
694
|
+
* in Gmail, which discards the body element outright.
|
|
695
|
+
*/
|
|
696
|
+
const pageBgUrl = t.bgImage ? cssUrl(t.bgImage) : '';
|
|
697
|
+
const pagePaint = pageBgUrl
|
|
698
|
+
? 'background-color:' + pageBg + ';'
|
|
699
|
+
+ 'background-image:url(' + attrEsc(pageBgUrl) + ');'
|
|
700
|
+
+ 'background-size:' + (t.bgSize || 'cover') + ';'
|
|
701
|
+
+ 'background-position:' + (t.bgPos || 'center') + ';'
|
|
702
|
+
+ 'background-repeat:' + (t.bgRepeat || 'no-repeat') + ';'
|
|
703
|
+
: 'background:' + pageBg + ';';
|
|
704
|
+
const pageAttr = (pageBg && pageBg !== 'transparent' && !/^rgba\(/.test(pageBg) ? ' bgcolor="' + pageBg + '"' : '')
|
|
705
|
+
+ (pageBgUrl ? ' background="' + attrEsc(pageBgUrl) + '"' : '');
|
|
706
|
+
/*
|
|
707
|
+
* The page image for Word, which reads neither the attribute nor the CSS.
|
|
708
|
+
* `v:background` is Word's own element for exactly this, and unlike the
|
|
709
|
+
* `v:rect` a row needs it takes NO dimensions -- it fills the page -- so
|
|
710
|
+
* there is no height to estimate. `frame` scales one copy to the page
|
|
711
|
+
* (the CSS `cover`); `tile` repeats it. First thing in the body, where
|
|
712
|
+
* Word expects it; every other client skips the conditional.
|
|
713
|
+
*/
|
|
714
|
+
let pageVml = '';
|
|
715
|
+
if (pageBgUrl) {
|
|
716
|
+
usedVml = true;
|
|
717
|
+
const tile = (t.bgRepeat || 'no-repeat') !== 'no-repeat';
|
|
718
|
+
pageVml = '\n<!--[if gte mso 9]><v:background fill="true"><v:fill type="' + (tile ? 'tile' : 'frame') + '" src="' + attrEsc(pageBgUrl) + '"'
|
|
719
|
+
+ (pageBg && pageBg !== 'transparent' && !/^rgba\(/.test(pageBg) ? ' color="' + pageBg + '"' : '') + ' /></v:background><![endif]-->';
|
|
720
|
+
}
|
|
721
|
+
const bodyStyle = 'margin:0;padding:0;' + pagePaint + 'font-family:' + t.font.replace(/"/g, "'") + ';color:' + t.text + ';-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:100%;text-size-adjust:100%;';
|
|
722
|
+
return msoHarden(stampLinks('<!doctype html>\n<html lang="en"' + docDir + ' xmlns:o="urn:schemas-microsoft-com:office:office"' + (usedVml ? ' xmlns:v="urn:schemas-microsoft-com:vml"' : '') + '>\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width,initial-scale=1">\n<meta name="color-scheme" content="light">\n<meta name="supported-color-schemes" content="light">\n<title>' + 'Email' + '</title>' + msoHead + stackCss + '\n</head>\n<body' + pageAttr + ' style="' + bodyStyle + '">' + pageVml + preheader + '\n<table role="presentation"' + pageAttr + ' width="100%" cellpadding="0" cellspacing="0" border="0" style="' + pagePaint + '">\n <tr><td align="center" style="padding:' + pagePad + ';">\n ' + ghostOpen + '\n ' + shell + '\n ' + ghostClose + '\n </td></tr>\n</table>\n</body>\n</html>'));
|
|
434
723
|
}
|
package/src/core/i18n/ar.js
CHANGED
|
@@ -193,6 +193,8 @@ export const AR = {
|
|
|
193
193
|
'toast.imageReplaced': 'تم استبدال الصورة',
|
|
194
194
|
'toast.imageAdded': 'تمت إضافة الصورة إلى اللوحة',
|
|
195
195
|
'toast.htmlCopied': 'تم نسخ HTML إلى الحافظة',
|
|
196
|
+
'toast.exportImageOne': 'صورة واحدة لن تظهر عند الإرسال — عنصر نائب أو مصدر data:/cid:',
|
|
197
|
+
'toast.exportImageMany': '{count} صور لن تظهر عند الإرسال — عناصر نائبة أو مصادر data:/cid:',
|
|
196
198
|
'toast.templateLoaded': 'تم تحميل {name}',
|
|
197
199
|
'toast.snippetDefaultLabel': 'مقتطف',
|
|
198
200
|
'toast.snippetInserted': 'تم إدراج {name}',
|
package/src/core/i18n/bg.js
CHANGED
|
@@ -170,6 +170,8 @@ export const BG = {
|
|
|
170
170
|
'toast.imageReplaced': 'Изображението е заменено',
|
|
171
171
|
'toast.imageAdded': 'Изображението е добавено към платното',
|
|
172
172
|
'toast.htmlCopied': 'HTML е копиран в клипборда',
|
|
173
|
+
'toast.exportImageOne': '1 изображение няма да се показва при изпращане — заместващ или data:/cid: източник',
|
|
174
|
+
'toast.exportImageMany': '{count} изображения няма да се показват при изпращане — заместващи или data:/cid: източници',
|
|
173
175
|
'toast.templateLoaded': '{name} е зареден',
|
|
174
176
|
'toast.snippetDefaultLabel': 'Откъс',
|
|
175
177
|
'toast.snippetInserted': '{name} е вмъкнат',
|
package/src/core/i18n/bn.js
CHANGED
|
@@ -192,6 +192,8 @@ export const BN = {
|
|
|
192
192
|
'toast.imageReplaced': 'ইমেজ প্রতিস্থাপিত হয়েছে',
|
|
193
193
|
'toast.imageAdded': 'ক্যানভাসে ইমেজ যোগ হয়েছে',
|
|
194
194
|
'toast.htmlCopied': 'HTML ক্লিপবোর্ডে কপি হয়েছে',
|
|
195
|
+
'toast.exportImageOne': '১টি ছবি পাঠানোর পর দেখা যাবে না — প্লেসহোল্ডার বা data:/cid: উৎস',
|
|
196
|
+
'toast.exportImageMany': '{count}টি ছবি পাঠানোর পর দেখা যাবে না — প্লেসহোল্ডার বা data:/cid: উৎস',
|
|
195
197
|
'toast.templateLoaded': '{name} লোড হয়েছে',
|
|
196
198
|
'toast.snippetDefaultLabel': 'স্নিপেট',
|
|
197
199
|
'toast.snippetInserted': '{name} বসানো হয়েছে',
|
package/src/core/i18n/ca.js
CHANGED
|
@@ -170,6 +170,8 @@ export const CA = {
|
|
|
170
170
|
'toast.imageReplaced': 'Imatge substituïda',
|
|
171
171
|
'toast.imageAdded': 'Imatge afegida al llenç',
|
|
172
172
|
'toast.htmlCopied': 'HTML copiat al porta-retalls',
|
|
173
|
+
'toast.exportImageOne': '1 imatge no es mostrarà en enviar-se — origen de marcador de posició o data:/cid:',
|
|
174
|
+
'toast.exportImageMany': '{count} imatges no es mostraran en enviar-se — orígens de marcador de posició o data:/cid:',
|
|
173
175
|
'toast.templateLoaded': '{name} carregat',
|
|
174
176
|
'toast.snippetDefaultLabel': 'Fragment',
|
|
175
177
|
'toast.snippetInserted': '{name} inserit',
|
package/src/core/i18n/cs.js
CHANGED
|
@@ -170,6 +170,8 @@ export const CS = {
|
|
|
170
170
|
'toast.imageReplaced': 'Obrázek nahrazen',
|
|
171
171
|
'toast.imageAdded': 'Obrázek přidán na plátno',
|
|
172
172
|
'toast.htmlCopied': 'HTML zkopírováno do schránky',
|
|
173
|
+
'toast.exportImageOne': '1 obrázek se po odeslání nezobrazí — zástupný nebo data:/cid: zdroj',
|
|
174
|
+
'toast.exportImageMany': '{count} obrázků se po odeslání nezobrazí — zástupné nebo data:/cid: zdroje',
|
|
173
175
|
'toast.templateLoaded': '{name} načteno',
|
|
174
176
|
'toast.snippetDefaultLabel': 'Úryvek',
|
|
175
177
|
'toast.snippetInserted': '{name} vloženo',
|
package/src/core/i18n/da.js
CHANGED
|
@@ -170,6 +170,8 @@ export const DA = {
|
|
|
170
170
|
'toast.imageReplaced': 'Billede erstattet',
|
|
171
171
|
'toast.imageAdded': 'Billede tilføjet til lærredet',
|
|
172
172
|
'toast.htmlCopied': 'HTML kopieret til udklipsholder',
|
|
173
|
+
'toast.exportImageOne': '1 billede vises ikke, når mailen sendes — pladsholder eller data:/cid:-kilde',
|
|
174
|
+
'toast.exportImageMany': '{count} billeder vises ikke, når mailen sendes — pladsholder- eller data:/cid:-kilder',
|
|
173
175
|
'toast.templateLoaded': '{name} indlæst',
|
|
174
176
|
'toast.snippetDefaultLabel': 'Uddrag',
|
|
175
177
|
'toast.snippetInserted': '{name} indsat',
|
package/src/core/i18n/de-CH.js
CHANGED
|
@@ -170,6 +170,8 @@ export const DE_CH = {
|
|
|
170
170
|
'toast.imageReplaced': 'Bild ersetzt',
|
|
171
171
|
'toast.imageAdded': 'Bild zur Canvas hinzugefügt',
|
|
172
172
|
'toast.htmlCopied': 'HTML in die Zwischenablage kopiert',
|
|
173
|
+
'toast.exportImageOne': '1 Bild wird beim Versand nicht angezeigt — Platzhalter oder data:/cid:-Quelle',
|
|
174
|
+
'toast.exportImageMany': '{count} Bilder werden beim Versand nicht angezeigt — Platzhalter- oder data:/cid:-Quellen',
|
|
173
175
|
'toast.templateLoaded': '{name} geladen',
|
|
174
176
|
'toast.snippetDefaultLabel': 'Snippet',
|
|
175
177
|
'toast.snippetInserted': '{name} eingefügt',
|
package/src/core/i18n/de.js
CHANGED
|
@@ -170,6 +170,8 @@ export const DE = {
|
|
|
170
170
|
'toast.imageReplaced': 'Bild ersetzt',
|
|
171
171
|
'toast.imageAdded': 'Bild zur Canvas hinzugefügt',
|
|
172
172
|
'toast.htmlCopied': 'HTML in die Zwischenablage kopiert',
|
|
173
|
+
'toast.exportImageOne': '1 Bild wird beim Versand nicht angezeigt — Platzhalter oder data:/cid:-Quelle',
|
|
174
|
+
'toast.exportImageMany': '{count} Bilder werden beim Versand nicht angezeigt — Platzhalter- oder data:/cid:-Quellen',
|
|
173
175
|
'toast.templateLoaded': '{name} geladen',
|
|
174
176
|
'toast.snippetDefaultLabel': 'Snippet',
|
|
175
177
|
'toast.snippetInserted': '{name} eingefügt',
|
package/src/core/i18n/dz.js
CHANGED
|
@@ -195,6 +195,8 @@ export const DZ = {
|
|
|
195
195
|
'toast.imageReplaced': 'གཟུགས་བརྙན་ཚབ་བཙུགས་བྱུང',
|
|
196
196
|
'toast.imageAdded': 'ཐིག་ཁྲམ་ལུ་གཟུགས་བརྙན་ཁ་སྣོན་བྱུང',
|
|
197
197
|
'toast.htmlCopied': 'HTML ཀ་ལིཔ་བོརཌ་ལུ་འདྲ་བཤུས་བྱུང',
|
|
198
|
+
'toast.exportImageOne': 'པར་ ༡ གཏང་བའི་སྐབས་ མི་སྟོན། — ཚབ་མའི་ ཡང་ན་ data:/cid: འབྱུང་ཁུངས།',
|
|
199
|
+
'toast.exportImageMany': 'པར་ {count} གཏང་བའི་སྐབས་ མི་སྟོན། — ཚབ་མའི་ ཡང་ན་ data:/cid: འབྱུང་ཁུངས།',
|
|
198
200
|
'toast.templateLoaded': '{name} འདྲེན་སྤྱོད་བྱུང',
|
|
199
201
|
'toast.snippetDefaultLabel': 'ཡིག་ཐིག',
|
|
200
202
|
'toast.snippetInserted': '{name} བཙུགས་བྱུང',
|
package/src/core/i18n/el.js
CHANGED
|
@@ -170,6 +170,8 @@ export const EL = {
|
|
|
170
170
|
'toast.imageReplaced': 'Η εικόνα αντικαταστάθηκε',
|
|
171
171
|
'toast.imageAdded': 'Η εικόνα προστέθηκε στον καμβά',
|
|
172
172
|
'toast.htmlCopied': 'Το HTML αντιγράφηκε στο πρόχειρο',
|
|
173
|
+
'toast.exportImageOne': '1 εικόνα δεν θα εμφανιστεί κατά την αποστολή — προσωρινή πηγή ή πηγή data:/cid:',
|
|
174
|
+
'toast.exportImageMany': '{count} εικόνες δεν θα εμφανιστούν κατά την αποστολή — προσωρινές πηγές ή πηγές data:/cid:',
|
|
173
175
|
'toast.templateLoaded': 'Το {name} φορτώθηκε',
|
|
174
176
|
'toast.snippetDefaultLabel': 'Απόσπασμα',
|
|
175
177
|
'toast.snippetInserted': 'Το {name} εισήχθη',
|
package/src/core/i18n/en.js
CHANGED
|
@@ -223,6 +223,8 @@ export const EN = {
|
|
|
223
223
|
'toast.imageReplaced': 'Image replaced',
|
|
224
224
|
'toast.imageAdded': 'Image added to canvas',
|
|
225
225
|
'toast.htmlCopied': 'HTML copied to clipboard',
|
|
226
|
+
'toast.exportImageOne': '1 image will not display when sent — placeholder or data:/cid: source',
|
|
227
|
+
'toast.exportImageMany': '{count} images will not display when sent — placeholder or data:/cid: sources',
|
|
226
228
|
'toast.templateLoaded': '{name} loaded',
|
|
227
229
|
'toast.snippetDefaultLabel': 'Snippet',
|
|
228
230
|
'toast.snippetInserted': '{name} inserted',
|
package/src/core/i18n/es.js
CHANGED
|
@@ -170,6 +170,8 @@ export const ES = {
|
|
|
170
170
|
'toast.imageReplaced': 'Imagen reemplazada',
|
|
171
171
|
'toast.imageAdded': 'Imagen añadida al lienzo',
|
|
172
172
|
'toast.htmlCopied': 'HTML copiado al portapapeles',
|
|
173
|
+
'toast.exportImageOne': '1 imagen no se mostrará al enviarse — origen de marcador de posición o data:/cid:',
|
|
174
|
+
'toast.exportImageMany': '{count} imágenes no se mostrarán al enviarse — orígenes de marcador de posición o data:/cid:',
|
|
173
175
|
'toast.templateLoaded': '{name} cargado',
|
|
174
176
|
'toast.snippetDefaultLabel': 'Fragmento',
|
|
175
177
|
'toast.snippetInserted': '{name} insertado',
|
package/src/core/i18n/et.js
CHANGED
|
@@ -170,6 +170,8 @@ export const ET = {
|
|
|
170
170
|
'toast.imageReplaced': 'Pilt asendatud',
|
|
171
171
|
'toast.imageAdded': 'Pilt lisatud lõuendile',
|
|
172
172
|
'toast.htmlCopied': 'HTML kopeeritud lõikelauale',
|
|
173
|
+
'toast.exportImageOne': '1 pilti ei kuvata saatmisel — kohatäite või data:/cid: allikas',
|
|
174
|
+
'toast.exportImageMany': '{count} pilti ei kuvata saatmisel — kohatäite või data:/cid: allikad',
|
|
173
175
|
'toast.templateLoaded': '{name} laaditud',
|
|
174
176
|
'toast.snippetDefaultLabel': 'Katke',
|
|
175
177
|
'toast.snippetInserted': '{name} lisatud',
|
package/src/core/i18n/fi.js
CHANGED
|
@@ -170,6 +170,8 @@ export const FI = {
|
|
|
170
170
|
'toast.imageReplaced': 'Kuva korvattu',
|
|
171
171
|
'toast.imageAdded': 'Kuva lisätty kankaalle',
|
|
172
172
|
'toast.htmlCopied': 'HTML kopioitu leikepöydälle',
|
|
173
|
+
'toast.exportImageOne': '1 kuva ei näy lähetetyssä viestissä — paikkamerkki- tai data:/cid:-lähde',
|
|
174
|
+
'toast.exportImageMany': '{count} kuvaa ei näy lähetetyssä viestissä — paikkamerkki- tai data:/cid:-lähteet',
|
|
173
175
|
'toast.templateLoaded': '{name} ladattu',
|
|
174
176
|
'toast.snippetDefaultLabel': 'Katkelma',
|
|
175
177
|
'toast.snippetInserted': '{name} lisätty',
|
package/src/core/i18n/fr.js
CHANGED
|
@@ -170,6 +170,8 @@ export const FR = {
|
|
|
170
170
|
'toast.imageReplaced': 'Image remplacée',
|
|
171
171
|
'toast.imageAdded': 'Image ajoutée au canevas',
|
|
172
172
|
'toast.htmlCopied': 'HTML copié dans le presse-papiers',
|
|
173
|
+
'toast.exportImageOne': '1 image ne s’affichera pas à l’envoi — source d’espace réservé ou data:/cid:',
|
|
174
|
+
'toast.exportImageMany': '{count} images ne s’afficheront pas à l’envoi — sources d’espace réservé ou data:/cid:',
|
|
173
175
|
'toast.templateLoaded': '{name} chargé',
|
|
174
176
|
'toast.snippetDefaultLabel': 'Extrait',
|
|
175
177
|
'toast.snippetInserted': '{name} inséré',
|
package/src/core/i18n/hr.js
CHANGED
|
@@ -170,6 +170,8 @@ export const HR = {
|
|
|
170
170
|
'toast.imageReplaced': 'Slika zamijenjena',
|
|
171
171
|
'toast.imageAdded': 'Slika dodana na platno',
|
|
172
172
|
'toast.htmlCopied': 'HTML kopiran u međuspremnik',
|
|
173
|
+
'toast.exportImageOne': '1 slika neće biti prikazana pri slanju — rezervirano mjesto ili data:/cid: izvor',
|
|
174
|
+
'toast.exportImageMany': '{count} slika neće biti prikazano pri slanju — rezervirana mjesta ili data:/cid: izvori',
|
|
173
175
|
'toast.templateLoaded': '{name} učitano',
|
|
174
176
|
'toast.snippetDefaultLabel': 'Isječak',
|
|
175
177
|
'toast.snippetInserted': '{name} umetnuto',
|
package/src/core/i18n/hu.js
CHANGED
|
@@ -170,6 +170,8 @@ export const HU = {
|
|
|
170
170
|
'toast.imageReplaced': 'Kép kicserélve',
|
|
171
171
|
'toast.imageAdded': 'Kép hozzáadva a rajzvászonhoz',
|
|
172
172
|
'toast.htmlCopied': 'HTML a vágólapra másolva',
|
|
173
|
+
'toast.exportImageOne': '1 kép nem jelenik meg az elküldött levélben — helyőrző vagy data:/cid: forrás',
|
|
174
|
+
'toast.exportImageMany': '{count} kép nem jelenik meg az elküldött levélben — helyőrző vagy data:/cid: források',
|
|
173
175
|
'toast.templateLoaded': '{name} betöltve',
|
|
174
176
|
'toast.snippetDefaultLabel': 'Részlet',
|
|
175
177
|
'toast.snippetInserted': '{name} beszúrva',
|
package/src/core/i18n/it.js
CHANGED
|
@@ -170,6 +170,8 @@ export const IT = {
|
|
|
170
170
|
'toast.imageReplaced': 'Immagine sostituita',
|
|
171
171
|
'toast.imageAdded': 'Immagine aggiunta al canvas',
|
|
172
172
|
'toast.htmlCopied': 'HTML copiato negli appunti',
|
|
173
|
+
'toast.exportImageOne': '1 immagine non verrà visualizzata all’invio — origine segnaposto o data:/cid:',
|
|
174
|
+
'toast.exportImageMany': '{count} immagini non verranno visualizzate all’invio — origini segnaposto o data:/cid:',
|
|
173
175
|
'toast.templateLoaded': '{name} caricato',
|
|
174
176
|
'toast.snippetDefaultLabel': 'Snippet',
|
|
175
177
|
'toast.snippetInserted': '{name} inserito',
|
package/src/core/i18n/lt.js
CHANGED
|
@@ -170,6 +170,8 @@ export const LT = {
|
|
|
170
170
|
'toast.imageReplaced': 'Vaizdas pakeistas',
|
|
171
171
|
'toast.imageAdded': 'Vaizdas pridėtas į drobę',
|
|
172
172
|
'toast.htmlCopied': 'HTML nukopijuotas į iškarpinę',
|
|
173
|
+
'toast.exportImageOne': '1 paveikslėlis nebus rodomas išsiuntus — vietos rezervavimo arba data:/cid: šaltinis',
|
|
174
|
+
'toast.exportImageMany': '{count} paveikslėliai nebus rodomi išsiuntus — vietos rezervavimo arba data:/cid: šaltiniai',
|
|
173
175
|
'toast.templateLoaded': '{name} įkeltas',
|
|
174
176
|
'toast.snippetDefaultLabel': 'Iškarpa',
|
|
175
177
|
'toast.snippetInserted': '{name} įterptas',
|