@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.
- package/CHANGELOG.md +43 -0
- package/locales/af/LC_MESSAGES/volto.po +12 -2
- package/locales/af.json +1 -1
- package/locales/ar/LC_MESSAGES/volto.po +12 -2
- package/locales/ar.json +1 -1
- package/locales/bg/LC_MESSAGES/volto.po +12 -2
- package/locales/bg.json +1 -1
- package/locales/bn/LC_MESSAGES/volto.po +12 -2
- package/locales/bn.json +1 -1
- package/locales/ca/LC_MESSAGES/volto.po +11 -1
- package/locales/ca.json +1 -1
- package/locales/cs/LC_MESSAGES/volto.po +12 -2
- package/locales/cs.json +1 -1
- package/locales/cy/LC_MESSAGES/volto.po +12 -2
- package/locales/cy.json +1 -1
- package/locales/da/LC_MESSAGES/volto.po +12 -2
- package/locales/da.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +11 -1
- package/locales/de.json +1 -1
- package/locales/el/LC_MESSAGES/volto.po +12 -2
- package/locales/el.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +12 -2
- package/locales/en.json +1 -1
- package/locales/en_AU/LC_MESSAGES/volto.po +12 -2
- package/locales/en_AU.json +1 -1
- package/locales/en_GB/LC_MESSAGES/volto.po +12 -2
- package/locales/en_GB.json +1 -1
- package/locales/eo/LC_MESSAGES/volto.po +12 -2
- package/locales/eo.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +24 -14
- package/locales/es.json +1 -1
- package/locales/et/LC_MESSAGES/volto.po +12 -2
- package/locales/et.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +23 -13
- package/locales/eu.json +1 -1
- package/locales/fa/LC_MESSAGES/volto.po +12 -2
- package/locales/fa.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +11 -1
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +11 -1
- package/locales/fr.json +1 -1
- package/locales/fu/LC_MESSAGES/volto.po +12 -2
- package/locales/fu.json +1 -1
- package/locales/gl/LC_MESSAGES/volto.po +1014 -1003
- package/locales/gl.json +1 -1
- package/locales/he/LC_MESSAGES/volto.po +12 -2
- package/locales/he.json +1 -1
- package/locales/hi/LC_MESSAGES/volto.po +11 -1
- package/locales/hi.json +1 -1
- package/locales/hr/LC_MESSAGES/volto.po +12 -2
- package/locales/hr.json +1 -1
- package/locales/hu/LC_MESSAGES/volto.po +12 -2
- package/locales/hu.json +1 -1
- package/locales/hy/LC_MESSAGES/volto.po +12 -2
- package/locales/hy.json +1 -1
- package/locales/id/LC_MESSAGES/volto.po +12 -2
- package/locales/id.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +14 -4
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +11 -1
- package/locales/ja.json +1 -1
- package/locales/ka/LC_MESSAGES/volto.po +12 -2
- package/locales/ka.json +1 -1
- package/locales/kn/LC_MESSAGES/volto.po +12 -2
- package/locales/kn.json +1 -1
- package/locales/ko/LC_MESSAGES/volto.po +12 -2
- package/locales/ko.json +1 -1
- package/locales/lt/LC_MESSAGES/volto.po +12 -2
- package/locales/lt.json +1 -1
- package/locales/lv/LC_MESSAGES/volto.po +12 -2
- package/locales/lv.json +1 -1
- package/locales/mi/LC_MESSAGES/volto.po +12 -2
- package/locales/mi.json +1 -1
- package/locales/mk/LC_MESSAGES/volto.po +12 -2
- package/locales/mk.json +1 -1
- package/locales/my/LC_MESSAGES/volto.po +12 -2
- package/locales/my.json +1 -1
- package/locales/nb_NO/LC_MESSAGES/volto.po +12 -2
- package/locales/nb_NO.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +26 -16
- package/locales/nl.json +1 -1
- package/locales/nn/LC_MESSAGES/volto.po +12 -2
- package/locales/nn.json +1 -1
- package/locales/pl/LC_MESSAGES/volto.po +12 -2
- package/locales/pl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +11 -1
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +11 -1
- package/locales/pt_BR.json +1 -1
- package/locales/rm/LC_MESSAGES/volto.po +12 -2
- package/locales/rm.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +11 -1
- package/locales/ro.json +1 -1
- package/locales/ru/LC_MESSAGES/volto.po +11 -1
- package/locales/ru.json +1 -1
- package/locales/sk/LC_MESSAGES/volto.po +12 -2
- package/locales/sk.json +1 -1
- package/locales/sl/LC_MESSAGES/volto.po +12 -2
- package/locales/sl.json +1 -1
- package/locales/sm/LC_MESSAGES/volto.po +12 -2
- package/locales/sm.json +1 -1
- package/locales/sq/LC_MESSAGES/volto.po +12 -2
- package/locales/sq.json +1 -1
- package/locales/sr/LC_MESSAGES/volto.po +12 -2
- package/locales/sr.json +1 -1
- package/locales/sr@cyrl/LC_MESSAGES/volto.po +12 -2
- package/locales/sr@cyrl.json +1 -1
- package/locales/sr@latn/LC_MESSAGES/volto.po +12 -2
- package/locales/sr@latn.json +1 -1
- package/locales/sv/LC_MESSAGES/volto.po +11 -1
- package/locales/sv.json +1 -1
- package/locales/ta/LC_MESSAGES/volto.po +11 -1
- package/locales/ta.json +1 -1
- package/locales/te/LC_MESSAGES/volto.po +12 -2
- package/locales/te.json +1 -1
- package/locales/th/LC_MESSAGES/volto.po +12 -2
- package/locales/th.json +1 -1
- package/locales/to/LC_MESSAGES/volto.po +12 -2
- package/locales/to.json +1 -1
- package/locales/tr/LC_MESSAGES/volto.po +37 -27
- package/locales/tr.json +1 -1
- package/locales/uk/LC_MESSAGES/volto.po +12 -2
- package/locales/uk.json +1 -1
- package/locales/vi/LC_MESSAGES/volto.po +12 -2
- package/locales/vi.json +1 -1
- package/locales/volto.pot +12 -2
- package/locales/zh_CN/LC_MESSAGES/volto.po +11 -1
- package/locales/zh_CN.json +1 -1
- package/locales/zh_Hant/LC_MESSAGES/volto.po +12 -2
- package/locales/zh_Hant.json +1 -1
- package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +12 -2
- package/locales/zh_Hant_HK.json +1 -1
- package/package.json +13 -26
- package/razzle.config.js +20 -5
- package/src/components/manage/Add/Add.jsx +9 -6
- package/src/components/manage/Blocks/Title/Edit.jsx +5 -0
- package/src/components/manage/Controlpanels/Users/UsersControlpanel.jsx +4 -5
- package/src/components/manage/Controlpanels/Users/UsersControlpanel.test.jsx +57 -11
- package/src/components/manage/Multilingual/CompareLanguages.jsx +10 -10
- package/src/components/manage/Multilingual/CreateTranslation.jsx +8 -5
- package/src/components/manage/Multilingual/ManageTranslations.jsx +9 -7
- package/src/components/manage/Multilingual/TranslationObject.jsx +11 -8
- package/src/components/manage/Preferences/PersonalPreferences.jsx +8 -5
- package/src/components/manage/Sharing/Sharing.jsx +11 -3
- package/src/components/manage/Toolbar/Types.crash.test.jsx +46 -0
- package/src/components/manage/Toolbar/Types.jsx +9 -7
- package/src/components/manage/Widgets/FileWidget.jsx +7 -0
- package/src/components/manage/Widgets/RegistryImageWidget.jsx +1 -1
- package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx +1 -0
- package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx +30 -0
- package/src/components/theme/LanguageSelector/LanguageSelector.tsx +7 -5
- package/src/components/theme/MultilingualRedirector/MultilingualRedirector.jsx +12 -7
- package/src/express-middleware/devproxy.js +3 -1
- package/src/express-middleware/files.js +1 -0
- package/src/express-middleware/files.test.js +59 -0
- package/src/express-middleware/images.js +1 -0
- package/src/express-middleware/images.test.js +50 -0
- package/src/helpers/Blocks/Blocks.js +6 -6
- package/src/helpers/Utils/Utils.jsx +17 -0
- package/src/helpers/Utils/Utils.test.jsx +39 -0
- package/src/middleware/api.js +7 -3
- package/src/server.jsx +14 -12
- package/test-setup-globals-vitest.js +25 -0
- package/theme/themes/pastanaga/collections/form.overrides +1 -1
- package/theme/themes/pastanaga/elements/segment.variables +1 -1
- package/theme/themes/pastanaga/extras/widgets.less +17 -0
- package/types/components/manage/Toolbar/Types.crash.test.d.ts +1 -0
- package/types/helpers/Utils/Utils.d.ts +1 -0
- 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].
|
|
60
|
+
langmap[t.language]?.nativeName || t.language
|
|
61
61
|
}`}
|
|
62
62
|
title={`${intl.formatMessage(messages.stop_compare)} ${
|
|
63
|
-
langmap[t.language].
|
|
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].
|
|
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
|
-
|
|
77
|
-
)
|
|
78
|
-
title={`${intl.formatMessage(
|
|
79
|
-
|
|
80
|
-
)
|
|
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].
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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]
|
|
196
|
+
<strong>{langmap?.[lang]?.nativeName || lang}</strong>
|
|
197
197
|
) : (
|
|
198
|
-
langmap[lang]
|
|
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
|
-
)} ${
|
|
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
|
-
|
|
257
|
-
)
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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]
|
|
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]
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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,
|
|
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
|
|
479
|
+
<IconOld
|
|
480
|
+
aria-hidden="true"
|
|
481
|
+
name="check circle outline"
|
|
482
|
+
color="green"
|
|
483
|
+
/>
|
|
480
484
|
),
|
|
481
485
|
global: (
|
|
482
|
-
<IconOld
|
|
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']
|
|
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']
|
|
57
|
+
content?.['@components']?.translations &&
|
|
58
58
|
(() => {
|
|
59
59
|
const translationsLeft = filter(
|
|
60
60
|
settings.supportedLanguages,
|
|
61
61
|
(lang) =>
|
|
62
62
|
!Boolean(
|
|
63
|
-
content['@components']
|
|
64
|
-
find(content['@components']
|
|
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:
|
|
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
|
-
|
|
65
|
-
|
|
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]
|
|
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]
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
}:${
|
|
87
|
+
}:${port}${instancePath}/++api++/VirtualHostRoot`;
|
|
86
88
|
|
|
87
89
|
return `${target}${path.replace('/++api++', '')}`;
|
|
88
90
|
},
|
|
@@ -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
|
+
});
|
|
@@ -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]
|
|
859
|
-
? getBlocksHierarchy(properties[blocksFieldName][n]
|
|
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
|
*
|