@akemona-org/strapi-plugin-i18n 3.7.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 (147) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +19 -0
  3. package/admin/src/assets/images/logo.svg +1 -0
  4. package/admin/src/components/CMEditViewCopyLocale/index.js +183 -0
  5. package/admin/src/components/CMEditViewCopyLocale/utils/cleanData.js +36 -0
  6. package/admin/src/components/CMEditViewCopyLocale/utils/generateOptions.js +22 -0
  7. package/admin/src/components/CMEditViewCopyLocale/utils/index.js +2 -0
  8. package/admin/src/components/CMEditViewCopyLocale/utils/removePasswordAndRelationsFieldFromData.js +54 -0
  9. package/admin/src/components/CMEditViewCopyLocale/utils/tests/cleanData.test.js +83 -0
  10. package/admin/src/components/CMEditViewCopyLocale/utils/tests/data.js +219 -0
  11. package/admin/src/components/CMEditViewCopyLocale/utils/tests/generateOptions.test.js +79 -0
  12. package/admin/src/components/CMEditViewCopyLocale/utils/tests/removePasswordAndRelationsFieldFromData.test.js +40 -0
  13. package/admin/src/components/CMEditViewInjectedComponents/index.js +58 -0
  14. package/admin/src/components/CMEditViewLocalePicker/Option.js +66 -0
  15. package/admin/src/components/CMEditViewLocalePicker/Wrapper.js +8 -0
  16. package/admin/src/components/CMEditViewLocalePicker/index.js +160 -0
  17. package/admin/src/components/CMEditViewLocalePicker/utils/addStatusColorToLocale.js +24 -0
  18. package/admin/src/components/CMEditViewLocalePicker/utils/createLocalesOption.js +20 -0
  19. package/admin/src/components/CMEditViewLocalePicker/utils/index.js +2 -0
  20. package/admin/src/components/CheckboxConfirmation/Wrapper.js +12 -0
  21. package/admin/src/components/CheckboxConfirmation/index.js +70 -0
  22. package/admin/src/components/DeleteModalAdditionalInfos/index.js +25 -0
  23. package/admin/src/components/LocaleList/index.js +101 -0
  24. package/admin/src/components/LocaleListCell/LocaleListCell.js +90 -0
  25. package/admin/src/components/LocaleListCell/tests/LocaleListCell.test.js +128 -0
  26. package/admin/src/components/LocalePicker/index.js +126 -0
  27. package/admin/src/components/LocaleRow/index.js +77 -0
  28. package/admin/src/components/ModalCreate/AdvancedForm.js +45 -0
  29. package/admin/src/components/ModalCreate/BaseForm.js +103 -0
  30. package/admin/src/components/ModalCreate/index.js +136 -0
  31. package/admin/src/components/ModalDelete/index.js +49 -0
  32. package/admin/src/components/ModalEdit/AdvancedForm.js +51 -0
  33. package/admin/src/components/ModalEdit/BaseForm.js +91 -0
  34. package/admin/src/components/ModalEdit/index.js +122 -0
  35. package/admin/src/components/SettingsModal.js +66 -0
  36. package/admin/src/components/index.js +2 -0
  37. package/admin/src/containers/Initializer.js +31 -0
  38. package/admin/src/containers/SettingsPage/LocaleSettingsPage.js +69 -0
  39. package/admin/src/containers/SettingsPage/index.js +33 -0
  40. package/admin/src/containers/SettingsPage/tests/SettingsPage.test.js +744 -0
  41. package/admin/src/containers/SettingsPage/tests/__snapshots__/SettingsPage.test.js.snap +241 -0
  42. package/admin/src/hooks/constants.js +6 -0
  43. package/admin/src/hooks/reducers.js +63 -0
  44. package/admin/src/hooks/tests/reducers.test.js +203 -0
  45. package/admin/src/hooks/useAddLocale/index.js +60 -0
  46. package/admin/src/hooks/useContentTypePermissions/index.js +16 -0
  47. package/admin/src/hooks/useDefaultLocales/index.js +27 -0
  48. package/admin/src/hooks/useDeleteLocale/index.js +45 -0
  49. package/admin/src/hooks/useEditLocale/index.js +46 -0
  50. package/admin/src/hooks/useHasI18n/index.js +13 -0
  51. package/admin/src/hooks/useLocales/index.js +35 -0
  52. package/admin/src/index.js +169 -0
  53. package/admin/src/middlewares/addCommonFieldsToInitialDataMiddleware.js +83 -0
  54. package/admin/src/middlewares/addLocaleColumnToListViewMiddleware.js +32 -0
  55. package/admin/src/middlewares/addLocaleToCollectionTypesMiddleware.js +25 -0
  56. package/admin/src/middlewares/addLocaleToSingleTypesMiddleware.js +25 -0
  57. package/admin/src/middlewares/extendCMEditViewLayoutMiddleware.js +159 -0
  58. package/admin/src/middlewares/extendCTBAttributeInitialDataMiddleware.js +58 -0
  59. package/admin/src/middlewares/extendCTBInitialDataMiddleware.js +33 -0
  60. package/admin/src/middlewares/index.js +21 -0
  61. package/admin/src/middlewares/localePermissionMiddleware.js +39 -0
  62. package/admin/src/middlewares/tests/addCommonFieldsToInitialDataMiddleware.test.js +97 -0
  63. package/admin/src/middlewares/tests/addLocaleColumnToListViewMiddleware.test.js +68 -0
  64. package/admin/src/middlewares/tests/addLocaleToCollectionTypesMiddleware.test.js +200 -0
  65. package/admin/src/middlewares/tests/addLocaleToSingleTypesMiddleware.test.js +193 -0
  66. package/admin/src/middlewares/tests/extendCMEditViewLayoutMiddleware.test.js +556 -0
  67. package/admin/src/middlewares/tests/extendCTBAttrributeInitialDataMiddleware.test.js +124 -0
  68. package/admin/src/middlewares/tests/extendCTBInitialDataMiddleware.test.js +92 -0
  69. package/admin/src/middlewares/tests/localePermissionMiddleware.test.js +150 -0
  70. package/admin/src/middlewares/utils/addLocaleToLinksSearch.js +56 -0
  71. package/admin/src/middlewares/utils/tests/addLocaleToLinksSearch.test.js +137 -0
  72. package/admin/src/permissions.js +9 -0
  73. package/admin/src/pluginId.js +5 -0
  74. package/admin/src/schemas.js +7 -0
  75. package/admin/src/selectors/selectCollectionTypesRelatedPermissions.js +4 -0
  76. package/admin/src/selectors/selectI18nLocales.js +3 -0
  77. package/admin/src/translations/en.json +60 -0
  78. package/admin/src/translations/fr.json +9 -0
  79. package/admin/src/translations/index.js +11 -0
  80. package/admin/src/translations/zh-Hans.json +60 -0
  81. package/admin/src/utils/getDefaultLocale.js +60 -0
  82. package/admin/src/utils/getInitialLocale.js +14 -0
  83. package/admin/src/utils/getLocaleFromQuery.js +7 -0
  84. package/admin/src/utils/getTrad.js +5 -0
  85. package/admin/src/utils/index.js +2 -0
  86. package/admin/src/utils/localizedFields.js +23 -0
  87. package/admin/src/utils/mutateCTBContentTypeSchema.js +66 -0
  88. package/admin/src/utils/tests/getDefaultLocale.test.js +337 -0
  89. package/admin/src/utils/tests/getInitialLocale.test.js +106 -0
  90. package/admin/src/utils/tests/mutateCTBContentTypeSchema.test.js +205 -0
  91. package/config/functions/bootstrap.js +57 -0
  92. package/config/functions/migrations/__tests__/content-type.test.js +255 -0
  93. package/config/functions/migrations/__tests__/field.test.js +150 -0
  94. package/config/functions/migrations/content-type/disable/index.js +34 -0
  95. package/config/functions/migrations/content-type/disable/migrate-for-bookshelf.js +58 -0
  96. package/config/functions/migrations/content-type/disable/migrate-for-mongoose.js +39 -0
  97. package/config/functions/migrations/content-type/enable/index.js +40 -0
  98. package/config/functions/migrations/content-type/utils/index.js +27 -0
  99. package/config/functions/migrations/field/__tests__/utils.test.js +53 -0
  100. package/config/functions/migrations/field/index.js +37 -0
  101. package/config/functions/migrations/field/migrate-for-bookshelf.js +72 -0
  102. package/config/functions/migrations/field/migrate-for-mongoose.js +24 -0
  103. package/config/functions/migrations/field/migrate.js +55 -0
  104. package/config/functions/migrations/field/utils.js +58 -0
  105. package/config/functions/register.js +46 -0
  106. package/config/policies/validateLocaleCreation.js +68 -0
  107. package/config/routes.json +64 -0
  108. package/constants/__tests__/index.test.js +27 -0
  109. package/constants/index.js +36 -0
  110. package/constants/iso-locales.json +2002 -0
  111. package/controllers/__tests__/content-types.test.js +113 -0
  112. package/controllers/__tests__/iso-locales.test.js +26 -0
  113. package/controllers/__tests__/locales.test.js +308 -0
  114. package/controllers/content-types.js +64 -0
  115. package/controllers/iso-locales.js +11 -0
  116. package/controllers/locales.js +104 -0
  117. package/domain/locale.js +10 -0
  118. package/middlewares/i18n/defaults.json +5 -0
  119. package/middlewares/i18n/index.js +28 -0
  120. package/models/Locale.settings.json +31 -0
  121. package/oas.yml +195 -0
  122. package/package.json +31 -0
  123. package/services/__tests__/__snapshots__/iso-locales.test.js.snap +2006 -0
  124. package/services/__tests__/content-types.test.js +545 -0
  125. package/services/__tests__/core-api.test.js +106 -0
  126. package/services/__tests__/entity-service-decorator.test.js +280 -0
  127. package/services/__tests__/iso-locales.test.js +11 -0
  128. package/services/__tests__/locales.test.js +237 -0
  129. package/services/__tests__/localizations.test.js +187 -0
  130. package/services/__tests__/metrics.test.js +90 -0
  131. package/services/content-types.js +200 -0
  132. package/services/core-api.js +296 -0
  133. package/services/entity-service-decorator.js +155 -0
  134. package/services/iso-locales.js +9 -0
  135. package/services/locales.js +97 -0
  136. package/services/localizations.js +65 -0
  137. package/services/metrics.js +24 -0
  138. package/services/permissions/actions.js +124 -0
  139. package/services/permissions/engine.js +63 -0
  140. package/services/permissions/sections-builder.js +48 -0
  141. package/services/permissions.js +11 -0
  142. package/tests/content-manager/list-relation.test.e2e.js +122 -0
  143. package/tests/graphql.test.e2e.js +120 -0
  144. package/tests/locales.test.e2e.js +414 -0
  145. package/utils/index.js +20 -0
  146. package/validation/content-types.js +30 -0
  147. package/validation/locales.js +39 -0
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ const { sendDidUpdateI18nLocalesEvent, sendDidInitializeEvent } = require('../metrics');
4
+ const { isLocalizedContentType } = require('../content-types');
5
+
6
+ describe('Metrics', () => {
7
+ test('sendDidInitializeEvent', async () => {
8
+ global.strapi = {
9
+ contentTypes: {
10
+ withI18n: {
11
+ pluginOptions: {
12
+ i18n: {
13
+ localized: true,
14
+ },
15
+ },
16
+ },
17
+ withoutI18n: {
18
+ pluginOptions: {
19
+ i18n: {
20
+ localized: false,
21
+ },
22
+ },
23
+ },
24
+ withNoOption: {
25
+ pluginOptions: {},
26
+ },
27
+ },
28
+ plugins: {
29
+ i18n: {
30
+ services: {
31
+ ['content-types']: {
32
+ isLocalizedContentType,
33
+ },
34
+ },
35
+ },
36
+ },
37
+ telemetry: {
38
+ send: jest.fn(),
39
+ },
40
+ };
41
+
42
+ await sendDidInitializeEvent();
43
+
44
+ /* expect(strapi.telemetry.send).toHaveBeenCalledWith('didInitializeI18n', {
45
+ numberOfContentTypes: 1,
46
+ }); */
47
+ });
48
+
49
+ test('sendDidUpdateI18nLocalesEvent', async () => {
50
+ global.strapi = {
51
+ contentTypes: {
52
+ withI18n: {
53
+ pluginOptions: {
54
+ i18n: {
55
+ localized: true,
56
+ },
57
+ },
58
+ },
59
+ withoutI18n: {
60
+ pluginOptions: {
61
+ i18n: {
62
+ localized: false,
63
+ },
64
+ },
65
+ },
66
+ withNoOption: {
67
+ pluginOptions: {},
68
+ },
69
+ },
70
+ plugins: {
71
+ i18n: {
72
+ services: {
73
+ locales: {
74
+ count: jest.fn(() => 3),
75
+ },
76
+ },
77
+ },
78
+ },
79
+ telemetry: {
80
+ send: jest.fn(),
81
+ },
82
+ };
83
+
84
+ await sendDidUpdateI18nLocalesEvent();
85
+
86
+ /* expect(strapi.telemetry.send).toHaveBeenCalledWith('didUpdateI18nLocales', {
87
+ numberOfLocales: 3,
88
+ }); */
89
+ });
90
+ });
@@ -0,0 +1,200 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+ const { pick, pipe, has, prop, isNil, cloneDeep, isArray } = require('lodash/fp');
5
+ const { isRelationalAttribute, getVisibleAttributes, isMediaAttribute, isTypedAttribute } =
6
+ require('@akemona-org/strapi-utils').contentTypes;
7
+ const { getService } = require('../utils');
8
+
9
+ const hasLocalizedOption = (modelOrAttribute) => {
10
+ return prop('pluginOptions.i18n.localized', modelOrAttribute) === true;
11
+ };
12
+
13
+ const getValidLocale = async (locale) => {
14
+ const localesService = getService('locales');
15
+
16
+ if (isNil(locale)) {
17
+ return localesService.getDefaultLocale();
18
+ }
19
+
20
+ const foundLocale = await localesService.findByCode(locale);
21
+ if (!foundLocale) {
22
+ throw new Error('Locale not found');
23
+ }
24
+
25
+ return locale;
26
+ };
27
+
28
+ /**
29
+ * Get the related entity used for entity creation
30
+ * @param {Object} relatedEntity related entity
31
+ * @returns {id[]} related entity
32
+ */
33
+ const getNewLocalizationsFrom = async (relatedEntity) => {
34
+ if (relatedEntity) {
35
+ return [relatedEntity.id, ...relatedEntity.localizations.map(prop('id'))];
36
+ }
37
+
38
+ return [];
39
+ };
40
+
41
+ /**
42
+ * Get the related entity used for entity creation
43
+ * @param {id} relatedEntityId related entity id
44
+ * @param {string} model corresponding model
45
+ * @param {string} locale locale of the entity to create
46
+ * @returns {Object} related entity
47
+ */
48
+ const getAndValidateRelatedEntity = async (relatedEntityId, model, locale) => {
49
+ const { kind } = strapi.getModel(model);
50
+ let relatedEntity;
51
+
52
+ if (kind === 'singleType') {
53
+ relatedEntity = await strapi.query(model).findOne({});
54
+ } else if (relatedEntityId) {
55
+ relatedEntity = await strapi.query(model).findOne({ id: relatedEntityId });
56
+ }
57
+
58
+ if (relatedEntityId && !relatedEntity) {
59
+ throw new Error("The related entity doesn't exist");
60
+ }
61
+
62
+ if (
63
+ relatedEntity &&
64
+ (relatedEntity.locale === locale ||
65
+ relatedEntity.localizations.map(prop('locale')).includes(locale))
66
+ ) {
67
+ throw new Error('The entity already exists in this locale');
68
+ }
69
+
70
+ return relatedEntity;
71
+ };
72
+
73
+ /**
74
+ * Returns whether an attribute is localized or not
75
+ * @param {*} attribute
76
+ * @returns
77
+ */
78
+ const isLocalizedAttribute = (model, attributeName) => {
79
+ const attribute = model.attributes[attributeName];
80
+
81
+ return (
82
+ hasLocalizedOption(attribute) ||
83
+ (isRelationalAttribute(attribute) && !isMediaAttribute(attribute)) ||
84
+ isTypedAttribute(attribute, 'uid')
85
+ );
86
+ };
87
+
88
+ /**
89
+ * Returns whether a model is localized or not
90
+ * @param {*} model
91
+ * @returns
92
+ */
93
+ const isLocalizedContentType = (model) => {
94
+ return hasLocalizedOption(model);
95
+ };
96
+
97
+ /**
98
+ * Returns the list of attribute names that are not localized
99
+ * @param {object} model
100
+ * @returns {string[]}
101
+ */
102
+ const getNonLocalizedAttributes = (model) => {
103
+ return getVisibleAttributes(model).filter(
104
+ (attributeName) => !isLocalizedAttribute(model, attributeName)
105
+ );
106
+ };
107
+
108
+ const removeId = (value) => {
109
+ if (typeof value === 'object' && (has('id', value) || has('_id', value))) {
110
+ delete value.id;
111
+ delete value._id;
112
+ }
113
+ };
114
+
115
+ const removeIds = (model) => (entry) => removeIdsMut(model, cloneDeep(entry));
116
+
117
+ const removeIdsMut = (model, entry) => {
118
+ if (isNil(entry)) {
119
+ return entry;
120
+ }
121
+
122
+ removeId(entry);
123
+
124
+ _.forEach(model.attributes, (attr, attrName) => {
125
+ const value = entry[attrName];
126
+ if (attr.type === 'dynamiczone' && isArray(value)) {
127
+ value.forEach((compo) => {
128
+ if (has('__component', compo)) {
129
+ const model = strapi.components[compo.__component];
130
+ removeIdsMut(model, compo);
131
+ }
132
+ });
133
+ } else if (attr.type === 'component') {
134
+ const [model] = strapi.db.getModelsByAttribute(attr);
135
+ if (isArray(value)) {
136
+ value.forEach((compo) => removeIdsMut(model, compo));
137
+ } else {
138
+ removeIdsMut(model, value);
139
+ }
140
+ }
141
+ });
142
+
143
+ return entry;
144
+ };
145
+
146
+ /**
147
+ * Returns a copy of an entry picking only its non localized attributes
148
+ * @param {object} model
149
+ * @param {object} entry
150
+ * @returns {object}
151
+ */
152
+ const copyNonLocalizedAttributes = (model, entry) => {
153
+ const nonLocalizedAttributes = getNonLocalizedAttributes(model);
154
+
155
+ return pipe(pick(nonLocalizedAttributes), removeIds(model))(entry);
156
+ };
157
+
158
+ /**
159
+ * Returns the list of attribute names that are localized
160
+ * @param {object} model
161
+ * @returns {string[]}
162
+ */
163
+ const getLocalizedAttributes = (model) => {
164
+ return getVisibleAttributes(model).filter((attributeName) =>
165
+ isLocalizedAttribute(model, attributeName)
166
+ );
167
+ };
168
+
169
+ /**
170
+ * Fill non localized fields of an entry if there are nil
171
+ * @param {Object} entry entry to fill
172
+ * @param {Object} relatedEntry values used to fill
173
+ * @param {Object} options
174
+ * @param {Object} options.model corresponding model
175
+ */
176
+ const fillNonLocalizedAttributes = (entry, relatedEntry, { model }) => {
177
+ if (isNil(relatedEntry)) {
178
+ return;
179
+ }
180
+
181
+ const modelDef = strapi.getModel(model);
182
+ const relatedEntryCopy = copyNonLocalizedAttributes(modelDef, relatedEntry);
183
+
184
+ _.forEach(relatedEntryCopy, (value, field) => {
185
+ if (isNil(entry[field])) {
186
+ entry[field] = value;
187
+ }
188
+ });
189
+ };
190
+
191
+ module.exports = {
192
+ isLocalizedContentType,
193
+ getValidLocale,
194
+ getNewLocalizationsFrom,
195
+ getLocalizedAttributes,
196
+ getNonLocalizedAttributes,
197
+ copyNonLocalizedAttributes,
198
+ getAndValidateRelatedEntity,
199
+ fillNonLocalizedAttributes,
200
+ };
@@ -0,0 +1,296 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+ const { has, prop, pick, reduce, map, keys, toPath } = require('lodash/fp');
5
+ const { contentTypes, parseMultipartData, sanitizeEntity } = require('@akemona-org/strapi-utils');
6
+
7
+ const { getService } = require('../utils');
8
+
9
+ const { getContentTypeRoutePrefix, isSingleType, getWritableAttributes } = contentTypes;
10
+
11
+ /**
12
+ * Returns a parsed request body. It handles whether the body is multipart or not
13
+ * @param {object} ctx - Koa request context
14
+ * @returns {{ data: { [key: string]: any }, files: { [key: string]: any } }}
15
+ */
16
+ const parseRequest = (ctx) => {
17
+ if (ctx.is('multipart')) {
18
+ return parseMultipartData(ctx);
19
+ } else {
20
+ return { data: ctx.request.body, files: {} };
21
+ }
22
+ };
23
+
24
+ /**
25
+ * Returns all locales for an entry
26
+ * @param {object} entry
27
+ * @returns {string[]}
28
+ */
29
+ const getAllLocales = (entry) => {
30
+ return [entry.locale, ...map(prop('locale'), entry.localizations)];
31
+ };
32
+
33
+ /**
34
+ * Returns all localizations ids for an entry
35
+ * @param {object} entry
36
+ * @returns {any[]}
37
+ */
38
+ const getAllLocalizationsIds = (entry) => {
39
+ return [entry.id, ...map(prop('id'), entry.localizations)];
40
+ };
41
+
42
+ /**
43
+ * Returns a sanitizer object with a data & a file sanitizer for a content type
44
+ * @param {object} contentType
45
+ * @returns {{
46
+ * sanitizeInput(data: object): object,
47
+ * sanitizeInputFiles(files: object): object
48
+ * }}
49
+ */
50
+ const createSanitizer = (contentType) => {
51
+ /**
52
+ * Returns the writable attributes of a content type in the localization routes
53
+ * @returns {string[]}
54
+ */
55
+ const getAllowedAttributes = () => {
56
+ return getWritableAttributes(contentType).filter(
57
+ (attributeName) => !['locale', 'localizations'].includes(attributeName)
58
+ );
59
+ };
60
+
61
+ /**
62
+ * Sanitizes uploaded files to keep only writable ones
63
+ * @param {object} files - input files to sanitize
64
+ * @returns {object}
65
+ */
66
+ const sanitizeInputFiles = (files) => {
67
+ const allowedFields = getAllowedAttributes();
68
+ return reduce(
69
+ (acc, keyPath) => {
70
+ const [rootKey] = toPath(keyPath);
71
+ if (allowedFields.includes(rootKey)) {
72
+ acc[keyPath] = files[keyPath];
73
+ }
74
+
75
+ return acc;
76
+ },
77
+ {},
78
+ keys(files)
79
+ );
80
+ };
81
+
82
+ /**
83
+ * Sanitizes input data to keep only writable attributes
84
+ * @param {object} data - input data to sanitize
85
+ * @returns {object}
86
+ */
87
+ const sanitizeInput = (data) => {
88
+ return pick(getAllowedAttributes(), data);
89
+ };
90
+
91
+ return { sanitizeInput, sanitizeInputFiles };
92
+ };
93
+
94
+ /**
95
+ * Returns a handler to handle localizations creation in the core api
96
+ * @param {object} contentType
97
+ * @returns {(object) => void}
98
+ */
99
+ const createLocalizationHandler = (contentType) => {
100
+ const { copyNonLocalizedAttributes } = getService('content-types');
101
+
102
+ const { sanitizeInput, sanitizeInputFiles } = createSanitizer(contentType);
103
+
104
+ /**
105
+ * Create localized entry from another one
106
+ */
107
+ const createFromBaseEntry = async (ctx, entry) => {
108
+ const { data, files } = parseRequest(ctx);
109
+
110
+ const { findByCode } = getService('locales');
111
+
112
+ if (!has('locale', data)) {
113
+ throw strapi.errors.badRequest('locale.missing');
114
+ }
115
+
116
+ const matchingLocale = await findByCode(data.locale);
117
+ if (!matchingLocale) {
118
+ throw strapi.errors.badRequest('locale.invalid');
119
+ }
120
+
121
+ const usedLocales = getAllLocales(entry);
122
+ if (usedLocales.includes(data.locale)) {
123
+ throw strapi.errors.badRequest('locale.already.used');
124
+ }
125
+
126
+ const sanitizedData = {
127
+ ...copyNonLocalizedAttributes(contentType, entry),
128
+ ...sanitizeInput(data),
129
+ locale: data.locale,
130
+ localizations: getAllLocalizationsIds(entry),
131
+ };
132
+
133
+ const sanitizedFiles = sanitizeInputFiles(files);
134
+
135
+ const newEntry = await strapi.entityService.create(
136
+ { data: sanitizedData, files: sanitizedFiles },
137
+ { model: contentType.uid }
138
+ );
139
+
140
+ ctx.body = sanitizeEntity(newEntry, { model: strapi.getModel(contentType.uid) });
141
+ };
142
+
143
+ if (isSingleType(contentType)) {
144
+ return async function (ctx) {
145
+ const entry = await strapi.query(contentType.uid).findOne();
146
+
147
+ if (!entry) {
148
+ throw strapi.errors.notFound('baseEntryId.invalid');
149
+ }
150
+
151
+ await createFromBaseEntry(ctx, entry);
152
+ };
153
+ }
154
+
155
+ return async function (ctx) {
156
+ const { id: baseEntryId } = ctx.params;
157
+
158
+ const entry = await strapi.query(contentType.uid).findOne({ id: baseEntryId });
159
+
160
+ if (!entry) {
161
+ throw strapi.errors.notFound('baseEntryId.invalid');
162
+ }
163
+
164
+ await createFromBaseEntry(ctx, entry);
165
+ };
166
+ };
167
+
168
+ /**
169
+ * Returns a route config to handle localizations creation in the core api
170
+ * @param {object} contentType
171
+ * @returns {{ method: string, path: string, handler: string, config: { policies: string[] }}}
172
+ */
173
+ const createLocalizationRoute = (contentType) => {
174
+ const { modelName } = contentType;
175
+
176
+ const routePrefix = getContentTypeRoutePrefix(contentType);
177
+ const routePath = isSingleType(contentType)
178
+ ? `/${routePrefix}/localizations`
179
+ : `/${routePrefix}/:id/localizations`;
180
+
181
+ return {
182
+ method: 'POST',
183
+ path: routePath,
184
+ handler: `${modelName}.createLocalization`,
185
+ config: {
186
+ policies: [],
187
+ },
188
+ };
189
+ };
190
+
191
+ /**
192
+ * Adds a route & an action to the core api controller of a content type to allow creating new localizations
193
+ * @param {object} contentType
194
+ */
195
+ const addCreateLocalizationAction = (contentType) => {
196
+ const { modelName, apiName } = contentType;
197
+
198
+ const localizationRoute = createLocalizationRoute(contentType);
199
+
200
+ const coreApiControllerPath = `api.${apiName}.controllers.${modelName}.createLocalization`;
201
+ const handler = createLocalizationHandler(contentType);
202
+
203
+ strapi.config.routes.push(localizationRoute);
204
+
205
+ _.set(strapi, coreApiControllerPath, handler);
206
+ };
207
+
208
+ const mergeCustomizer = (dest, src) => {
209
+ if (typeof dest === 'string') {
210
+ return `${dest}\n${src}`;
211
+ }
212
+ };
213
+
214
+ /**
215
+ * Add a graphql schema to the plugin's global graphl schema to be processed
216
+ * @param {object} schema
217
+ */
218
+ const addGraphqlSchema = (schema) => {
219
+ _.mergeWith(strapi.plugins.i18n.config.schema.graphql, schema, mergeCustomizer);
220
+ };
221
+
222
+ /**
223
+ * Add localization mutation & filters to use with the graphql plugin
224
+ * @param {object} contentType
225
+ */
226
+ const addGraphqlLocalizationAction = (contentType) => {
227
+ const { globalId, modelName } = contentType;
228
+
229
+ if (!strapi.plugins.graphql) {
230
+ return;
231
+ }
232
+
233
+ const { toSingular, toPlural } = strapi.plugins.graphql.services.naming;
234
+
235
+ // We use a string instead of an enum as the locales can be changed in the admin
236
+ // NOTE: We could use a custom scalar so the validation becomes dynamic
237
+ const localeArgs = {
238
+ args: {
239
+ locale: 'String',
240
+ },
241
+ };
242
+
243
+ // add locale arguments in the existing queries
244
+ if (isSingleType(contentType)) {
245
+ const queryName = toSingular(modelName);
246
+ const mutationSuffix = _.upperFirst(queryName);
247
+
248
+ addGraphqlSchema({
249
+ resolver: {
250
+ Query: {
251
+ [queryName]: localeArgs,
252
+ },
253
+ Mutation: {
254
+ [`update${mutationSuffix}`]: localeArgs,
255
+ [`delete${mutationSuffix}`]: localeArgs,
256
+ },
257
+ },
258
+ });
259
+ } else {
260
+ const queryName = toPlural(modelName);
261
+
262
+ addGraphqlSchema({
263
+ resolver: {
264
+ Query: {
265
+ [queryName]: localeArgs,
266
+ [`${queryName}Connection`]: localeArgs,
267
+ },
268
+ },
269
+ });
270
+ }
271
+
272
+ // add new mutation to create a localization
273
+ const typeName = globalId;
274
+
275
+ const capitalizedName = _.upperFirst(toSingular(modelName));
276
+ const mutationName = `create${capitalizedName}Localization`;
277
+ const mutationDef = `${mutationName}(input: update${capitalizedName}Input!): ${typeName}!`;
278
+ const actionName = `${contentType.uid}.createLocalization`;
279
+
280
+ addGraphqlSchema({
281
+ mutation: mutationDef,
282
+ resolver: {
283
+ Mutation: {
284
+ [mutationName]: {
285
+ resolver: actionName,
286
+ },
287
+ },
288
+ },
289
+ });
290
+ };
291
+
292
+ module.exports = {
293
+ addCreateLocalizationAction,
294
+ addGraphqlLocalizationAction,
295
+ createSanitizer,
296
+ };