@plone/volto 18.32.4 → 18.33.1

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 +43 -0
  2. package/locales/af/LC_MESSAGES/volto.po +12 -2
  3. package/locales/af.json +1 -1
  4. package/locales/ar/LC_MESSAGES/volto.po +12 -2
  5. package/locales/ar.json +1 -1
  6. package/locales/bg/LC_MESSAGES/volto.po +12 -2
  7. package/locales/bg.json +1 -1
  8. package/locales/bn/LC_MESSAGES/volto.po +12 -2
  9. package/locales/bn.json +1 -1
  10. package/locales/ca/LC_MESSAGES/volto.po +11 -1
  11. package/locales/ca.json +1 -1
  12. package/locales/cs/LC_MESSAGES/volto.po +12 -2
  13. package/locales/cs.json +1 -1
  14. package/locales/cy/LC_MESSAGES/volto.po +12 -2
  15. package/locales/cy.json +1 -1
  16. package/locales/da/LC_MESSAGES/volto.po +12 -2
  17. package/locales/da.json +1 -1
  18. package/locales/de/LC_MESSAGES/volto.po +11 -1
  19. package/locales/de.json +1 -1
  20. package/locales/el/LC_MESSAGES/volto.po +12 -2
  21. package/locales/el.json +1 -1
  22. package/locales/en/LC_MESSAGES/volto.po +12 -2
  23. package/locales/en.json +1 -1
  24. package/locales/en_AU/LC_MESSAGES/volto.po +12 -2
  25. package/locales/en_AU.json +1 -1
  26. package/locales/en_GB/LC_MESSAGES/volto.po +12 -2
  27. package/locales/en_GB.json +1 -1
  28. package/locales/eo/LC_MESSAGES/volto.po +12 -2
  29. package/locales/eo.json +1 -1
  30. package/locales/es/LC_MESSAGES/volto.po +24 -14
  31. package/locales/es.json +1 -1
  32. package/locales/et/LC_MESSAGES/volto.po +12 -2
  33. package/locales/et.json +1 -1
  34. package/locales/eu/LC_MESSAGES/volto.po +23 -13
  35. package/locales/eu.json +1 -1
  36. package/locales/fa/LC_MESSAGES/volto.po +12 -2
  37. package/locales/fa.json +1 -1
  38. package/locales/fi/LC_MESSAGES/volto.po +11 -1
  39. package/locales/fi.json +1 -1
  40. package/locales/fr/LC_MESSAGES/volto.po +11 -1
  41. package/locales/fr.json +1 -1
  42. package/locales/fu/LC_MESSAGES/volto.po +12 -2
  43. package/locales/fu.json +1 -1
  44. package/locales/gl/LC_MESSAGES/volto.po +1014 -1003
  45. package/locales/gl.json +1 -1
  46. package/locales/he/LC_MESSAGES/volto.po +12 -2
  47. package/locales/he.json +1 -1
  48. package/locales/hi/LC_MESSAGES/volto.po +11 -1
  49. package/locales/hi.json +1 -1
  50. package/locales/hr/LC_MESSAGES/volto.po +12 -2
  51. package/locales/hr.json +1 -1
  52. package/locales/hu/LC_MESSAGES/volto.po +12 -2
  53. package/locales/hu.json +1 -1
  54. package/locales/hy/LC_MESSAGES/volto.po +12 -2
  55. package/locales/hy.json +1 -1
  56. package/locales/id/LC_MESSAGES/volto.po +12 -2
  57. package/locales/id.json +1 -1
  58. package/locales/it/LC_MESSAGES/volto.po +14 -4
  59. package/locales/it.json +1 -1
  60. package/locales/ja/LC_MESSAGES/volto.po +11 -1
  61. package/locales/ja.json +1 -1
  62. package/locales/ka/LC_MESSAGES/volto.po +12 -2
  63. package/locales/ka.json +1 -1
  64. package/locales/kn/LC_MESSAGES/volto.po +12 -2
  65. package/locales/kn.json +1 -1
  66. package/locales/ko/LC_MESSAGES/volto.po +12 -2
  67. package/locales/ko.json +1 -1
  68. package/locales/lt/LC_MESSAGES/volto.po +12 -2
  69. package/locales/lt.json +1 -1
  70. package/locales/lv/LC_MESSAGES/volto.po +12 -2
  71. package/locales/lv.json +1 -1
  72. package/locales/mi/LC_MESSAGES/volto.po +12 -2
  73. package/locales/mi.json +1 -1
  74. package/locales/mk/LC_MESSAGES/volto.po +12 -2
  75. package/locales/mk.json +1 -1
  76. package/locales/my/LC_MESSAGES/volto.po +12 -2
  77. package/locales/my.json +1 -1
  78. package/locales/nb_NO/LC_MESSAGES/volto.po +12 -2
  79. package/locales/nb_NO.json +1 -1
  80. package/locales/nl/LC_MESSAGES/volto.po +26 -16
  81. package/locales/nl.json +1 -1
  82. package/locales/nn/LC_MESSAGES/volto.po +12 -2
  83. package/locales/nn.json +1 -1
  84. package/locales/pl/LC_MESSAGES/volto.po +12 -2
  85. package/locales/pl.json +1 -1
  86. package/locales/pt/LC_MESSAGES/volto.po +11 -1
  87. package/locales/pt.json +1 -1
  88. package/locales/pt_BR/LC_MESSAGES/volto.po +11 -1
  89. package/locales/pt_BR.json +1 -1
  90. package/locales/rm/LC_MESSAGES/volto.po +12 -2
  91. package/locales/rm.json +1 -1
  92. package/locales/ro/LC_MESSAGES/volto.po +11 -1
  93. package/locales/ro.json +1 -1
  94. package/locales/ru/LC_MESSAGES/volto.po +11 -1
  95. package/locales/ru.json +1 -1
  96. package/locales/sk/LC_MESSAGES/volto.po +12 -2
  97. package/locales/sk.json +1 -1
  98. package/locales/sl/LC_MESSAGES/volto.po +12 -2
  99. package/locales/sl.json +1 -1
  100. package/locales/sm/LC_MESSAGES/volto.po +12 -2
  101. package/locales/sm.json +1 -1
  102. package/locales/sq/LC_MESSAGES/volto.po +12 -2
  103. package/locales/sq.json +1 -1
  104. package/locales/sr/LC_MESSAGES/volto.po +12 -2
  105. package/locales/sr.json +1 -1
  106. package/locales/sr@cyrl/LC_MESSAGES/volto.po +12 -2
  107. package/locales/sr@cyrl.json +1 -1
  108. package/locales/sr@latn/LC_MESSAGES/volto.po +12 -2
  109. package/locales/sr@latn.json +1 -1
  110. package/locales/sv/LC_MESSAGES/volto.po +11 -1
  111. package/locales/sv.json +1 -1
  112. package/locales/ta/LC_MESSAGES/volto.po +11 -1
  113. package/locales/ta.json +1 -1
  114. package/locales/te/LC_MESSAGES/volto.po +12 -2
  115. package/locales/te.json +1 -1
  116. package/locales/th/LC_MESSAGES/volto.po +12 -2
  117. package/locales/th.json +1 -1
  118. package/locales/to/LC_MESSAGES/volto.po +12 -2
  119. package/locales/to.json +1 -1
  120. package/locales/tr/LC_MESSAGES/volto.po +37 -27
  121. package/locales/tr.json +1 -1
  122. package/locales/uk/LC_MESSAGES/volto.po +12 -2
  123. package/locales/uk.json +1 -1
  124. package/locales/vi/LC_MESSAGES/volto.po +12 -2
  125. package/locales/vi.json +1 -1
  126. package/locales/volto.pot +12 -2
  127. package/locales/zh_CN/LC_MESSAGES/volto.po +11 -1
  128. package/locales/zh_CN.json +1 -1
  129. package/locales/zh_Hant/LC_MESSAGES/volto.po +12 -2
  130. package/locales/zh_Hant.json +1 -1
  131. package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +12 -2
  132. package/locales/zh_Hant_HK.json +1 -1
  133. package/package.json +13 -26
  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/Controlpanels/Users/UsersControlpanel.jsx +4 -5
  138. package/src/components/manage/Controlpanels/Users/UsersControlpanel.test.jsx +57 -11
  139. package/src/components/manage/Multilingual/CompareLanguages.jsx +10 -10
  140. package/src/components/manage/Multilingual/CreateTranslation.jsx +8 -5
  141. package/src/components/manage/Multilingual/ManageTranslations.jsx +9 -7
  142. package/src/components/manage/Multilingual/TranslationObject.jsx +11 -8
  143. package/src/components/manage/Preferences/PersonalPreferences.jsx +8 -5
  144. package/src/components/manage/Sharing/Sharing.jsx +11 -3
  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/RegistryImageWidget.jsx +1 -1
  149. package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx +1 -0
  150. package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx +30 -0
  151. package/src/components/theme/LanguageSelector/LanguageSelector.tsx +7 -5
  152. package/src/components/theme/MultilingualRedirector/MultilingualRedirector.jsx +12 -7
  153. package/src/express-middleware/devproxy.js +3 -1
  154. package/src/express-middleware/files.js +1 -0
  155. package/src/express-middleware/files.test.js +59 -0
  156. package/src/express-middleware/images.js +1 -0
  157. package/src/express-middleware/images.test.js +50 -0
  158. package/src/helpers/Blocks/Blocks.js +6 -6
  159. package/src/helpers/Utils/Utils.jsx +17 -0
  160. package/src/helpers/Utils/Utils.test.jsx +39 -0
  161. package/src/middleware/api.js +7 -3
  162. package/src/server.jsx +14 -12
  163. package/test-setup-globals-vitest.js +25 -0
  164. package/theme/themes/pastanaga/collections/form.overrides +1 -1
  165. package/theme/themes/pastanaga/elements/segment.variables +1 -1
  166. package/theme/themes/pastanaga/extras/widgets.less +17 -0
  167. package/types/components/manage/Toolbar/Types.crash.test.d.ts +1 -0
  168. package/types/helpers/Utils/Utils.d.ts +1 -0
  169. package/vitest.config.mjs +86 -40
@@ -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();
@@ -473,13 +473,21 @@ class SharingComponent extends Component {
473
473
  <p className="help">
474
474
  <FormattedMessage
475
475
  id="By default, permissions from the container of this item are inherited. If you disable this, only the explicitly defined sharing permissions will be valid. In the overview, the symbol {inherited} indicates an inherited value. Similarly, the symbol {global} indicates a global role, which is managed by the site administrator."
476
- defaultMessage="By default, permissions from the container of this item are inherited. If you disable this, only the explicitly defined sharing permissions will be valid. In the overview, the symbol {inherited} indicates an inherited value. Similarly, the symbol {global} indicates a global role, which is managed by the site administrator."
476
+ defaultMessage="By default, permissions from the container of this item are inherited. If you disable this, only the explicitly defined sharing permissions will be valid. In the overview, inherited values are explicitly labeled as 'Inherited value' and receive a green check mark {inherited}. Similarly, roles managed by the site administrator are labeled as 'Global role' and receive a blue check mark {global}."
477
477
  values={{
478
478
  inherited: (
479
- <IconOld name="check circle outline" color="green" />
479
+ <IconOld
480
+ aria-hidden="true"
481
+ name="check circle outline"
482
+ color="green"
483
+ />
480
484
  ),
481
485
  global: (
482
- <IconOld name="check circle outline" color="blue" />
486
+ <IconOld
487
+ aria-hidden="true"
488
+ name="check circle outline"
489
+ color="blue"
490
+ />
483
491
  ),
484
492
  }}
485
493
  />
@@ -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({
@@ -99,7 +99,7 @@ const RegistryImageWidget = (props) => {
99
99
 
100
100
  readAsDataURL(file).then((data) => {
101
101
  const fields = data.match(/^data:(.*);(.*),(.*)$/);
102
- onChange(id, `filenameb64:${btoa(file.name)};datab64:${fields[3]}}`);
102
+ onChange(id, `filenameb64:${btoa(file.name)};datab64:${fields[3]}`);
103
103
  });
104
104
 
105
105
  let reader = new FileReader();
@@ -8,6 +8,7 @@ const AlternateHrefLangs = (props) => {
8
8
  <Helmet>
9
9
  {config.settings.isMultilingual &&
10
10
  content['@components']?.translations?.items &&
11
+ content.language?.token &&
11
12
  [
12
13
  ...content['@components']?.translations?.items,
13
14
  { '@id': content['@id'], language: content.language.token },
@@ -36,6 +36,36 @@ describe('AlternateHrefLangs', () => {
36
36
  expect(helmetLinks.length).toBe(0);
37
37
  });
38
38
 
39
+ it('multilingual site, content without language field, renders nothing', () => {
40
+ config.settings.publicURL = 'https://plone.org';
41
+ config.settings.supportedLanguages = ['en', 'es'];
42
+
43
+ const content = {
44
+ '@id': 'http://localhost:8080/Plone/en/newsroom/news',
45
+ '@components': {
46
+ translations: {
47
+ items: [{ '@id': 'http://localhost:8080/Plone/es', language: 'es' }],
48
+ },
49
+ },
50
+ };
51
+
52
+ const store = mockStore({
53
+ intl: {
54
+ locale: 'en',
55
+ messages: {},
56
+ },
57
+ });
58
+
59
+ renderer.create(
60
+ <Provider store={store}>
61
+ <AlternateHrefLangs content={content} />
62
+ </Provider>,
63
+ );
64
+
65
+ const helmetLinks = Helmet.peek().linkTags;
66
+ expect(helmetLinks.length).toBe(0);
67
+ });
68
+
39
69
  it('multilingual site, with some translations', () => {
40
70
  config.settings.publicURL = 'https://plone.org';
41
71
  config.settings.isMultilingual = true;
@@ -60,18 +60,20 @@ const LanguageSelector = ({
60
60
  );
61
61
  return (
62
62
  <Link
63
- aria-label={`${intl.formatMessage(
64
- messages.switchLanguageTo,
65
- )} ${langmap[langKey].nativeName.toLowerCase()}`}
63
+ aria-label={`${intl.formatMessage(messages.switchLanguageTo)} ${(
64
+ langmap?.[langKey]?.nativeName ||
65
+ langKey ||
66
+ ''
67
+ ).toLowerCase()}`}
66
68
  className={cx({ selected: toReactIntlLang(lang) === currentLang })}
67
69
  to={translation ? flattenToAppURL(translation['@id']) : `/${lang}`}
68
- title={langmap[langKey].nativeName}
70
+ title={langmap?.[langKey]?.nativeName || langKey}
69
71
  onClick={() => {
70
72
  onClickAction();
71
73
  }}
72
74
  key={`language-selector-${lang}`}
73
75
  >
74
- {langmap[langKey].nativeName}
76
+ {langmap?.[langKey]?.nativeName || langKey}
75
77
  </Link>
76
78
  );
77
79
  })}
@@ -24,13 +24,18 @@ const MultilingualRedirector = (props) => {
24
24
  let mounted = true;
25
25
  if (settings.isMultilingual && pathname === '/') {
26
26
  const langFileName = toGettextLang(redirectToLanguage);
27
- import(
28
- /* @vite-ignore */ '@root/../locales/' + langFileName + '.json'
29
- ).then((locale) => {
30
- if (mounted) {
31
- dispatch(changeLanguage(redirectToLanguage, locale.default));
32
- }
33
- });
27
+ import(/* @vite-ignore */ '@root/../locales/' + langFileName + '.json')
28
+ .then((locale) => {
29
+ if (mounted) {
30
+ dispatch(changeLanguage(redirectToLanguage, locale.default));
31
+ }
32
+ })
33
+ .catch(() => {
34
+ // If locale file doesn't exist, still switch language with empty locale
35
+ if (mounted) {
36
+ dispatch(changeLanguage(redirectToLanguage, {}));
37
+ }
38
+ });
34
39
  }
35
40
  return () => {
36
41
  mounted = false;
@@ -78,11 +78,13 @@ export default function devProxyMiddleware() {
78
78
  },
79
79
  pathRewrite: (path, req) => {
80
80
  const { apiPathURL, instancePath } = getEnv();
81
+ const port =
82
+ apiPathURL.port || (apiPathURL.protocol === 'https:' ? 443 : 80);
81
83
  const target =
82
84
  config.settings.proxyRewriteTarget ||
83
85
  `/VirtualHostBase/${apiPathURL.protocol.slice(0, -1)}/${
84
86
  apiPathURL.hostname
85
- }:${apiPathURL.port}${instancePath}/++api++/VirtualHostRoot`;
87
+ }:${port}${instancePath}/++api++/VirtualHostRoot`;
86
88
 
87
89
  return `${target}${path.replace('/++api++', '')}`;
88
90
  },
@@ -3,6 +3,7 @@ import { getAPIResourceWithAuth } from '@plone/volto/helpers/Api/APIResourceWith
3
3
 
4
4
  const HEADERS = [
5
5
  'accept-ranges',
6
+ 'cache-status',
6
7
  'cache-control',
7
8
  'content-disposition',
8
9
  'content-range',
@@ -0,0 +1,59 @@
1
+ import filesMiddleware from './files';
2
+ import { getAPIResourceWithAuth } from '@plone/volto/helpers/Api/APIResourceWithAuth';
3
+
4
+ vi.mock('@plone/volto/helpers/Api/APIResourceWithAuth', () => ({
5
+ getAPIResourceWithAuth: vi.fn(),
6
+ }));
7
+
8
+ describe('files middleware', () => {
9
+ it('handles requests containing @@download and forwards headers/body', async () => {
10
+ getAPIResourceWithAuth.mockResolvedValue({
11
+ headers: {
12
+ 'content-type': 'text/plain',
13
+ 'cache-status': 'Souin; hit; ttl=664',
14
+ },
15
+ body: 'OK',
16
+ get(name) {
17
+ return this.headers[name];
18
+ },
19
+ statusCode: 200,
20
+ });
21
+
22
+ const mw = filesMiddleware();
23
+ const layer = mw.stack.find(
24
+ (l) => l.regexp && l.regexp.source.includes('@@download'),
25
+ );
26
+ expect(layer).toBeTruthy();
27
+
28
+ const req = {
29
+ path: '/some/@@download',
30
+ method: 'GET',
31
+ app: { locals: {} },
32
+ };
33
+ const res = {
34
+ headers: {},
35
+ set(name, val) {
36
+ this.headers[name] = val;
37
+ },
38
+ status(code) {
39
+ this.statusCode = code;
40
+ return this;
41
+ },
42
+ send(body) {
43
+ this.body = body;
44
+ },
45
+ };
46
+
47
+ // invoke the middleware handler for that layer
48
+ layer.handle(req, res, (err) => {
49
+ if (err) throw err;
50
+ });
51
+
52
+ await new Promise(process.nextTick);
53
+
54
+ expect(res.body).toBe('OK');
55
+ expect(res.statusCode).toBe(200);
56
+ expect(res.headers['content-type']).toBe('text/plain');
57
+ expect(res.headers['cache-status']).toBe('Souin; hit; ttl=664');
58
+ });
59
+ });
@@ -4,6 +4,7 @@ import { getAPIResourceWithAuth } from '@plone/volto/helpers/Api/APIResourceWith
4
4
  const HEADERS = [
5
5
  'content-type',
6
6
  'content-disposition',
7
+ 'cache-status',
7
8
  'cache-control',
8
9
  'x-sendfile',
9
10
  'x-accel-redirect',
@@ -0,0 +1,50 @@
1
+ import imagesMiddleware from './images';
2
+ import { getAPIResourceWithAuth } from '@plone/volto/helpers/Api/APIResourceWithAuth';
3
+
4
+ vi.mock('@plone/volto/helpers/Api/APIResourceWithAuth', () => ({
5
+ getAPIResourceWithAuth: vi.fn(),
6
+ }));
7
+
8
+ describe('images middleware', () => {
9
+ it('handles requests containing @@images and forwards headers/body', async () => {
10
+ getAPIResourceWithAuth.mockResolvedValue({
11
+ headers: {
12
+ 'content-type': 'image/png',
13
+ 'cache-status': 'Souin; hit; ttl=664',
14
+ },
15
+ body: 'OK',
16
+ });
17
+
18
+ const mw = imagesMiddleware();
19
+ const layer = mw.stack.find(
20
+ (l) => l.regexp && l.regexp.source.includes('@@images'),
21
+ );
22
+ expect(layer).toBeTruthy();
23
+
24
+ const req = {
25
+ path: '/some/@@images/image',
26
+ method: 'GET',
27
+ app: { locals: {} },
28
+ };
29
+ const res = {
30
+ headers: {},
31
+ set(name, val) {
32
+ this.headers[name] = val;
33
+ },
34
+ send(body) {
35
+ this.body = body;
36
+ },
37
+ };
38
+
39
+ // invoke the middleware handler for that layer
40
+ layer.handle(req, res, (err) => {
41
+ if (err) throw err;
42
+ });
43
+
44
+ await new Promise(process.nextTick);
45
+
46
+ expect(res.body).toBe('OK');
47
+ expect(res.headers['content-type']).toBe('image/png');
48
+ expect(res.headers['cache-status']).toBe('Souin; hit; ttl=664');
49
+ });
50
+ });
@@ -852,12 +852,12 @@ export const getBlocksHierarchy = (properties) => {
852
852
  const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
853
853
  return properties[blocksLayoutFieldname]?.items?.map((n) => ({
854
854
  id: n,
855
- title: properties[blocksFieldName][n]?.['@type'],
856
- data: properties[blocksFieldName][n],
857
- children: isBlockContainer(properties[blocksFieldName][n])
858
- ? properties[blocksFieldName][n].data
859
- ? getBlocksHierarchy(properties[blocksFieldName][n].data)
860
- : getBlocksHierarchy(properties[blocksFieldName][n])
855
+ title: properties?.[blocksFieldName]?.[n]?.['@type'],
856
+ data: properties?.[blocksFieldName]?.[n],
857
+ children: isBlockContainer(properties?.[blocksFieldName]?.[n])
858
+ ? properties?.[blocksFieldName]?.[n]?.data
859
+ ? getBlocksHierarchy(properties?.[blocksFieldName]?.[n]?.data)
860
+ : getBlocksHierarchy(properties?.[blocksFieldName]?.[n])
861
861
  : [],
862
862
  }));
863
863
  };
@@ -37,6 +37,23 @@ export const safeWrapper = (func) => (config) => {
37
37
  return res;
38
38
  };
39
39
 
40
+ /**
41
+ * Extract a readable error message from several possible error shapes
42
+ * @param {object} error
43
+ * @returns {string} message
44
+ */
45
+ export const getErrorMessage = (error) => {
46
+ const respBody = error?.response?.body;
47
+ if (respBody?.error?.message) return respBody.error.message;
48
+ if (respBody?.message) return respBody.message;
49
+ if (error?.message) return error.message;
50
+ try {
51
+ return JSON.stringify(error);
52
+ } catch (e) {
53
+ return String(error);
54
+ }
55
+ };
56
+
40
57
  /**
41
58
  * A helper to pipe a configuration object through configuration loaders
42
59
  *