apostrophe 3.10.0 → 3.13.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/index.js +37 -1
  3. package/lib/moog.js +2 -2
  4. package/modules/@apostrophecms/asset/lib/globalIcons.js +2 -0
  5. package/modules/@apostrophecms/attachment/index.js +1 -1
  6. package/modules/@apostrophecms/doc/index.js +2 -2
  7. package/modules/@apostrophecms/doc-type/index.js +2 -2
  8. package/modules/@apostrophecms/express/index.js +7 -1
  9. package/modules/@apostrophecms/i18n/i18n/en.json +1 -0
  10. package/modules/@apostrophecms/i18n/i18n/sk.json +23 -1
  11. package/modules/@apostrophecms/i18n/index.js +62 -13
  12. package/modules/@apostrophecms/login/index.js +295 -52
  13. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +245 -76
  14. package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +108 -0
  15. package/modules/@apostrophecms/module/index.js +12 -2
  16. package/modules/@apostrophecms/page/index.js +7 -8
  17. package/modules/@apostrophecms/permission/index.js +1 -1
  18. package/modules/@apostrophecms/piece-type/index.js +1 -1
  19. package/modules/@apostrophecms/schema/index.js +18 -2
  20. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +12 -5
  21. package/modules/@apostrophecms/schema/ui/apos/components/AposInputCheckboxes.vue +4 -0
  22. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +18 -0
  23. package/modules/@apostrophecms/util/index.js +3 -9
  24. package/package.json +9 -9
  25. package/test/login-requirements.js +328 -0
  26. package/test/modules/base-type/i18n/custom/en.json +4 -0
  27. package/test/modules/base-type/i18n/en.json +3 -0
  28. package/test/modules/nested-module-subdirs/example1/index.js +5 -0
  29. package/test/modules/nested-module-subdirs/modules.js +7 -0
  30. package/test/modules/subtype/i18n/custom/en.json +4 -0
  31. package/test/modules/subtype/index.js +7 -0
  32. package/test/pages-rest.js +39 -0
  33. package/test/static-i18n.js +28 -0
  34. package/test/with-nested-module-subdirs.js +32 -0
  35. package/test/without-nested-module-subdirs.js +31 -0
  36. package/test-lib/util.js +4 -2
  37. package/.github/pull_request_template.md +0 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,58 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.13.0 - 2022-02-04
4
+
5
+ ### Adds
6
+
7
+ * Additional requirements and related UI may be imposed on native ApostropheCMS logins using the new `requirements` feature, which can be extended in modules that `improve` the `@apostrophecms/login` module. These requirements are not imposed for single sign-on logins via `@apostrophecms/passport-bridge`. See the documentation for more information.
8
+ * Adds latest Slovak translation strings to SK.json in `i18n/` folder. Thanks to [Michael Huna](https://github.com/Miselrkba) for the contribution.
9
+ * Verifies `afterPasswordVerified` requirements one by one when emitting done event, allows to manage errors ans success before to go to the next requirement. Stores and validate each requirement in the token. Checks the new `askForConfirmation` requirement option to go to the next step when emitting done event or waiting for the confirm event (in order to manage success messages). Removes support for `afterSubmit` for now.
10
+
11
+ ### Fixes
12
+
13
+ * Decodes the testReq `param` property in `serveNotFound`. This fixes a problem where page titles using diacritics triggered false 404 errors.
14
+ * Registers the default namespace in the Vue instance of i18n, fixing a lack of support for un-namespaced l10n keys in the UI.
15
+
16
+ ## 3.12.0 - 2022-01-21
17
+
18
+ ### Adds
19
+
20
+ * It is now best practice to deliver namespaced i18n strings as JSON files in module-level subdirectories of `i18n/` named to match the namespace, e.g. `i18n/ourTeam` if the namespace is `ourTeam`. This allows base class modules to deliver phrases to any namespace without conflicting with those introduced at project level. The `i18n` option is now deprecated in favor of the new `i18n` module format section, which is only needed if `browser: true` must be specified for a namespace.
21
+ * Brought back the `nestedModuleSubdirs` feature from A2, which allows modules to be nested in subdirectories if `nestedModuleSubdirs: true` is set in `app.js`. As in A2, module configuration (including activation) can also be grouped in a `modules.js` file in such subdirectories.
22
+
23
+ ### Fixes
24
+
25
+ * Fixes minor inline documentation comments.
26
+ * UI strings that are not registered localization keys will now display properly when they contain a colon (`:`). These were previously interpreted as i18next namespace/key pairs and the "namespace" portion was left out.
27
+ * Fixes a bug where changing the page type immediately after clicking "New Page" would produce a console error. In general, areas and checkboxes now correctly handle their value being changed to `null` by the parent schema after initial startup of the `AposInputArea` or `AposInputCheckboxes` component.
28
+ * It is now best practice to deliver namespaced i18n strings as JSON files in module-level subdirectories of `i18n/` named to match the namespace, e.g. `i18n/ourTeam` if the namespace is `ourTeam`. This allows base class modules to deliver phrases to any namespace without conflicting with those introduced at project level. The `i18n` option is now deprecated in favor of the new `i18n` module format section, which is only needed if `browser: true` must be specified for a namespace.
29
+ * Removes the `@apostrophecms/util` module template helper `indexBy`, which was using a lodash method not included in lodash v4.
30
+ * Removes an unimplemented `csrfExceptions` module section cascade. Use the `csrfExceptions` *option* of any module to set an array of URLs excluded from CSRF protection. More information is forthcoming in the documentation.
31
+ * Fix `[Object Object]` in the console when warning `A permission.can() call was made with a type that has no manager` is printed.
32
+
33
+ ### Changes
34
+
35
+ * Temporarily removes `npm audit` from our automated tests because of a sub-dependency of vue-loader that doesn't actually cause a security vulnerability for apostrophe.
36
+
37
+ ## 3.11.0 - 2022-01-06
38
+
39
+ ### Adds
40
+
41
+ * Apostrophe now extends Passport's `req.login` to emit an `afterSessionLogin` event from the `@apostrophecms:login` module, with `req` as an argument. Note that this does not occur at all for login API calls that return a bearer token rather than establishing an Express session.
42
+
43
+ ### Fixes
44
+
45
+ * Apostrophe's extension of `req.login` now accounts for the `req.logIn` alias and the skippable `options` parameter, which is relied upon in some `passport` strategies.
46
+ * Apostrophe now warns if a nonexistent widget type is configured for an area field, with special attention to when `-widget` has been erroneously included in the name. For backwards compatibility this is a startup warning rather than a fatal error, as sites generally did operate successfully otherwise with this type of bug present.
47
+
48
+ ### Changes
49
+
50
+ * Unpins `vue-click-outside-element` the packaging of which has been fixed upstream.
51
+ * Adds deprecation note to `__testDefaults` option. It is not in use, but removing would be a minor BC break we don't need to make.
52
+ * Allows test modules to use a custom port as an option on the `@apostrophecms/express` module.
53
+ * Removes the code base pull request template to instead inherit the organization-level template.
54
+ * Adds `npm audit` back to the test scripts.
55
+
3
56
  ## 3.10.0 - 2021-12-22
4
57
 
5
58
  ### Fixes
package/index.js CHANGED
@@ -7,6 +7,7 @@ const cluster = require('cluster');
7
7
  const { cpus } = require('os');
8
8
  const process = require('process');
9
9
  const npmResolve = require('resolve');
10
+ const glob = require('glob');
10
11
 
11
12
  let defaults = require('./defaults.js');
12
13
 
@@ -273,6 +274,33 @@ module.exports = async function(options) {
273
274
  return _module;
274
275
  }
275
276
 
277
+ function nestedModuleSubdirs() {
278
+ if (!options.nestedModuleSubdirs) {
279
+ return;
280
+ }
281
+ const configs = glob.sync(self.localModules + '/**/modules.js');
282
+ _.each(configs, function(config) {
283
+ try {
284
+ _.merge(self.options.modules, require(config));
285
+ } catch (e) {
286
+ console.error(stripIndent`
287
+ When nestedModuleSubdirs is active, any modules.js file beneath:
288
+
289
+ ${self.localModules}
290
+
291
+ must export an object containing configuration for Apostrophe modules.
292
+
293
+ The file:
294
+
295
+ ${config}
296
+
297
+ did not parse.
298
+ `);
299
+ throw e;
300
+ }
301
+ });
302
+ }
303
+
276
304
  function autodetectBundles() {
277
305
  const modules = _.keys(self.options.modules);
278
306
  _.each(modules, function(name) {
@@ -406,7 +434,13 @@ module.exports = async function(options) {
406
434
  localModules: self.localModules,
407
435
  defaultBaseClass: '@apostrophecms/module',
408
436
  sections: [ 'helpers', 'handlers', 'routes', 'apiRoutes', 'restApiRoutes', 'renderRoutes', 'middleware', 'customTags', 'components', 'tasks' ],
409
- unparsedSections: [ 'queries', 'extendQueries', 'icons' ]
437
+ nestedModuleSubdirs: self.options.nestedModuleSubdirs,
438
+ unparsedSections: [
439
+ 'queries',
440
+ 'extendQueries',
441
+ 'icons',
442
+ 'i18n'
443
+ ]
410
444
  });
411
445
 
412
446
  self.synth = synth;
@@ -417,6 +451,8 @@ module.exports = async function(options) {
417
451
  self.redefine = self.synth.redefine;
418
452
  self.create = self.synth.create;
419
453
 
454
+ nestedModuleSubdirs();
455
+
420
456
  _.each(self.options.modules, function(options, name) {
421
457
  synth.define(name, options);
422
458
  });
package/lib/moog.js CHANGED
@@ -369,9 +369,9 @@ module.exports = function(options) {
369
369
  }
370
370
 
371
371
  function capture(section) {
372
- that[section] = {};
372
+ that.__meta[section] = {};
373
373
  for (const step of steps) {
374
- that[section][step.__meta.name] = step[section];
374
+ that.__meta[section][step.__meta.name] = step[section];
375
375
  }
376
376
  }
377
377
 
@@ -12,6 +12,8 @@ module.exports = {
12
12
  'checkbox-blank-icon': 'CheckboxBlankOutline',
13
13
  'check-all-icon': 'CheckAll',
14
14
  'check-bold-icon': 'CheckBold',
15
+ 'check-circle-icon': 'CheckCircle',
16
+ 'check-decagram-icon': 'CheckDecagram',
15
17
  'checkbox-marked-icon': 'CheckboxMarked',
16
18
  'chevron-down-icon': 'ChevronDown',
17
19
  'chevron-left-icon': 'ChevronLeft',
@@ -1114,7 +1114,7 @@ module.exports = {
1114
1114
  await copyOut(uploadfsPath, tempFile);
1115
1115
  await self.sanitizeSvg(tempFile);
1116
1116
  await copyIn(tempFile, uploadfsPath);
1117
- await self.db.update({
1117
+ await self.db.updateOne({
1118
1118
  _id: attachment._id
1119
1119
  }, {
1120
1120
  $set: {
@@ -675,10 +675,10 @@ module.exports = {
675
675
  },
676
676
 
677
677
  // Insert the given document. Called by `.insert()`. You will usually want to
678
- // call the update method of the appropriate doc type manager instead:
678
+ // call the insert method of the appropriate doc type manager instead:
679
679
  //
680
680
  // ```javascript
681
- // self.apos.doc.getManager(doc.type).update(...)
681
+ // self.apos.doc.getManager(doc.type).insert(...)
682
682
  // ```
683
683
  //
684
684
  // However you can override this method to alter the
@@ -194,11 +194,11 @@ module.exports = {
194
194
  _id: doc._id.replace(':draft', ':previous')
195
195
  });
196
196
  if (published) {
197
- await self.apos.doc.db.remove({ _id: published._id });
197
+ await self.apos.doc.db.removeOne({ _id: published._id });
198
198
  await self.emit('afterDelete', req, published, { checkForChildren: false });
199
199
  }
200
200
  if (previous) {
201
- await self.apos.doc.db.remove({ _id: previous._id });
201
+ await self.apos.doc.db.removeOne({ _id: previous._id });
202
202
  await self.emit('afterDelete', req, previous, { checkForChildren: false });
203
203
  }
204
204
  },
@@ -307,7 +307,13 @@ module.exports = {
307
307
  // "expires" ourselves too
308
308
  const bearer = await self.apos.login.bearerTokens.findOne({
309
309
  _id: req.token,
310
- expires: { $gte: new Date() }
310
+ expires: { $gte: new Date() },
311
+ // requirementsToVerify array should be empty or inexistant
312
+ // for the token to be usable to log in.
313
+ $or: [
314
+ { requirementsToVerify: { $exists: false } },
315
+ { requirementsToVerify: { $ne: [] } }
316
+ ]
311
317
  });
312
318
  return bearer && bearer.userId;
313
319
  }
@@ -167,6 +167,7 @@
167
167
  "localizeNewRelated": "Localize new related documents",
168
168
  "localizingBusy": "Localizing Content",
169
169
  "login": "Login",
170
+ "loginErrorGeneric": "An error occurred. Please try again.",
170
171
  "loginDisabled": "Login Disabled",
171
172
  "loginPageBothRequired": "Both the username and the password are required.",
172
173
  "loginPageBadCredentials": "Your credentials are incorrect, or there is no such user",
@@ -3,6 +3,7 @@
3
3
  "addItem": "Pridať položku",
4
4
  "addWidgetType": "Pridať {{ label }}",
5
5
  "admin": "Admin",
6
+ "affirmativeLabel": "Áno, pokračovať.",
6
7
  "altText": "Alternatívny text",
7
8
  "altTextHelp": "Alternatívny popis obrázkov pre zlepšenú prístupnosť",
8
9
  "any": "ľubovoľný",
@@ -10,6 +11,8 @@
10
11
  "applyToSubpages": "Aplikovať na podstánky",
11
12
  "arrayCancelDescription": "Chcete zahodiť zmeny v tomto zozname?",
12
13
  "archive": "Archív",
14
+ "archivingBatchConfirmation": "Naozaj chcete archivovať {{ count }} {{ type }}?",
15
+ "archivingBatchConfirmationButton": "Áno, archivovať obsah.",
13
16
  "archiveImage": "Archivovať obrázok",
14
17
  "archiveOnlyThisPage": "Archivovať len túto stránku",
15
18
  "archivePageAndSubpages": "Archivovať túto stránku a všetky podradené stránky",
@@ -121,6 +124,7 @@
121
124
  "errorPageMessage": "Došlo k chybe",
122
125
  "errorPageStatusCode": "500",
123
126
  "errorPageTitle": "Došlo k chybe v nadpise stánky",
127
+ "errorBatchOperationNoti": "Skupinová operácia {{ operation }} zlyhala.",
124
128
  "everythingElse": "Všetko ostatné",
125
129
  "exit": "Odísť",
126
130
  "fetchPublishedVersionFailed": "Pri načítaní zverejnenej verzie dokumentu sa vyskytla chyba.",
@@ -173,6 +177,8 @@
173
177
  "manageDocType": "Spravovať {{ type }}",
174
178
  "manageDraftSubmissions": "Spravujte návrhy príspevkov",
175
179
  "managePages": "Spravovať stránky",
180
+ "maxLabel": "Max:",
181
+ "maxUi": "Max: {{ number }}",
176
182
  "mediaCreatedDate": "Nahraté: {{ createdDate }}",
177
183
  "mediaDimensions": "Rozmery: {{ width }} 𝗑 {{ height }}",
178
184
  "mediaFileSize": "Veľkosť súboru: {{ fileSize }}",
@@ -180,6 +186,10 @@
180
186
  "mediaMB": "{{ size }}MB",
181
187
  "mediaUploadViaDrop": "Presuňte ich sem, keď budete pripravení",
182
188
  "mediaUploadViaExplorer": "Alebo kliknutím otvorte adresárovú štruktúru",
189
+ "minLabel": "Min:",
190
+ "minUi": "Min: {{ number }}",
191
+ "modify": "Zmeniť",
192
+ "modifyOrDelete": "Zmeniť / Vymazať",
183
193
  "moreOptions": "Viac možností",
184
194
  "moreOperations": "Viac úprav",
185
195
  "multipleEditors": "Viaceré editory",
@@ -202,17 +212,19 @@
202
212
  "notFoundPageStatusCode": "404",
203
213
  "notFoundPageTitle": "404 - Stránka nenájdená",
204
214
  "notInLocale": "Aktuálna stránka v {{ label }} neexistuje. Preložte verziu z {{ currentLocale }}?",
215
+ "notificationClearEventError": "Pri vymazávaní zaregistrovanej notifikačnej udalosti sa vyskytla chyba.",
205
216
  "noTypeFound": "Nenašiel sa žiadny {{ type }}",
206
217
  "parentNotLocalized": "Najprv preložte nadradenú stránku",
207
218
  "notYetPublished": "Tento dokument ešte nebol zverejnený.",
208
219
  "nudgeDown": "Posunúť nadol",
209
220
  "nudgeUp": "Posunúť nahor",
221
+ "numberAdded": "{{ count }} Pridané",
210
222
  "office": "Kancelária",
211
223
  "openGlobal": "Otvorte globálne nastavenia webu",
212
224
  "page": "Stránka",
213
225
  "pageDoesNotExistYet": "Stránka zatiaľ neexistuje",
214
226
  "pageDoesNotExistYetDescription": "Stránka, ktorá poskytuje záznam pre tento diel, ešte nie je k dispozícii ako {{ mode }} v jazykovej mutácii {{ locale }}.",
215
- "pageIsParked": "Táto stránka je zaparkovaná a nemožno ju presunúť",
227
+ "pageIsParked": "Táto stránka je uzamknutá nemožno ju presunúť",
216
228
  "pageNumber": "Stránka {{ number }}",
217
229
  "pageTitle": "Názov stránky",
218
230
  "pages": "Stránky",
@@ -242,6 +254,8 @@
242
254
  "richTextUndo": "Vrátiť späť",
243
255
  "richTextStyleConfigWarning": "Nesprávne nakonfigurovaný štýl: popiska: {{ label }}, {{ tag }}",
244
256
  "password": "Heslo",
257
+ "passwordErrorMin": "Minimum {{ min }} znakov",
258
+ "passwordErrorMax": "Maximum {{ max }} znakov",
245
259
  "passwordResetRequest": "Vaša žiadosť o obnovenie hesla zo stránky {{ site }}",
246
260
  "pasteWidget": "Prilepiť {{ widget }}",
247
261
  "pending": "Čaká na spracovanie",
@@ -277,6 +291,8 @@
277
291
  "relatedDocsOnly": "Len súvisiace dokumenty",
278
292
  "relatedDocsDefinition": "Súvisiace dokumenty sú dokumenty, na ktoré sa odkazuje v tomto dokumente. Obvykle to zahŕňa obrázky, obsah definovaný vzťahmi atď.",
279
293
  "restore": "Obnoviť",
294
+ "restoreBatchConfirmation": "Naozaj chcete obnoviť {{ count }} {{ type }}?",
295
+ "restoreBatchConfirmationButton": "Áno, obnoviť obsah.",
280
296
  "resolveErrorsBeforeSaving": "Pred uložením vyriešte chyby.",
281
297
  "resolveErrorsFirst": "Najprv vyriešte chyby.",
282
298
  "restoreOnlyThisPage": "Obnovte iba túto stránku",
@@ -305,6 +321,12 @@
305
321
  "select": "Vybrať",
306
322
  "selectedMenuItem": "✓ {{ label }}",
307
323
  "selectAll": "Vybrať všetko",
324
+ "selectBoxMessage": "{{ num }} {{ label }} vybraté.",
325
+ "selectBoxMessagePage": "{{ num }} {{ label }} na tejto stránke vybraté.",
326
+ "selectBoxMessageAllButton": "Vybrať všetko {{ num }} {{ label }}.",
327
+ "selectBoxMessageButton": "Vybrať {{ num }} {{ label }}.",
328
+ "selectBoxMessageSelected": "{{ num }} {{ label }} vybraté.",
329
+ "selectBoxMessageAllSelected": "Všetky {{ num }} {{ label }} vybraté.",
308
330
  "deselectAll": "Odznačiť všetko",
309
331
  "selectContent": "Vyberte obsah",
310
332
  "selectContentToLocalize": "Ktorý obsah chcete lokalizovať?",
@@ -10,6 +10,7 @@ const fs = require('fs');
10
10
  const _ = require('lodash');
11
11
  const { stripIndent } = require('common-tags');
12
12
  const ExpressSessionCookie = require('express-session/session/cookie');
13
+ const path = require('path');
13
14
 
14
15
  const apostropheI18nDebugPlugin = {
15
16
  type: 'postProcessor',
@@ -46,6 +47,7 @@ module.exports = {
46
47
  }
47
48
  },
48
49
  async init(self) {
50
+ self.defaultNamespace = 'default';
49
51
  self.namespaces = {};
50
52
  self.debug = process.env.APOS_DEBUG_I18N ? true : self.options.debug;
51
53
  self.show = process.env.APOS_SHOW_I18N ? true : self.options.show;
@@ -83,7 +85,7 @@ module.exports = {
83
85
  // Nunjucks and Vue will already do this
84
86
  escapeValue: false
85
87
  },
86
- defaultNS: 'default',
88
+ defaultNS: self.defaultNamespace,
87
89
  debug: self.debug
88
90
  });
89
91
  if (self.show) {
@@ -380,21 +382,65 @@ module.exports = {
380
382
  // Add the i18next resources provided by the specified module,
381
383
  // merging with any existing phrases for the same locales and namespaces
382
384
  addResourcesForModule(module) {
383
- if (!module.options.i18n) {
384
- return;
385
- }
386
- const ns = module.options.i18n.ns || 'default';
385
+ self.addDefaultResourcesForModule(module);
386
+ self.addNamespacedResourcesForModule(module);
387
+ },
388
+ // Automatically adds any localizations found in .json files in the main `i18n` subdirectory
389
+ // of a module.
390
+ //
391
+ // These are added to the `default` namespace, unless the legacy `i18n.ns` option is set
392
+ // for the module (not the preferred way, use namespace subdirectories in new projects).
393
+ addDefaultResourcesForModule(module) {
394
+ const ns = (module.options.i18n && module.options.i18n.ns) || 'default';
387
395
  self.namespaces[ns] = self.namespaces[ns] || {};
388
- self.namespaces[ns].browser = self.namespaces[ns].browser || !!module.options.i18n.browser;
396
+ self.namespaces[ns].browser = self.namespaces[ns].browser || (module.options.i18n && module.options.i18n.browser);
389
397
  for (const entry of module.__meta.chain) {
390
- const localizationsDir = `${entry.dirname}/i18n`;
391
- if (!fs.existsSync(localizationsDir)) {
392
- continue;
398
+ const localizationsDir = path.join(entry.dirname, 'i18n');
399
+ if (!self.defaultLocalizationsDirsAdded.has(localizationsDir)) {
400
+ self.defaultLocalizationsDirsAdded.add(localizationsDir);
401
+ if (!fs.existsSync(localizationsDir)) {
402
+ continue;
403
+ }
404
+ for (const localizationFile of fs.readdirSync(localizationsDir)) {
405
+ if (!localizationFile.endsWith('.json')) {
406
+ // Likely a namespace subdirectory
407
+ continue;
408
+ }
409
+ const data = JSON.parse(fs.readFileSync(path.join(localizationsDir, localizationFile)));
410
+ const locale = localizationFile.replace('.json', '');
411
+ self.i18next.addResourceBundle(locale, ns, data, true, true);
412
+ }
393
413
  }
394
- for (const localizationFile of fs.readdirSync(localizationsDir)) {
395
- const data = JSON.parse(fs.readFileSync(`${localizationsDir}/${localizationFile}`));
396
- const locale = localizationFile.replace('.json', '');
397
- self.i18next.addResourceBundle(locale, ns, data, true, true);
414
+ }
415
+ },
416
+ // Automatically adds any localizations found in subdirectories of the main `i18n`
417
+ // subdirectory of a module. The subdirectory's name is treated as an i18n namespace
418
+ // name.
419
+ addNamespacedResourcesForModule(module) {
420
+ for (const entry of module.__meta.chain) {
421
+ const metadata = module.__meta.i18n[entry.name] || {};
422
+ const localizationsDir = `${entry.dirname}/i18n`;
423
+ if (!self.namespacedLocalizationsDirsAdded.has(localizationsDir)) {
424
+ self.namespacedLocalizationsDirsAdded.add(localizationsDir);
425
+ if (!fs.existsSync(localizationsDir)) {
426
+ continue;
427
+ }
428
+ for (const ns of fs.readdirSync(localizationsDir)) {
429
+ if (ns.endsWith('.json')) {
430
+ // A JSON file for the default namespace, already handled
431
+ continue;
432
+ }
433
+ self.namespaces[ns] = self.namespaces[ns] || {};
434
+ self.namespaces[ns].browser = self.namespaces[ns].browser ||
435
+ (metadata[ns] && metadata[ns].browser);
436
+ const namespaceDir = path.join(localizationsDir, ns);
437
+ for (const localizationFile of fs.readdirSync(namespaceDir)) {
438
+ const fullLocalizationFile = path.join(namespaceDir, localizationFile);
439
+ const data = JSON.parse(fs.readFileSync(fullLocalizationFile));
440
+ const locale = localizationFile.replace('.json', '');
441
+ self.i18next.addResourceBundle(locale, ns, data, true, true);
442
+ }
443
+ }
398
444
  }
399
445
  }
400
446
  },
@@ -402,6 +448,8 @@ module.exports = {
402
448
  // itself, called by init. Later modules call addResourcesForModule(self),
403
449
  // making phrases available gradually as Apostrophe starts up
404
450
  addInitialResources() {
451
+ self.defaultLocalizationsDirsAdded = new Set();
452
+ self.namespacedLocalizationsDirsAdded = new Set();
405
453
  for (const module of Object.values(self.apos.modules)) {
406
454
  self.addResourcesForModule(module);
407
455
  }
@@ -493,6 +541,7 @@ module.exports = {
493
541
  i18n,
494
542
  locale: req.locale,
495
543
  defaultLocale: self.defaultLocale,
544
+ defaultNamespace: self.defaultNamespace,
496
545
  locales: self.locales,
497
546
  debug: self.debug,
498
547
  show: self.show,