@plone/volto 18.32.3 → 18.33.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 (169) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/locales/af/LC_MESSAGES/volto.po +10 -0
  3. package/locales/af.json +1 -1
  4. package/locales/ar/LC_MESSAGES/volto.po +10 -0
  5. package/locales/ar.json +1 -1
  6. package/locales/bg/LC_MESSAGES/volto.po +10 -0
  7. package/locales/bg.json +1 -1
  8. package/locales/bn/LC_MESSAGES/volto.po +10 -0
  9. package/locales/bn.json +1 -1
  10. package/locales/ca/LC_MESSAGES/volto.po +10 -0
  11. package/locales/ca.json +1 -1
  12. package/locales/cs/LC_MESSAGES/volto.po +10 -0
  13. package/locales/cs.json +1 -1
  14. package/locales/cy/LC_MESSAGES/volto.po +10 -0
  15. package/locales/cy.json +1 -1
  16. package/locales/da/LC_MESSAGES/volto.po +10 -0
  17. package/locales/da.json +1 -1
  18. package/locales/de/LC_MESSAGES/volto.po +10 -0
  19. package/locales/de.json +1 -1
  20. package/locales/el/LC_MESSAGES/volto.po +10 -0
  21. package/locales/el.json +1 -1
  22. package/locales/en/LC_MESSAGES/volto.po +10 -0
  23. package/locales/en.json +1 -1
  24. package/locales/en_AU/LC_MESSAGES/volto.po +10 -0
  25. package/locales/en_AU.json +1 -1
  26. package/locales/en_GB/LC_MESSAGES/volto.po +10 -0
  27. package/locales/en_GB.json +1 -1
  28. package/locales/eo/LC_MESSAGES/volto.po +10 -0
  29. package/locales/eo.json +1 -1
  30. package/locales/es/LC_MESSAGES/volto.po +23 -13
  31. package/locales/es.json +1 -1
  32. package/locales/et/LC_MESSAGES/volto.po +10 -0
  33. package/locales/et.json +1 -1
  34. package/locales/eu/LC_MESSAGES/volto.po +22 -12
  35. package/locales/eu.json +1 -1
  36. package/locales/fa/LC_MESSAGES/volto.po +10 -0
  37. package/locales/fa.json +1 -1
  38. package/locales/fi/LC_MESSAGES/volto.po +10 -0
  39. package/locales/fi.json +1 -1
  40. package/locales/fr/LC_MESSAGES/volto.po +10 -0
  41. package/locales/fr.json +1 -1
  42. package/locales/fu/LC_MESSAGES/volto.po +10 -0
  43. package/locales/fu.json +1 -1
  44. package/locales/gl/LC_MESSAGES/volto.po +1013 -1002
  45. package/locales/gl.json +1 -1
  46. package/locales/he/LC_MESSAGES/volto.po +10 -0
  47. package/locales/he.json +1 -1
  48. package/locales/hi/LC_MESSAGES/volto.po +10 -0
  49. package/locales/hi.json +1 -1
  50. package/locales/hr/LC_MESSAGES/volto.po +10 -0
  51. package/locales/hr.json +1 -1
  52. package/locales/hu/LC_MESSAGES/volto.po +10 -0
  53. package/locales/hu.json +1 -1
  54. package/locales/hy/LC_MESSAGES/volto.po +10 -0
  55. package/locales/hy.json +1 -1
  56. package/locales/id/LC_MESSAGES/volto.po +10 -0
  57. package/locales/id.json +1 -1
  58. package/locales/it/LC_MESSAGES/volto.po +13 -3
  59. package/locales/it.json +1 -1
  60. package/locales/ja/LC_MESSAGES/volto.po +10 -0
  61. package/locales/ja.json +1 -1
  62. package/locales/ka/LC_MESSAGES/volto.po +10 -0
  63. package/locales/ka.json +1 -1
  64. package/locales/kn/LC_MESSAGES/volto.po +10 -0
  65. package/locales/kn.json +1 -1
  66. package/locales/ko/LC_MESSAGES/volto.po +10 -0
  67. package/locales/ko.json +1 -1
  68. package/locales/lt/LC_MESSAGES/volto.po +10 -0
  69. package/locales/lt.json +1 -1
  70. package/locales/lv/LC_MESSAGES/volto.po +10 -0
  71. package/locales/lv.json +1 -1
  72. package/locales/mi/LC_MESSAGES/volto.po +10 -0
  73. package/locales/mi.json +1 -1
  74. package/locales/mk/LC_MESSAGES/volto.po +10 -0
  75. package/locales/mk.json +1 -1
  76. package/locales/my/LC_MESSAGES/volto.po +10 -0
  77. package/locales/my.json +1 -1
  78. package/locales/nb_NO/LC_MESSAGES/volto.po +10 -0
  79. package/locales/nb_NO.json +1 -1
  80. package/locales/nl/LC_MESSAGES/volto.po +25 -15
  81. package/locales/nl.json +1 -1
  82. package/locales/nn/LC_MESSAGES/volto.po +10 -0
  83. package/locales/nn.json +1 -1
  84. package/locales/pl/LC_MESSAGES/volto.po +10 -0
  85. package/locales/pl.json +1 -1
  86. package/locales/pt/LC_MESSAGES/volto.po +10 -0
  87. package/locales/pt.json +1 -1
  88. package/locales/pt_BR/LC_MESSAGES/volto.po +10 -0
  89. package/locales/pt_BR.json +1 -1
  90. package/locales/rm/LC_MESSAGES/volto.po +10 -0
  91. package/locales/rm.json +1 -1
  92. package/locales/ro/LC_MESSAGES/volto.po +10 -0
  93. package/locales/ro.json +1 -1
  94. package/locales/ru/LC_MESSAGES/volto.po +10 -0
  95. package/locales/ru.json +1 -1
  96. package/locales/sk/LC_MESSAGES/volto.po +10 -0
  97. package/locales/sk.json +1 -1
  98. package/locales/sl/LC_MESSAGES/volto.po +10 -0
  99. package/locales/sl.json +1 -1
  100. package/locales/sm/LC_MESSAGES/volto.po +10 -0
  101. package/locales/sm.json +1 -1
  102. package/locales/sq/LC_MESSAGES/volto.po +10 -0
  103. package/locales/sq.json +1 -1
  104. package/locales/sr/LC_MESSAGES/volto.po +10 -0
  105. package/locales/sr.json +1 -1
  106. package/locales/sr@cyrl/LC_MESSAGES/volto.po +10 -0
  107. package/locales/sr@cyrl.json +1 -1
  108. package/locales/sr@latn/LC_MESSAGES/volto.po +10 -0
  109. package/locales/sr@latn.json +1 -1
  110. package/locales/sv/LC_MESSAGES/volto.po +10 -0
  111. package/locales/sv.json +1 -1
  112. package/locales/ta/LC_MESSAGES/volto.po +10 -0
  113. package/locales/ta.json +1 -1
  114. package/locales/te/LC_MESSAGES/volto.po +10 -0
  115. package/locales/te.json +1 -1
  116. package/locales/th/LC_MESSAGES/volto.po +10 -0
  117. package/locales/th.json +1 -1
  118. package/locales/to/LC_MESSAGES/volto.po +10 -0
  119. package/locales/to.json +1 -1
  120. package/locales/tr/LC_MESSAGES/volto.po +35 -25
  121. package/locales/tr.json +1 -1
  122. package/locales/uk/LC_MESSAGES/volto.po +10 -0
  123. package/locales/uk.json +1 -1
  124. package/locales/vi/LC_MESSAGES/volto.po +10 -0
  125. package/locales/vi.json +1 -1
  126. package/locales/volto.pot +11 -1
  127. package/locales/zh_CN/LC_MESSAGES/volto.po +10 -0
  128. package/locales/zh_CN.json +1 -1
  129. package/locales/zh_Hant/LC_MESSAGES/volto.po +10 -0
  130. package/locales/zh_Hant.json +1 -1
  131. package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +10 -0
  132. package/locales/zh_Hant_HK.json +1 -1
  133. package/package.json +13 -27
  134. package/razzle.config.js +20 -5
  135. package/src/components/manage/Add/Add.jsx +9 -6
  136. package/src/components/manage/Blocks/Title/Edit.jsx +5 -0
  137. package/src/components/manage/Contents/Contents.jsx +8 -2
  138. package/src/components/manage/Controlpanels/Users/UsersControlpanel.jsx +4 -5
  139. package/src/components/manage/Controlpanels/Users/UsersControlpanel.test.jsx +57 -11
  140. package/src/components/manage/Multilingual/CompareLanguages.jsx +10 -10
  141. package/src/components/manage/Multilingual/CreateTranslation.jsx +8 -5
  142. package/src/components/manage/Multilingual/ManageTranslations.jsx +9 -7
  143. package/src/components/manage/Multilingual/TranslationObject.jsx +11 -8
  144. package/src/components/manage/Preferences/PersonalPreferences.jsx +8 -5
  145. package/src/components/manage/Toolbar/Types.crash.test.jsx +46 -0
  146. package/src/components/manage/Toolbar/Types.jsx +9 -7
  147. package/src/components/manage/Widgets/FileWidget.jsx +7 -0
  148. package/src/components/manage/Widgets/FormFieldWrapper.jsx +168 -146
  149. package/src/components/theme/LanguageSelector/LanguageSelector.tsx +7 -5
  150. package/src/components/theme/MultilingualRedirector/MultilingualRedirector.jsx +12 -7
  151. package/src/express-middleware/devproxy.js +3 -1
  152. package/src/express-middleware/files.js +1 -0
  153. package/src/express-middleware/files.test.js +59 -0
  154. package/src/express-middleware/images.js +1 -0
  155. package/src/express-middleware/images.test.js +50 -0
  156. package/src/helpers/Blocks/Blocks.js +6 -6
  157. package/src/helpers/Utils/Utils.jsx +17 -0
  158. package/src/helpers/Utils/Utils.test.jsx +39 -0
  159. package/src/middleware/api.js +7 -3
  160. package/src/reducers/users/users.js +1 -1
  161. package/src/server.jsx +14 -12
  162. package/test-setup-globals-vitest.js +25 -0
  163. package/theme/themes/pastanaga/extras/main.less +0 -2
  164. package/theme/themes/pastanaga/extras/widgets.less +17 -0
  165. package/types/components/manage/Toolbar/Types.crash.test.d.ts +1 -0
  166. package/types/components/manage/Widgets/FormFieldWrapper.d.ts +5 -28
  167. package/types/components/manage/Widgets/index.d.ts +1 -1
  168. package/types/helpers/Utils/Utils.d.ts +1 -0
  169. package/vitest.config.mjs +86 -40
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  }
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "18.32.3",
12
+ "version": "18.33.0",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git@github.com:plone/volto.git"
@@ -34,14 +34,6 @@
34
34
  },
35
35
  "main": "src/index.js",
36
36
  "types": "types/index.d.ts",
37
- "bundlewatch": {
38
- "files": [
39
- {
40
- "path": "build/public/static/js/*.js",
41
- "maxSize": "700kB"
42
- }
43
- ]
44
- },
45
37
  "jest": {
46
38
  "transform": {
47
39
  "^.+\\.js(x)?$": "babel-jest",
@@ -166,16 +158,15 @@
166
158
  "github-slugger": "1.4.0",
167
159
  "history": "4.10.1",
168
160
  "hoist-non-react-statics": "3.3.2",
169
- "http-proxy-middleware": "2.0.1",
161
+ "http-proxy-middleware": "2.0.9",
170
162
  "image-extensions": "1.1.0",
171
- "immutable": "3",
172
163
  "is-hotkey": "0.2.0",
173
164
  "is-url": "1.2.4",
174
165
  "jotai": "2.11.3",
175
166
  "jwt-decode": "2.2.0",
176
167
  "linkify-it": "3.0.2",
177
168
  "locale": "0.1.0",
178
- "lodash": "4.17.21",
169
+ "lodash": "4.17.23",
179
170
  "lodash-move": "1.1.1",
180
171
  "moment": "2.29.4",
181
172
  "object-assign": "4.1.1",
@@ -193,7 +184,7 @@
193
184
  "react-animate-height": "2.0.17",
194
185
  "react-beautiful-dnd": "13.0.0",
195
186
  "react-cookie": "4.1.1",
196
- "react-dates": "21.5.1",
187
+ "react-dates": "21.8.0",
197
188
  "react-detect-click-outside": "1.1.1",
198
189
  "react-dnd": "5.0.0",
199
190
  "react-dnd-html5-backend": "5.0.1",
@@ -229,7 +220,7 @@
229
220
  "rrule": "2.7.1",
230
221
  "semantic-ui-less": "2.4.1",
231
222
  "semantic-ui-react": "2.1.5",
232
- "serialize-javascript": "3.1.0",
223
+ "serialize-javascript": "7.0.4",
233
224
  "slate": "0.100.0",
234
225
  "slate-hyperscript": "0.100.0",
235
226
  "slate-react": "0.98.4",
@@ -241,9 +232,9 @@
241
232
  "url": "^0.11.3",
242
233
  "use-deep-compare-effect": "1.8.1",
243
234
  "uuid": "^8.3.2",
244
- "@plone/registry": "2.7.0",
245
- "@plone/volto-slate": "18.8.1",
246
- "@plone/scripts": "3.10.4"
235
+ "@plone/volto-slate": "18.9.0",
236
+ "@plone/registry": "2.7.1",
237
+ "@plone/scripts": "3.10.5"
247
238
  },
248
239
  "devDependencies": {
249
240
  "@babel/core": "^7.0.0",
@@ -299,7 +290,6 @@
299
290
  "babel-plugin-react-intl": "5.1.17",
300
291
  "babel-plugin-root-import": "6.1.0",
301
292
  "babel-preset-razzle": "4.2.18",
302
- "bundlewatch": "0.3.3",
303
293
  "circular-dependency-plugin": "5.2.2",
304
294
  "css-loader": "5.2.7",
305
295
  "cypress": "13.17.0",
@@ -326,7 +316,7 @@
326
316
  "jiti": "^2.4.2",
327
317
  "jsdom": "^16.7.0",
328
318
  "jsonwebtoken": "9.0.0",
329
- "less": "3.11.1",
319
+ "less": "3.13.1",
330
320
  "less-loader": "11.1.0",
331
321
  "lodash-webpack-plugin": "0.11.6",
332
322
  "mini-css-extract-plugin": "2.7.2",
@@ -346,7 +336,7 @@
346
336
  "react-is": "^18.2.0",
347
337
  "release-it": "^19.0.5",
348
338
  "semver": "^7.5.4",
349
- "start-server-and-test": "1.14.0",
339
+ "start-server-and-test": "2.1.5",
350
340
  "storybook": "^8.6.15",
351
341
  "style-loader": "3.3.1",
352
342
  "stylelint": "^16.3.1",
@@ -356,14 +346,13 @@
356
346
  "svgo": "^3.0.0",
357
347
  "svgo-loader": "3.0.3",
358
348
  "terser-webpack-plugin": "5.3.6",
359
- "tmp": "0.2.1",
360
349
  "ts-jest": "^26.4.2",
361
350
  "ts-loader": "9.4.4",
362
351
  "typescript": "^5.7.3",
363
352
  "use-trace-update": "1.3.2",
364
- "vitest": "^3.0.4",
365
- "wait-on": "6.0.0",
366
- "webpack": "5.90.1",
353
+ "vitest": "^3.2.4",
354
+ "wait-on": "^9.0.4",
355
+ "webpack": "5.105.4",
367
356
  "webpack-bundle-analyzer": "4.10.1",
368
357
  "webpack-dev-server": "4.11.1",
369
358
  "webpack-node-externals": "3.0.0",
@@ -371,9 +360,6 @@
371
360
  "@plone/types": "1.6.1",
372
361
  "@plone/volto-coresandbox": "1.0.0"
373
362
  },
374
- "volta": {
375
- "node": "20.9.0"
376
- },
377
363
  "scripts": {
378
364
  "analyze": "BUNDLE_ANALYZE=true razzle build",
379
365
  "start": "make build-deps && razzle start",
package/razzle.config.js CHANGED
@@ -420,11 +420,26 @@ const defaultModify = ({
420
420
  const addonExtenders = registry.getAddonExtenders().map((m) => require(m));
421
421
 
422
422
  const defaultPlugins = [
423
- { object: require('./webpack-plugins/webpack-less-plugin')({ registry }) },
424
- { object: require('./webpack-plugins/webpack-svg-plugin') },
425
- { object: require('./webpack-plugins/webpack-bundle-analyze-plugin') },
426
- { object: require('./jest-extender-plugin') },
427
- 'scss',
423
+ {
424
+ name: 'less',
425
+ object: require('./webpack-plugins/webpack-less-plugin')({ registry }),
426
+ },
427
+ {
428
+ name: 'svg',
429
+ object: require('./webpack-plugins/webpack-svg-plugin'),
430
+ },
431
+ {
432
+ name: 'bundle-analyze',
433
+ object: require('./webpack-plugins/webpack-bundle-analyze-plugin'),
434
+ },
435
+ {
436
+ name: 'jest-extender',
437
+ object: require('./jest-extender-plugin'),
438
+ },
439
+ {
440
+ name: 'scss',
441
+ object: require('razzle-plugin-scss'),
442
+ },
428
443
  ];
429
444
 
430
445
  const plugins = addonExtenders.reduce(
@@ -241,11 +241,13 @@ class Add extends Component {
241
241
  if (this.props.location?.state?.translationOf) {
242
242
  const language = this.props.location.state.languageFrom;
243
243
  const langFileName = toGettextLang(language);
244
- import(
245
- /* @vite-ignore */ '@root/../locales/' + langFileName + '.json'
246
- ).then((locale) => {
247
- this.props.changeLanguage(language, locale.default);
248
- });
244
+ import(/* @vite-ignore */ '@root/../locales/' + langFileName + '.json')
245
+ .then((locale) => {
246
+ this.props.changeLanguage(language, locale.default);
247
+ })
248
+ .catch(() => {
249
+ this.props.changeLanguage(language, {});
250
+ });
249
251
  this.props.history.push(this.props.location?.state?.translationOf);
250
252
  } else {
251
253
  this.props.history.push(getBaseUrl(this.props.pathname));
@@ -269,7 +271,8 @@ class Add extends Component {
269
271
  const translationObject = this.props.location?.state?.translationObject;
270
272
 
271
273
  const translateTo = translationObject
272
- ? langmap?.[this.props.location?.state?.language]?.nativeName
274
+ ? langmap?.[this.props.location?.state?.language]?.nativeName ||
275
+ this.props.location?.state?.language
273
276
  : null;
274
277
 
275
278
  // Get initial blocks from local config, if any
@@ -17,6 +17,10 @@ const messages = defineMessages({
17
17
  id: 'Type the title…',
18
18
  defaultMessage: 'Type the title…',
19
19
  },
20
+ editable_title: {
21
+ id: 'Content title',
22
+ defaultMessage: 'Content title',
23
+ },
20
24
  });
21
25
 
22
26
  function usePrevious(value) {
@@ -159,6 +163,7 @@ export const TitleBlockEdit = (props) => {
159
163
  renderElement={renderElement}
160
164
  onFocus={handleFocus}
161
165
  aria-multiline="false"
166
+ aria-label={intl.formatMessage(messages.editable_title)}
162
167
  ></Editable>
163
168
  </Slate>
164
169
  );
@@ -504,11 +504,17 @@ class Contents extends Component {
504
504
  }
505
505
 
506
506
  if (this.props.deleteRequest.loading && nextProps.deleteRequest.error) {
507
+ const deleteErrorMessageTitle = this.props.intl.formatMessage(
508
+ messages.deleteError,
509
+ );
510
+ const deleteErrorMessageContent =
511
+ nextProps.deleteRequest.error?.response?.body?.message ||
512
+ deleteErrorMessageTitle;
507
513
  this.props.toastify.toast.error(
508
514
  <Toast
509
515
  error
510
- title={this.props.intl.formatMessage(messages.deleteError)}
511
- content={this.props.intl.formatMessage(messages.deleteError)}
516
+ title={deleteErrorMessageTitle}
517
+ content={deleteErrorMessageContent}
512
518
  />,
513
519
  );
514
520
  }
@@ -25,6 +25,7 @@ import { Link } from 'react-router-dom';
25
25
  import Helmet from '@plone/volto/helpers/Helmet/Helmet';
26
26
  import { messages } from '@plone/volto/helpers/MessageLabels/MessageLabels';
27
27
  import { isManager, canAssignGroup } from '@plone/volto/helpers/User/User';
28
+ import { getErrorMessage } from '@plone/volto/helpers/Utils/Utils';
28
29
  import clearSVG from '@plone/volto/icons/clear.svg';
29
30
  import addUserSvg from '@plone/volto/icons/add-user.svg';
30
31
  import saveSVG from '@plone/volto/icons/save.svg';
@@ -351,9 +352,7 @@ const UsersControlpanel = () => {
351
352
  })
352
353
  .catch((error) => {
353
354
  // Handle error
354
- setAddUserError(
355
- error.response?.body?.error?.message || 'Error creating user',
356
- );
355
+ setAddUserError(getErrorMessage(error));
357
356
  });
358
357
  }
359
358
  },
@@ -417,11 +416,11 @@ const UsersControlpanel = () => {
417
416
  /**
418
417
  * Handle Errors after createUser()
419
418
  *
420
- * @param {object} error object. Requires the property .message
419
+ * @param {object} error object
421
420
  * @returns {undefined}
422
421
  */
423
422
  const onAddUserError = useCallback((error) => {
424
- setAddUserError(error.response.body.error.message);
423
+ setAddUserError(getErrorMessage(error));
425
424
  }, []);
426
425
 
427
426
  /**
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { render, act } from '@testing-library/react';
2
+ import { render, waitFor } from '@testing-library/react';
3
3
  import configureStore from 'redux-mock-store';
4
4
  import { Provider } from 'react-intl-redux';
5
5
  import { MemoryRouter } from 'react-router-dom';
@@ -39,17 +39,63 @@ describe('UsersControlpanel', () => {
39
39
  messages: {},
40
40
  },
41
41
  });
42
- const { container } = await act(async () => {
43
- return render(
44
- <Provider store={store}>
45
- <MemoryRouter initialEntries={['/controlpanel/users']}>
46
- <UsersControlpanel />
47
- <div id="toolbar"></div>
48
- </MemoryRouter>
49
- </Provider>,
50
- );
51
- });
42
+ const { container } = render(
43
+ <Provider store={store}>
44
+ <MemoryRouter initialEntries={['/controlpanel/users']}>
45
+ <UsersControlpanel />
46
+ <div id="toolbar"></div>
47
+ </MemoryRouter>
48
+ </Provider>,
49
+ );
50
+ await waitFor(() => {});
52
51
 
53
52
  expect(container).toMatchSnapshot();
54
53
  });
54
+
55
+ it('handles createRequest error when response body has only message', async () => {
56
+ const store = mockStore({
57
+ userSession: {
58
+ token: jwt.sign({ sub: 'john' }, 'secret'),
59
+ },
60
+ roles: { roles: [] },
61
+ users: {
62
+ users: [],
63
+ create: {
64
+ loading: false,
65
+ error: {
66
+ response: { body: { message: 'SMTP relay access denied' } },
67
+ },
68
+ },
69
+ user: {
70
+ roles: ['Manager'],
71
+ '@id': 'admin',
72
+ },
73
+ },
74
+ groups: {
75
+ groups: [],
76
+ create: { loading: false },
77
+ },
78
+ authRole: {
79
+ authenticatedRole: [],
80
+ },
81
+ intl: {
82
+ locale: 'en',
83
+ messages: {},
84
+ },
85
+ });
86
+
87
+ render(
88
+ <Provider store={store}>
89
+ <MemoryRouter initialEntries={['/controlpanel/users']}>
90
+ <UsersControlpanel />
91
+ <div id="toolbar"></div>
92
+ </MemoryRouter>
93
+ </Provider>,
94
+ );
95
+ await waitFor(() => {});
96
+
97
+ // If the component attempted to read a missing property it would throw.
98
+ // Reaching this line means the error shape was handled without exceptions.
99
+ expect(true).toBe(true);
100
+ });
55
101
  });
@@ -57,33 +57,33 @@ const CompareLanguagesMenu = ({
57
57
  {comparingLanguage === t.language ? (
58
58
  <button
59
59
  aria-label={`${intl.formatMessage(messages.stop_compare)} ${
60
- langmap[t.language].nativeName
60
+ langmap[t.language]?.nativeName || t.language
61
61
  }`}
62
62
  title={`${intl.formatMessage(messages.stop_compare)} ${
63
- langmap[t.language].nativeName
63
+ langmap[t.language]?.nativeName || t.language
64
64
  }`}
65
65
  onClick={() => {
66
66
  setComparingLanguage(null);
67
67
  closeMenu();
68
68
  }}
69
69
  >
70
- {langmap[t.language].nativeName}
70
+ {langmap[t.language]?.nativeName || t.language}
71
71
  <Icon name={clearSVG} size="30px" />
72
72
  </button>
73
73
  ) : (
74
74
  <button
75
- aria-label={`${intl.formatMessage(
76
- messages.compare_to,
77
- )} ${langmap[t.language].nativeName.toLowerCase()}`}
78
- title={`${intl.formatMessage(
79
- messages.compare_to,
80
- )} ${langmap[t.language].nativeName.toLowerCase()}`}
75
+ aria-label={`${intl.formatMessage(messages.compare_to)} ${(
76
+ langmap[t.language]?.nativeName || t.language
77
+ ).toLowerCase()}`}
78
+ title={`${intl.formatMessage(messages.compare_to)} ${(
79
+ langmap[t.language]?.nativeName || t.language
80
+ ).toLowerCase()}`}
81
81
  onClick={() => {
82
82
  setComparingLanguage(t.language);
83
83
  closeMenu();
84
84
  }}
85
85
  >
86
- {langmap[t.language].nativeName}
86
+ {langmap[t.language]?.nativeName || t.language}
87
87
  </button>
88
88
  )}
89
89
  </li>
@@ -32,11 +32,14 @@ const CreateTranslation = (props) => {
32
32
  // We change the interface language
33
33
  if (config.settings.supportedLanguages.includes(language)) {
34
34
  const langFileName = toGettextLang(language);
35
- import(
36
- /* @vite-ignore */ '@root/../locales/' + langFileName + '.json'
37
- ).then((locale) => {
38
- dispatch(changeLanguage(language, locale.default));
39
- });
35
+ import(/* @vite-ignore */ '@root/../locales/' + langFileName + '.json')
36
+ .then((locale) => {
37
+ dispatch(changeLanguage(language, locale.default));
38
+ })
39
+ .catch(() => {
40
+ // If locale file doesn't exist, still switch language with empty locale
41
+ dispatch(changeLanguage(language, {}));
42
+ });
40
43
  }
41
44
  };
42
45
  // On mount only
@@ -176,7 +176,7 @@ const ManageTranslations = (props) => {
176
176
  <FormattedMessage
177
177
  id="Manage translations for {title}"
178
178
  defaultMessage="Manage translations for {title}"
179
- values={{ title: <q>{content.title}</q> }}
179
+ values={{ title: <q>{content?.title || ''}</q> }}
180
180
  />
181
181
  </Segment>
182
182
  {content && (
@@ -193,9 +193,9 @@ const ManageTranslations = (props) => {
193
193
  <Table.Row key={lang}>
194
194
  <Table.Cell collapsing>
195
195
  {lang === content.language.token ? (
196
- <strong>{langmap[lang].nativeName}</strong>
196
+ <strong>{langmap?.[lang]?.nativeName || lang}</strong>
197
197
  ) : (
198
- langmap[lang].nativeName
198
+ langmap?.[lang]?.nativeName || lang
199
199
  )}
200
200
  </Table.Cell>
201
201
  <Table.Cell>
@@ -233,7 +233,9 @@ const ManageTranslations = (props) => {
233
233
  <Button
234
234
  aria-label={`${intl.formatMessage(
235
235
  messages.unlink,
236
- )} ${langmap[lang].nativeName.toLowerCase()}`}
236
+ )} ${(
237
+ langmap?.[lang]?.nativeName || lang
238
+ ).toLowerCase()}`}
237
239
  basic
238
240
  icon
239
241
  disabled={lang === content.language.token}
@@ -252,9 +254,9 @@ const ManageTranslations = (props) => {
252
254
  ) : (
253
255
  <Button.Group>
254
256
  <Button
255
- aria-label={`${intl.formatMessage(
256
- messages.link,
257
- )} ${langmap[lang].nativeName.toLowerCase()}`}
257
+ aria-label={`${intl.formatMessage(messages.link)} ${(
258
+ langmap?.[lang]?.nativeName || lang
259
+ ).toLowerCase()}`}
258
260
  basic
259
261
  icon
260
262
  disabled={lang === content.language.token}
@@ -48,12 +48,15 @@ const TranslationObject = ({
48
48
  let lang =
49
49
  config.settings.supportedLanguages[Object.keys(locales).length];
50
50
  const langFileName = toGettextLang(lang);
51
- import(
52
- /* @vite-ignore */ '@root/../locales/' + langFileName + '.json'
53
- ).then((locale) => {
54
- setLocales({ ...locales, [toReactIntlLang(lang)]: locale.default });
55
- setLoadingLocale(false);
56
- });
51
+ import(/* @vite-ignore */ '@root/../locales/' + langFileName + '.json')
52
+ .then((locale) => {
53
+ setLocales({ ...locales, [toReactIntlLang(lang)]: locale.default });
54
+ setLoadingLocale(false);
55
+ })
56
+ .catch(() => {
57
+ setLocales({ ...locales, [toReactIntlLang(lang)]: {} });
58
+ setLoadingLocale(false);
59
+ });
57
60
  }
58
61
  }, [loadingLocale, locales]);
59
62
 
@@ -83,7 +86,7 @@ const TranslationObject = ({
83
86
  active={activeMenu === 'language'}
84
87
  onClick={handleMenuClick}
85
88
  >
86
- {langmap[lang].nativeName}
89
+ {langmap?.[lang]?.nativeName || lang}
87
90
  </Menu.Item>
88
91
  {visual && (
89
92
  <Menu.Item
@@ -107,7 +110,7 @@ const TranslationObject = ({
107
110
  hideActions
108
111
  pathname={flattenToAppURL(translationObject['@id'])}
109
112
  visual={visual}
110
- title={langmap[lang].nativeName}
113
+ title={langmap?.[lang]?.nativeName || lang}
111
114
  loading={false}
112
115
  isFormSelected={isFormSelected}
113
116
  onSelectForm={onSelectForm}
@@ -53,11 +53,14 @@ const PersonalPreferences = (props) => {
53
53
  let language = data.language || 'en';
54
54
  if (config.settings.supportedLanguages.includes(language)) {
55
55
  const langFileName = toGettextLang(language);
56
- import(
57
- /* @vite-ignore */ '@root/../locales/' + langFileName + '.json'
58
- ).then((locale) => {
59
- dispatch(changeLanguage(language, locale.default));
60
- });
56
+ import(/* @vite-ignore */ '@root/../locales/' + langFileName + '.json')
57
+ .then((locale) => {
58
+ dispatch(changeLanguage(language, locale.default));
59
+ })
60
+ .catch(() => {
61
+ // If locale file doesn't exist, still switch language with empty locale
62
+ dispatch(changeLanguage(language, {}));
63
+ });
61
64
  }
62
65
  toast.success(<Toast success title={intl.formatMessage(messages.saved)} />);
63
66
  closeMenu();
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { Provider } from 'react-intl-redux';
4
+ import { MemoryRouter } from 'react-router-dom';
5
+ import configureStore from 'redux-mock-store';
6
+ import config from '@plone/volto/registry';
7
+ import Types from './Types';
8
+
9
+ config.settings.isMultilingual = true;
10
+ config.settings.supportedLanguages = ['en', 'missing-lang'];
11
+
12
+ const mockStore = configureStore();
13
+
14
+ describe('Types', () => {
15
+ it('should not crash if a language is not in langmap', () => {
16
+ const store = mockStore({
17
+ intl: {
18
+ locale: 'en',
19
+ messages: {},
20
+ },
21
+ types: {
22
+ types: [],
23
+ },
24
+ });
25
+
26
+ const content = {
27
+ '@type': 'Folder',
28
+ '@id': '/folder',
29
+ '@components': {
30
+ translations: {
31
+ items: [],
32
+ },
33
+ },
34
+ };
35
+
36
+ const { getByText } = render(
37
+ <Provider store={store}>
38
+ <MemoryRouter>
39
+ <Types pathname="/folder" types={[]} content={content} />
40
+ </MemoryRouter>
41
+ </Provider>,
42
+ );
43
+
44
+ expect(getByText('Translate to missing-lang')).toBeInTheDocument();
45
+ });
46
+ });
@@ -15,7 +15,7 @@ import config from '@plone/volto/registry';
15
15
  const Types = ({ types, pathname, content, currentLanguage }) => {
16
16
  const { settings } = config;
17
17
  return types.length > 0 ||
18
- (settings.isMultilingual && content['@components'].translations) ? (
18
+ (settings.isMultilingual && content?.['@components']?.translations) ? (
19
19
  <div className="menu-more pastanaga-menu">
20
20
  {types.length > 0 && (
21
21
  <>
@@ -54,14 +54,14 @@ const Types = ({ types, pathname, content, currentLanguage }) => {
54
54
  </>
55
55
  )}
56
56
  {settings.isMultilingual &&
57
- content['@components'].translations &&
57
+ content?.['@components']?.translations &&
58
58
  (() => {
59
59
  const translationsLeft = filter(
60
60
  settings.supportedLanguages,
61
61
  (lang) =>
62
62
  !Boolean(
63
- content['@components'].translations &&
64
- find(content['@components'].translations.items, {
63
+ content?.['@components']?.translations &&
64
+ find(content?.['@components']?.translations?.items, {
65
65
  language: lang,
66
66
  }),
67
67
  ) && toBackendLang(currentLanguage) !== lang,
@@ -84,8 +84,8 @@ const Types = ({ types, pathname, content, currentLanguage }) => {
84
84
  to={{
85
85
  pathname: `${pathname}/create-translation`,
86
86
  state: {
87
- type: content['@type'],
88
- translationOf: flattenToAppURL(content['@id']),
87
+ type: content?.['@type'],
88
+ translationOf: flattenToAppURL(content?.['@id']),
89
89
  language: lang,
90
90
  },
91
91
  }}
@@ -95,7 +95,9 @@ const Types = ({ types, pathname, content, currentLanguage }) => {
95
95
  id="Translate to {lang}"
96
96
  defaultMessage="Translate to {lang}"
97
97
  values={{
98
- lang: langmap[lang].nativeName.toLowerCase(),
98
+ lang: (
99
+ langmap?.[lang]?.nativeName || lang
100
+ ).toLowerCase(),
99
101
  }}
100
102
  />
101
103
  </Link>
@@ -59,6 +59,10 @@ const messages = defineMessages({
59
59
  id: 'File is not of the accepted type {accept}',
60
60
  defaultMessage: 'File is not of the accepted type {accept}',
61
61
  },
62
+ dragAndDropActionA11y: {
63
+ id: 'Press Enter to browse files from your computer.',
64
+ defaultMessage: 'Press Enter to browse files from your computer.',
65
+ },
62
66
  });
63
67
 
64
68
  /**
@@ -205,6 +209,9 @@ const FileWidget = (props) => {
205
209
  {value
206
210
  ? intl.formatMessage(messages.replaceFile)
207
211
  : intl.formatMessage(messages.addNewFile)}
212
+ <span className="visually-hidden">
213
+ {intl.formatMessage(messages.dragAndDropActionA11y)}
214
+ </span>
208
215
  </label>
209
216
  <input
210
217
  {...getInputProps({