@notifiles/sdk 1.5.3 → 1.6.0
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/package.json +1 -1
- package/src/lib/cloudLibrary/minio/upload.js +2 -2
- package/src/template/index.js +1 -0
- package/src/template/locales/build.js +75 -0
- package/src/template/locales/build.test.js +42 -0
- package/src/template/locales/index.js +2 -0
- package/src/template/locales/publish.js +44 -0
- package/src/template/update/path.js +6 -0
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ export default async ({
|
|
|
4
4
|
sourceUrl,
|
|
5
5
|
filename,
|
|
6
6
|
auth,
|
|
7
|
-
|
|
7
|
+
contentType = null,
|
|
8
8
|
}) => {
|
|
9
9
|
|
|
10
10
|
try {
|
|
@@ -33,7 +33,7 @@ export default async ({
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const metaData = {
|
|
36
|
-
|
|
36
|
+
...(contentType ? { 'Content-Type': contentType } : {}),
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const minioResult = await new Promise((resolve, reject) => {
|
package/src/template/index.js
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// The locales index of an app's templates (`locales.json`, published at the root of each cloud by every update):
|
|
2
|
+
// which locales each template exists in, the app's default locale, and each language's default locale. A sender
|
|
3
|
+
// reads it to pick the best locale a template has for a reader (en-gb -> en-us) instead of probing for files.
|
|
4
|
+
//
|
|
5
|
+
// A template's locales are what its version step builds: the keys of its i18n.json plus the default locale
|
|
6
|
+
// (template/format/entry.js). Every template is listed, the ones an update skipped as unchanged included, so the
|
|
7
|
+
// index always describes the whole app.
|
|
8
|
+
//
|
|
9
|
+
// Each language's default comes from the app (`settings.languageDefaults`, e.g. `{ en: 'en-us', pt: 'pt-br' }`).
|
|
10
|
+
// Without one for a language, it is the app's default locale when that is of the language, else the language's only
|
|
11
|
+
// locale; a language with several locales and no default named is left out, for the sender to choose.
|
|
12
|
+
|
|
13
|
+
export const LOCALES_INDEX_FILE_NAME = 'locales.json'
|
|
14
|
+
export const LOCALES_INDEX_VERSION = 1
|
|
15
|
+
|
|
16
|
+
export const normalizedLocaleOf = (locale) =>
|
|
17
|
+
locale ? String(locale).trim().toLowerCase().replace(/_/g, '-') : null
|
|
18
|
+
|
|
19
|
+
const languageOf = (locale) => normalizedLocaleOf(locale)?.split('-')[0] || null
|
|
20
|
+
|
|
21
|
+
/** The locales one template is built in, normalised, sorted. */
|
|
22
|
+
export const localesOfTheEntry = ({ entry, defaultLocale }) => {
|
|
23
|
+
const locales = [...Object.keys(entry?.i18n || {}), defaultLocale]
|
|
24
|
+
.map(normalizedLocaleOf)
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
return [...new Set(locales)].sort()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const languageDefaultsOf = ({ allLocales, defaultLocale, languageDefaults = {} }) => {
|
|
30
|
+
const named = Object.fromEntries(
|
|
31
|
+
Object.entries(languageDefaults || {})
|
|
32
|
+
.map(([language, locale]) => [String(language).toLowerCase(), normalizedLocaleOf(locale)])
|
|
33
|
+
.filter(([, locale]) => Boolean(locale))
|
|
34
|
+
)
|
|
35
|
+
const byLanguage = {}
|
|
36
|
+
for (const locale of allLocales) {
|
|
37
|
+
const language = languageOf(locale)
|
|
38
|
+
byLanguage[language] = [...(byLanguage[language] || []), locale]
|
|
39
|
+
}
|
|
40
|
+
const derived = {}
|
|
41
|
+
for (const [language, locales] of Object.entries(byLanguage)) {
|
|
42
|
+
if (named[language]) {
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
if (languageOf(defaultLocale) === language) {
|
|
46
|
+
derived[language] = defaultLocale
|
|
47
|
+
} else if (locales.length === 1) {
|
|
48
|
+
derived[language] = locales[0]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return Object.fromEntries(Object.entries({ ...derived, ...named }).sort(([a], [b]) => a.localeCompare(b)))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The index for `entries` (as getEntries reads them) and the app's settings. */
|
|
55
|
+
export const buildLocalesIndex = ({ entries = [], settings = {}, now = new Date() }) => {
|
|
56
|
+
const defaultLocale = normalizedLocaleOf(settings.defaultLocale) || 'en-us'
|
|
57
|
+
const templates = {}
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (!entry?.slug) {
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
templates[String(entry.slug).toLowerCase()] = localesOfTheEntry({ entry, defaultLocale })
|
|
63
|
+
}
|
|
64
|
+
const allLocales = [...new Set(Object.values(templates).flat())].sort()
|
|
65
|
+
return {
|
|
66
|
+
version: LOCALES_INDEX_VERSION,
|
|
67
|
+
generatedAt: now.toISOString(),
|
|
68
|
+
defaultLocale,
|
|
69
|
+
languageDefaults: languageDefaultsOf({ allLocales, defaultLocale, languageDefaults: settings.languageDefaults }),
|
|
70
|
+
locales: allLocales,
|
|
71
|
+
templates: Object.fromEntries(Object.entries(templates).sort(([a], [b]) => a.localeCompare(b))),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default buildLocalesIndex
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { buildLocalesIndex, localesOfTheEntry } from './build.js'
|
|
2
|
+
|
|
3
|
+
const now = new Date('2026-10-22T08:00:00.000Z')
|
|
4
|
+
|
|
5
|
+
const entries = [
|
|
6
|
+
{ slug: 'automationemail', i18n: { 'en-us': {}, 'fr-fr': {}, 'es-es': {} } },
|
|
7
|
+
{ slug: 'Welcome', i18n: { 'fr_FR': {}, 'pt-br': {}, 'pt-pt': {} } },
|
|
8
|
+
{ slug: 'bare', i18n: {} },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
describe('buildLocalesIndex', () => {
|
|
12
|
+
it('lists every template with the locales it is built in, the default locale included', () => {
|
|
13
|
+
const index = buildLocalesIndex({ entries, settings: { defaultLocale: 'en-us' }, now })
|
|
14
|
+
expect(index.templates).toEqual({
|
|
15
|
+
automationemail: ['en-us', 'es-es', 'fr-fr'],
|
|
16
|
+
bare: ['en-us'],
|
|
17
|
+
welcome: ['en-us', 'fr-fr', 'pt-br', 'pt-pt'],
|
|
18
|
+
})
|
|
19
|
+
expect(index.defaultLocale).toBe('en-us')
|
|
20
|
+
expect(index.version).toBe(1)
|
|
21
|
+
expect(index.generatedAt).toBe('2026-10-22T08:00:00.000Z')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('names a default per language: the app default for its language, a lone locale for the others', () => {
|
|
25
|
+
const index = buildLocalesIndex({ entries, settings: { defaultLocale: 'en-us' }, now })
|
|
26
|
+
expect(index.languageDefaults).toEqual({ en: 'en-us', es: 'es-es', fr: 'fr-fr' })
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('takes the app\'s named defaults over the derived ones, for ambiguous languages too', () => {
|
|
30
|
+
const index = buildLocalesIndex({
|
|
31
|
+
entries,
|
|
32
|
+
settings: { defaultLocale: 'en_US', languageDefaults: { pt: 'pt_BR', fr: 'fr-fr' } },
|
|
33
|
+
now,
|
|
34
|
+
})
|
|
35
|
+
expect(index.languageDefaults).toEqual({ en: 'en-us', es: 'es-es', fr: 'fr-fr', pt: 'pt-br' })
|
|
36
|
+
expect(index.locales).toEqual(['en-us', 'es-es', 'fr-fr', 'pt-br', 'pt-pt'])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('normalises locales', () => {
|
|
40
|
+
expect(localesOfTheEntry({ entry: { i18n: { 'EN_gb': {} } }, defaultLocale: 'en-us' })).toEqual(['en-gb', 'en-us'])
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import os from 'os'
|
|
3
|
+
import fsPath from 'path'
|
|
4
|
+
import { buildLocalesIndex, LOCALES_INDEX_FILE_NAME } from './build.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds the locales index of `entries` and uploads it as `locales.json` at the root of every cloud in
|
|
8
|
+
* `settings.clouds`. Answers `{ index, uploads: [{ id, url }] }`; a cloud that refuses the upload is reported with a
|
|
9
|
+
* null url and never fails the update (the templates themselves are already synced).
|
|
10
|
+
*/
|
|
11
|
+
export const publishLocalesIndex = async ({ entries, settings, now = new Date() }) => {
|
|
12
|
+
const index = buildLocalesIndex({ entries, settings, now })
|
|
13
|
+
const folder = await fs.promises.mkdtemp(fsPath.join(os.tmpdir(), 'notifiles-locales-'))
|
|
14
|
+
const sourceUrl = fsPath.join(folder, LOCALES_INDEX_FILE_NAME)
|
|
15
|
+
await fs.promises.writeFile(sourceUrl, JSON.stringify(index, null, 2))
|
|
16
|
+
|
|
17
|
+
const uploads = []
|
|
18
|
+
try {
|
|
19
|
+
for (const cloud of settings.clouds || []) {
|
|
20
|
+
const cloudLibrary = settings.cloudLibrary?.({ id: cloud.id })
|
|
21
|
+
if (!cloudLibrary?.upload) {
|
|
22
|
+
uploads.push({ id: cloud.id, url: null })
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
const result = await cloudLibrary.upload({
|
|
26
|
+
sourceUrl,
|
|
27
|
+
filename: LOCALES_INDEX_FILE_NAME,
|
|
28
|
+
auth: cloud.auth,
|
|
29
|
+
contentType: 'application/json',
|
|
30
|
+
})
|
|
31
|
+
uploads.push({ id: cloud.id, url: result?.url || null })
|
|
32
|
+
}
|
|
33
|
+
} finally {
|
|
34
|
+
await fs.promises.rm(folder, { recursive: true, force: true })
|
|
35
|
+
}
|
|
36
|
+
for (const upload of uploads) {
|
|
37
|
+
console.log(upload.url
|
|
38
|
+
? `[notifiles] Locales index published: ${upload.url}`
|
|
39
|
+
: `[notifiles] Locales index not published to ${upload.id}`)
|
|
40
|
+
}
|
|
41
|
+
return { index, uploads }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default publishLocalesIndex
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
updateTemplateHashCache
|
|
14
14
|
} from './lib/cache.js'
|
|
15
15
|
import runWithConcurrency from '../../lib/async/runWithConcurrency.js'
|
|
16
|
+
import publishLocalesIndex from '../locales/publish.js'
|
|
16
17
|
|
|
17
18
|
const DEFAULT_CONCURRENCY = 10
|
|
18
19
|
|
|
@@ -67,5 +68,10 @@ export default async ({
|
|
|
67
68
|
}
|
|
68
69
|
})
|
|
69
70
|
|
|
71
|
+
// The index of every template's locales, unchanged ones included, so a sender picks a locale a template has.
|
|
72
|
+
if (!settings.dryRun && settings.isLocalesIndexPublished !== false) {
|
|
73
|
+
await publishLocalesIndex({ entries, settings })
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
return true
|
|
71
77
|
}
|