|
16 | 16 | * docs : os validate / os build refuse (docs/duplicate-name), a source boot keeps the last |
17 | 17 | * ``` |
18 | 18 | * |
19 | | - * The identity is what the registrars key an entry by: `name` (25 keys), |
20 | | - * `extend` (`objectExtensions`), and for `views` the object an aggregated |
21 | | - * container binds to (`deriveViewContainerObject`). Out of the table, each |
| 19 | + * The identity is what the registrars key an entry by: `name` (24 keys), |
| 20 | + * `(name, locale)` (`emailTemplates`, whose i18n bundle is one name across |
| 21 | + * several locales), `extend` (`objectExtensions`), and for `views` the object |
| 22 | + * an aggregated container binds to (`deriveViewContainerObject`). Out of the table, each |
22 | 23 | * for its own reason: `plugins`, `agents`, `picklistExtensions`, and `actions` |
23 | 24 | * / `functions`, which have their own checks (`stack-duplicate-action-key`, |
24 | 25 | * `stack-duplicate-function-name`). |
@@ -119,7 +120,14 @@ const BATCH: readonly Row[] = [ |
119 | 120 | byName('datasets', 'Dataset', dataset), |
120 | 121 | byName('flows', 'Flow', flow), |
121 | 122 | byName('jobs', 'Job', { name: 'dupe_job', label: 'Job', schedule: { type: 'interval', intervalMs: 60000 }, body: { language: 'js', source: 'return 1;', capabilities: [] } }), |
122 | | - byName('emailTemplates', 'Email template', { name: 'dupe_mail', label: 'Mail', subject: 'Hi', bodyHtml: '<p>Hello</p>' }), |
| 123 | + { |
| 124 | + // An i18n bundle is one name across several locales: the identity is the |
| 125 | + // pair, an untagged entry keyed as the floor tag `en-US`. |
| 126 | + ...byName('emailTemplates', 'Email template', { name: 'dupe_mail', label: 'Mail', subject: 'Hi', bodyHtml: '<p>Hello</p>' }), |
| 127 | + identity: 'dupe_mail', |
| 128 | + sameIdentity: (e) => ({ ...e, label: 'Second', locale: 'en-US' }), |
| 129 | + subject: "Email template 'dupe_mail' for locale 'en-US'", |
| 130 | + }, |
123 | 131 | byName('books', 'Book', { name: 'dupe_book', label: 'Book', groups: [] }), |
124 | 132 | byName('positions', 'Position', { name: 'dupe_pos', label: 'Pos' }), |
125 | 133 | byName('permissions', 'Permission set', { name: 'dupe_perm', label: 'Perm', objects: {} }), |
@@ -372,6 +380,20 @@ describe('defineStack - CONTROL: distinct identities and the out-of-batch collec |
372 | 380 | }, |
373 | 381 | ); |
374 | 382 |
|
| 383 | + it('emailTemplates: one name across several locales is an i18n bundle, not a duplicate', () => { |
| 384 | + const mail = (locale?: string) => ({ |
| 385 | + name: 'dupe_mail', |
| 386 | + label: `Mail ${locale ?? 'floor'}`, |
| 387 | + ...(locale === undefined ? {} : { locale }), |
| 388 | + subject: 'Hi', |
| 389 | + bodyHtml: '<p>Hello</p>', |
| 390 | + }); |
| 391 | + expect(refusal({ manifest, objects: [thing], emailTemplates: [mail(), mail('zh-CN'), mail('ja-JP')] })).toBeNull(); |
| 392 | + const r = refusal({ manifest, objects: [thing], emailTemplates: [mail(), mail('zh-CN'), mail('zh-CN')] }); |
| 393 | + expectCrossReferenceEnvelope(r, 1); |
| 394 | + findingFor(r, 'emailTemplates', "Email template 'dupe_mail' for locale 'zh-CN'", [1, 2]); |
| 395 | + }); |
| 396 | + |
375 | 397 | it('the cross-scope action pair — one global, one object-bound, one name — stays accepted', () => { |
376 | 398 | const act = { name: 'dupe_go', label: 'Go', type: 'script' as const, target: 'noop' }; |
377 | 399 | expect(refusal({ manifest, objects: [thing], actions: [act, { ...act, objectName: 'dupe_thing' }] })).toBeNull(); |
|
0 commit comments