@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,58 @@
1
+ 'use strict';
2
+
3
+ const pmap = require('p-map');
4
+
5
+ const BATCH_SIZE = 1000;
6
+
7
+ const migrateForBookshelf = async (
8
+ { ORM, defaultLocale, definition, previousDefinition, model },
9
+ context
10
+ ) => {
11
+ const localizationsTable = `${previousDefinition.collectionName}__localizations`;
12
+ const trx = await ORM.knex.transaction();
13
+ try {
14
+ let offset = 0;
15
+ // eslint-disable-next-line no-constant-condition
16
+ while (true) {
17
+ let batch = await trx
18
+ .select(['id'])
19
+ .from(model.collectionName)
20
+ .whereNot('locale', defaultLocale)
21
+ .orderBy('id')
22
+ .offset(offset)
23
+ .limit(BATCH_SIZE);
24
+ offset += BATCH_SIZE;
25
+
26
+ await pmap(batch, entry => model.deleteRelations(entry.id, { transacting: trx }), {
27
+ concurrency: 100,
28
+ stopOnError: true,
29
+ });
30
+
31
+ if (batch.length < BATCH_SIZE) {
32
+ break;
33
+ }
34
+ }
35
+ await trx
36
+ .from(model.collectionName)
37
+ .del()
38
+ .whereNot('locale', defaultLocale);
39
+ await trx.commit();
40
+ } catch (e) {
41
+ await trx.rollback();
42
+ throw e;
43
+ }
44
+
45
+ if (definition.client === 'sqlite3') {
46
+ // Bug when dropping column with sqlite3 https://github.com/knex/knex/issues/631
47
+ // Need to recreate the table
48
+ context.recreateSqliteTable = true;
49
+ } else {
50
+ await ORM.knex.schema.table(definition.collectionName, t => {
51
+ t.dropColumn('locale');
52
+ });
53
+ }
54
+
55
+ await ORM.knex.schema.dropTableIfExists(localizationsTable);
56
+ };
57
+
58
+ module.exports = migrateForBookshelf;
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const pmap = require('p-map');
4
+
5
+ const BATCH_SIZE = 1000;
6
+
7
+ const migrateForMongoose = async ({ model, defaultLocale }) => {
8
+ let lastId;
9
+ const findParams = { locale: { $ne: defaultLocale } };
10
+
11
+ // eslint-disable-next-line no-constant-condition
12
+ while (true) {
13
+ if (lastId) {
14
+ findParams._id = { $gt: lastId };
15
+ }
16
+
17
+ const batch = await model
18
+ .find(findParams, ['id'])
19
+ .sort({ _id: 1 })
20
+ .limit(BATCH_SIZE);
21
+
22
+ if (batch.length > 0) {
23
+ lastId = batch[batch.length - 1]._id;
24
+ }
25
+
26
+ await pmap(batch, entry => model.deleteRelations(entry), {
27
+ concurrency: 100,
28
+ stopOnError: true,
29
+ });
30
+
31
+ if (batch.length < BATCH_SIZE) {
32
+ break;
33
+ }
34
+ }
35
+ await model.deleteMany({ locale: { $ne: defaultLocale } });
36
+ await model.updateMany({}, { $unset: { locale: '' } }, { strict: false });
37
+ };
38
+
39
+ module.exports = migrateForMongoose;
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const { getService } = require('../../../../../utils');
4
+ const { getDefaultLocale } = require('../utils');
5
+
6
+ const updateLocale = (model, ORM, locale) => {
7
+ if (model.orm === 'bookshelf') {
8
+ return ORM.knex
9
+ .update({ locale })
10
+ .from(model.collectionName)
11
+ .where({ locale: null });
12
+ }
13
+
14
+ if (model.orm === 'mongoose') {
15
+ return model.updateMany(
16
+ { $or: [{ locale: { $exists: false } }, { locale: null }] },
17
+ { locale }
18
+ );
19
+ }
20
+ };
21
+
22
+ // Enable i18n on CT -> Add default locale to all existing entities
23
+ const after = async ({ model, definition, previousDefinition, ORM }) => {
24
+ const { isLocalizedContentType } = getService('content-types');
25
+
26
+ if (!isLocalizedContentType(definition) || isLocalizedContentType(previousDefinition)) {
27
+ return;
28
+ }
29
+
30
+ const defaultLocale = await getDefaultLocale(model, ORM);
31
+
32
+ await updateLocale(model, ORM, defaultLocale);
33
+ };
34
+
35
+ const before = () => {};
36
+
37
+ module.exports = {
38
+ before,
39
+ after,
40
+ };
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const { DEFAULT_LOCALE } = require('../../../../../constants');
4
+
5
+ const getDefaultLocale = async (model, ORM) => {
6
+ let defaultLocaleRows;
7
+ if (model.orm === 'bookshelf') {
8
+ defaultLocaleRows = await ORM.knex
9
+ .select('value')
10
+ .from('core_store')
11
+ .where({ key: 'plugin_i18n_default_locale' });
12
+ } else if (model.orm === 'mongoose') {
13
+ defaultLocaleRows = await strapi.models['core_store'].find({
14
+ key: 'plugin_i18n_default_locale',
15
+ });
16
+ }
17
+
18
+ if (defaultLocaleRows.length > 0) {
19
+ return JSON.parse(defaultLocaleRows[0].value);
20
+ }
21
+
22
+ return DEFAULT_LOCALE.code;
23
+ };
24
+
25
+ module.exports = {
26
+ getDefaultLocale,
27
+ };
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ const { shouldBeProcessed, getUpdatesInfo } = require('../utils');
4
+
5
+ describe('i18n - migration utils', () => {
6
+ describe('shouldBeProcessed', () => {
7
+ const testData = [
8
+ [[], [], false],
9
+ [['en'], [], false],
10
+ [['en', 'fr'], [], false],
11
+ [['en', 'fr'], [{ locale: 'en' }], false],
12
+ [['en', 'fr'], [{ locale: 'fr' }], false],
13
+ [['en'], [{ locale: 'fr' }, { locale: 'en' }], false],
14
+ [['en', 'fr'], [{ locale: 'fr' }, { locale: 'en' }], false],
15
+ [[], [{ locale: 'en' }], true],
16
+ [['en'], [{ locale: 'fr' }], true],
17
+ [['en', 'fr'], [{ locale: 'it' }], true],
18
+ ];
19
+
20
+ test.each(testData)('%p %j : %p', (processedLocaleCodes, localizations, expectedResult) => {
21
+ const result = shouldBeProcessed(processedLocaleCodes)({ localizations });
22
+
23
+ expect(result).toBe(expectedResult);
24
+ });
25
+ });
26
+
27
+ describe('getUpdatesInfo', () => {
28
+ const testData = [
29
+ [
30
+ [{ name: 'Name', nickname: 'Nickname', localizations: [{ id: 1 }, { id: 2 }] }],
31
+ ['name'],
32
+ [{ entriesIdsToUpdate: [1, 2], attributesValues: { name: 'Name' } }],
33
+ ],
34
+ [
35
+ [
36
+ { name: 'Name 1', nickname: 'Nickname 1', localizations: [{ id: 1 }, { id: 2 }] },
37
+ { name: 'Name 2', nickname: 'Nickname 2', localizations: [{ id: 3 }, { id: 4 }] },
38
+ ],
39
+ ['name'],
40
+ [
41
+ { entriesIdsToUpdate: [1, 2], attributesValues: { name: 'Name 1' } },
42
+ { entriesIdsToUpdate: [3, 4], attributesValues: { name: 'Name 2' } },
43
+ ],
44
+ ],
45
+ ];
46
+
47
+ test.each(testData)('%j', (entriesToProcess, attributesToMigrate, expectedResult) => {
48
+ const result = getUpdatesInfo({ entriesToProcess, attributesToMigrate });
49
+
50
+ expect(result).toEqual(expectedResult);
51
+ });
52
+ });
53
+ });
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const { difference, keys, intersection, isEmpty } = require('lodash/fp');
4
+ const { getService } = require('../../../../utils');
5
+ const migrateForMongoose = require('./migrate-for-mongoose');
6
+ const migrateForBookshelf = require('./migrate-for-bookshelf');
7
+
8
+ // Migration when i18n is disabled on a field of a content-type that have i18n enabled
9
+ const after = async ({ model, definition, previousDefinition, ORM }) => {
10
+ const { isLocalizedContentType, getLocalizedAttributes } = getService('content-types');
11
+
12
+ if (!isLocalizedContentType(model) || !isLocalizedContentType(previousDefinition)) {
13
+ return;
14
+ }
15
+
16
+ const localizedAttributes = getLocalizedAttributes(definition);
17
+ const prevLocalizedAttributes = getLocalizedAttributes(previousDefinition);
18
+ const attributesDisabled = difference(prevLocalizedAttributes, localizedAttributes);
19
+ const attributesToMigrate = intersection(keys(definition.attributes), attributesDisabled);
20
+
21
+ if (isEmpty(attributesToMigrate)) {
22
+ return;
23
+ }
24
+
25
+ if (model.orm === 'bookshelf') {
26
+ await migrateForBookshelf({ ORM, model, attributesToMigrate });
27
+ } else if (model.orm === 'mongoose') {
28
+ await migrateForMongoose({ model, attributesToMigrate });
29
+ }
30
+ };
31
+
32
+ const before = () => {};
33
+
34
+ module.exports = {
35
+ before,
36
+ after,
37
+ };
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ const { migrate } = require('./migrate');
4
+ const { areScalarAttributesOnly } = require('./utils');
5
+
6
+ const TMP_TABLE_NAME = '__tmp__i18n_field_migration';
7
+
8
+ const batchInsertInTmpTable = async ({ updatesInfo }, { transacting: trx }) => {
9
+ const tmpEntries = [];
10
+ updatesInfo.forEach(({ entriesIdsToUpdate, attributesValues }) => {
11
+ entriesIdsToUpdate.forEach(id => {
12
+ tmpEntries.push({ id, ...attributesValues });
13
+ });
14
+ });
15
+ await trx.batchInsert(TMP_TABLE_NAME, tmpEntries, 100);
16
+ };
17
+
18
+ const updateFromTmpTable = async ({ model, attributesToMigrate }, { transacting: trx }) => {
19
+ const collectionName = model.collectionName;
20
+ if (model.client === 'pg') {
21
+ const substitutes = attributesToMigrate.map(() => '?? = ??.??').join(',');
22
+ const bindings = [collectionName];
23
+ attributesToMigrate.forEach(attr => bindings.push(attr, TMP_TABLE_NAME, attr));
24
+ bindings.push(TMP_TABLE_NAME, collectionName, TMP_TABLE_NAME);
25
+
26
+ await trx.raw(`UPDATE ?? SET ${substitutes} FROM ?? WHERE ??.id = ??.id;`, bindings);
27
+ } else if (model.client === 'mysql') {
28
+ const substitutes = attributesToMigrate.map(() => '??.?? = ??.??').join(',');
29
+ const bindings = [collectionName, TMP_TABLE_NAME, collectionName, TMP_TABLE_NAME];
30
+ attributesToMigrate.forEach(attr => bindings.push(collectionName, attr, TMP_TABLE_NAME, attr));
31
+
32
+ await trx.raw(`UPDATE ?? JOIN ?? ON ??.id = ??.id SET ${substitutes};`, bindings);
33
+ }
34
+ };
35
+
36
+ const createTmpTable = async ({ ORM, attributesToMigrate, model }) => {
37
+ const columnsToCopy = ['id', ...attributesToMigrate];
38
+ await deleteTmpTable({ ORM });
39
+ await ORM.knex.raw(`CREATE TABLE ?? AS ??`, [
40
+ TMP_TABLE_NAME,
41
+ ORM.knex
42
+ .select(columnsToCopy)
43
+ .from(model.collectionName)
44
+ .whereRaw('?', 0),
45
+ ]);
46
+ };
47
+
48
+ const deleteTmpTable = ({ ORM }) => ORM.knex.schema.dropTableIfExists(TMP_TABLE_NAME);
49
+
50
+ const migrateForBookshelf = async ({ ORM, model, attributesToMigrate }) => {
51
+ const onlyScalarAttrs = areScalarAttributesOnly({ model, attributes: attributesToMigrate });
52
+
53
+ // optimize migration for pg and mysql when there are only scalar attributes to migrate
54
+ if (onlyScalarAttrs && ['pg', 'mysql'].includes(model.client)) {
55
+ // create table outside of the transaction because mysql doesn't accept the creation inside
56
+ await createTmpTable({ ORM, attributesToMigrate, model });
57
+ await ORM.knex.transaction(async transacting => {
58
+ await migrate(
59
+ { model, attributesToMigrate },
60
+ { migrateFn: batchInsertInTmpTable, transacting }
61
+ );
62
+ await updateFromTmpTable({ model, attributesToMigrate }, { transacting });
63
+ });
64
+ await deleteTmpTable({ ORM });
65
+ } else {
66
+ await ORM.knex.transaction(async transacting => {
67
+ await migrate({ model, attributesToMigrate }, { transacting });
68
+ });
69
+ }
70
+ };
71
+
72
+ module.exports = migrateForBookshelf;
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const { migrate } = require('./migrate');
4
+ const { areScalarAttributesOnly } = require('./utils');
5
+
6
+ const batchUpdate = async ({ updatesInfo, model }) => {
7
+ const updates = updatesInfo.map(({ entriesIdsToUpdate, attributesValues }) => ({
8
+ updateMany: { filter: { _id: { $in: entriesIdsToUpdate } }, update: attributesValues },
9
+ }));
10
+
11
+ await model.bulkWrite(updates);
12
+ };
13
+
14
+ const migrateForMongoose = async ({ model, attributesToMigrate }) => {
15
+ const onlyScalarAttrs = areScalarAttributesOnly({ model, attributes: attributesToMigrate });
16
+
17
+ if (onlyScalarAttrs) {
18
+ await migrate({ model, attributesToMigrate }, { migrateFn: batchUpdate });
19
+ } else {
20
+ await migrate({ model, attributesToMigrate });
21
+ }
22
+ };
23
+
24
+ module.exports = migrateForMongoose;
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ const { pick, prop } = require('lodash/fp');
4
+ const { getService } = require('../../../../utils');
5
+ const { shouldBeProcessed, getUpdatesInfo, getSortedLocales } = require('./utils');
6
+
7
+ const BATCH_SIZE = 1000;
8
+
9
+ const migrateBatch = async (entries, { model, attributesToMigrate }, { transacting }) => {
10
+ const { copyNonLocalizedAttributes } = getService('content-types');
11
+
12
+ const updatePromises = entries.map(entity => {
13
+ const updateValues = pick(attributesToMigrate, copyNonLocalizedAttributes(model, entity));
14
+ const entriesIdsToUpdate = entity.localizations.map(prop('id'));
15
+ return Promise.all(
16
+ entriesIdsToUpdate.map(id =>
17
+ strapi.query(model.uid).update({ id }, updateValues, { transacting })
18
+ )
19
+ );
20
+ });
21
+
22
+ await Promise.all(updatePromises);
23
+ };
24
+
25
+ const migrate = async ({ model, attributesToMigrate }, { migrateFn, transacting } = {}) => {
26
+ const locales = await getSortedLocales({ transacting });
27
+ const processedLocaleCodes = [];
28
+ for (const locale of locales) {
29
+ let offset = 0;
30
+ // eslint-disable-next-line no-constant-condition
31
+ while (true) {
32
+ const entries = await strapi
33
+ .query(model.uid)
34
+ .find({ locale, _start: offset, _limit: BATCH_SIZE }, null, { transacting });
35
+ const entriesToProcess = entries.filter(shouldBeProcessed(processedLocaleCodes));
36
+
37
+ if (migrateFn) {
38
+ const updatesInfo = getUpdatesInfo({ entriesToProcess, attributesToMigrate });
39
+ await migrateFn({ updatesInfo, model }, { transacting });
40
+ } else {
41
+ await migrateBatch(entriesToProcess, { model, attributesToMigrate }, { transacting });
42
+ }
43
+
44
+ if (entries.length < BATCH_SIZE) {
45
+ break;
46
+ }
47
+ offset += BATCH_SIZE;
48
+ }
49
+ processedLocaleCodes.push(locale);
50
+ }
51
+ };
52
+
53
+ module.exports = {
54
+ migrate,
55
+ };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const { isScalarAttribute } = require('@akemona-org/strapi-utils').contentTypes;
4
+ const { pick, prop, map, intersection, isEmpty, orderBy, pipe, every } = require('lodash/fp');
5
+ const { getService } = require('../../../../utils');
6
+
7
+ const shouldBeProcessed = (processedLocaleCodes) => (entry) => {
8
+ return (
9
+ entry.localizations.length > 0 &&
10
+ intersection(entry.localizations.map(prop('locale')), processedLocaleCodes).length === 0
11
+ );
12
+ };
13
+
14
+ const getUpdatesInfo = ({ entriesToProcess, attributesToMigrate }) => {
15
+ const updates = [];
16
+ for (const entry of entriesToProcess) {
17
+ const attributesValues = pick(attributesToMigrate, entry);
18
+ const entriesIdsToUpdate = entry.localizations.map(prop('id'));
19
+ updates.push({ entriesIdsToUpdate, attributesValues });
20
+ }
21
+ return updates;
22
+ };
23
+
24
+ const getSortedLocales = async ({ transacting } = {}) => {
25
+ const localeService = getService('locales');
26
+
27
+ let defaultLocale;
28
+ try {
29
+ const storeRes = await strapi
30
+ .query('core_store')
31
+ .findOne({ key: 'plugin_i18n_default_locale' }, null, { transacting });
32
+ defaultLocale = JSON.parse(storeRes.value);
33
+ } catch (e) {
34
+ throw new Error("Could not migrate because the default locale doesn't exist");
35
+ }
36
+
37
+ const locales = await localeService.find({}, null, { transacting });
38
+ if (isEmpty(locales)) {
39
+ throw new Error('Could not migrate because no locale exist');
40
+ }
41
+
42
+ // Put default locale first
43
+ return pipe(
44
+ map((locale) => ({ code: locale.code, isDefault: locale.code === defaultLocale })),
45
+ orderBy(['isDefault', 'code'], ['desc', 'asc']),
46
+ map(prop('code'))
47
+ )(locales);
48
+ };
49
+
50
+ const areScalarAttributesOnly = ({ model, attributes }) =>
51
+ pipe(pick(attributes), every(isScalarAttribute))(model.attributes);
52
+
53
+ module.exports = {
54
+ shouldBeProcessed,
55
+ getUpdatesInfo,
56
+ getSortedLocales,
57
+ areScalarAttributesOnly,
58
+ };
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+ const { PUBLISHED_AT_ATTRIBUTE } = require('@akemona-org/strapi-utils').contentTypes.constants;
5
+
6
+ const { getService } = require('../../utils');
7
+ const fieldMigration = require('./migrations/field');
8
+ const enableContentTypeMigration = require('./migrations/content-type/enable');
9
+ const disableContentTypeMigration = require('./migrations/content-type/disable');
10
+
11
+ module.exports = () => {
12
+ const contentTypeService = getService('content-types');
13
+ const coreApiService = getService('core-api');
14
+
15
+ _.set(strapi.plugins.i18n.config, 'schema.graphql', {});
16
+
17
+ Object.values(strapi.contentTypes).forEach((contentType) => {
18
+ if (contentTypeService.isLocalizedContentType(contentType)) {
19
+ const { attributes, modelName } = contentType;
20
+
21
+ _.set(attributes, 'localizations', {
22
+ writable: true,
23
+ private: false,
24
+ configurable: false,
25
+ visible: false,
26
+ collection: modelName,
27
+ populate: ['_id', 'id', 'locale', PUBLISHED_AT_ATTRIBUTE],
28
+ });
29
+
30
+ _.set(attributes, 'locale', {
31
+ writable: true,
32
+ private: false,
33
+ configurable: false,
34
+ visible: false,
35
+ type: 'string',
36
+ });
37
+
38
+ coreApiService.addCreateLocalizationAction(contentType);
39
+ coreApiService.addGraphqlLocalizationAction(contentType);
40
+ }
41
+ });
42
+
43
+ strapi.db.migrations.register(fieldMigration);
44
+ strapi.db.migrations.register(enableContentTypeMigration);
45
+ strapi.db.migrations.register(disableContentTypeMigration);
46
+ };
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ const { get } = require('lodash/fp');
4
+ const { getService } = require('../../utils');
5
+
6
+ const validateLocaleCreation = async (ctx, next) => {
7
+ const { model } = ctx.params;
8
+ const { query, body } = ctx.request;
9
+
10
+ const {
11
+ getValidLocale,
12
+ getNewLocalizationsFrom,
13
+ isLocalizedContentType,
14
+ getAndValidateRelatedEntity,
15
+ fillNonLocalizedAttributes,
16
+ } = getService('content-types');
17
+
18
+ const modelDef = strapi.getModel(model);
19
+
20
+ if (!isLocalizedContentType(modelDef)) {
21
+ return next();
22
+ }
23
+
24
+ const locale = get('plugins.i18n.locale', query);
25
+ const relatedEntityId = get('plugins.i18n.relatedEntityId', query);
26
+ // cleanup to avoid creating duplicates in singletypes
27
+ ctx.request.query = {};
28
+
29
+ let entityLocale;
30
+ try {
31
+ entityLocale = await getValidLocale(locale);
32
+ } catch (e) {
33
+ return ctx.badRequest("This locale doesn't exist");
34
+ }
35
+
36
+ body.locale = entityLocale;
37
+
38
+ if (modelDef.kind === 'singleType') {
39
+ const entity = await strapi.entityService.find(
40
+ { params: { _locale: entityLocale } },
41
+ { model }
42
+ );
43
+
44
+ ctx.request.query._locale = body.locale;
45
+
46
+ // updating
47
+ if (entity) {
48
+ return next();
49
+ }
50
+ }
51
+
52
+ let relatedEntity;
53
+ try {
54
+ relatedEntity = await getAndValidateRelatedEntity(relatedEntityId, model, entityLocale);
55
+ } catch (e) {
56
+ return ctx.badRequest(
57
+ "The related entity doesn't exist or the entity already exists in this locale"
58
+ );
59
+ }
60
+
61
+ fillNonLocalizedAttributes(body, relatedEntity, { model });
62
+ const localizations = await getNewLocalizationsFrom(relatedEntity);
63
+ body.localizations = localizations;
64
+
65
+ return next();
66
+ };
67
+
68
+ module.exports = validateLocaleCreation;
@@ -0,0 +1,64 @@
1
+ {
2
+ "routes": [
3
+ {
4
+ "method": "GET",
5
+ "path": "/iso-locales",
6
+ "handler": "iso-locales.listIsoLocales",
7
+ "config": {
8
+ "policies": [
9
+ "admin::isAuthenticatedAdmin",
10
+ ["plugins::content-manager.hasPermissions", ["plugins::i18n.locale.read"]]
11
+ ]
12
+ }
13
+ },
14
+ {
15
+ "method": "GET",
16
+ "path": "/locales",
17
+ "handler": "locales.listLocales",
18
+ "config": {
19
+ "policies": []
20
+ }
21
+ },
22
+ {
23
+ "method": "POST",
24
+ "path": "/locales",
25
+ "handler": "locales.createLocale",
26
+ "config": {
27
+ "policies": [
28
+ "admin::isAuthenticatedAdmin",
29
+ ["plugins::content-manager.hasPermissions", ["plugins::i18n.locale.create"]]
30
+ ]
31
+ }
32
+ },
33
+ {
34
+ "method": "PUT",
35
+ "path": "/locales/:id",
36
+ "handler": "locales.updateLocale",
37
+ "config": {
38
+ "policies": [
39
+ "admin::isAuthenticatedAdmin",
40
+ ["plugins::content-manager.hasPermissions", ["plugins::i18n.locale.update"]]
41
+ ]
42
+ }
43
+ },
44
+ {
45
+ "method": "DELETE",
46
+ "path": "/locales/:id",
47
+ "handler": "locales.deleteLocale",
48
+ "config": {
49
+ "policies": [
50
+ "admin::isAuthenticatedAdmin",
51
+ ["plugins::content-manager.hasPermissions", ["plugins::i18n.locale.delete"]]
52
+ ]
53
+ }
54
+ },
55
+ {
56
+ "method": "POST",
57
+ "path": "/content-manager/actions/get-non-localized-fields",
58
+ "handler": "content-types.getNonLocalizedAttributes",
59
+ "config": {
60
+ "policies": ["admin::isAuthenticatedAdmin"]
61
+ }
62
+ }
63
+ ]
64
+ }
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const { getInitLocale } = require('../');
4
+
5
+ describe('I18N default locale', () => {
6
+ describe('getInitLocale', () => {
7
+ test('The init locale is english by default', () => {
8
+ expect(getInitLocale()).toStrictEqual({
9
+ code: 'en',
10
+ name: 'English (en)',
11
+ });
12
+ });
13
+
14
+ test('The init locale can be configured by an env var', () => {
15
+ process.env.STRAPI_PLUGIN_I18N_INIT_LOCALE_CODE = 'fr';
16
+ expect(getInitLocale()).toStrictEqual({
17
+ code: 'fr',
18
+ name: 'French (fr)',
19
+ });
20
+ });
21
+
22
+ test('Throws if env var code is unknown in iso list', () => {
23
+ process.env.STRAPI_PLUGIN_I18N_INIT_LOCALE_CODE = 'zzzzz';
24
+ expect(() => getInitLocale()).toThrow();
25
+ });
26
+ });
27
+ });