@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.
- package/LICENSE +22 -0
- package/README.md +19 -0
- package/admin/src/assets/images/logo.svg +1 -0
- package/admin/src/components/CMEditViewCopyLocale/index.js +183 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/cleanData.js +36 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/generateOptions.js +22 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/index.js +2 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/removePasswordAndRelationsFieldFromData.js +54 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/tests/cleanData.test.js +83 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/tests/data.js +219 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/tests/generateOptions.test.js +79 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/tests/removePasswordAndRelationsFieldFromData.test.js +40 -0
- package/admin/src/components/CMEditViewInjectedComponents/index.js +58 -0
- package/admin/src/components/CMEditViewLocalePicker/Option.js +66 -0
- package/admin/src/components/CMEditViewLocalePicker/Wrapper.js +8 -0
- package/admin/src/components/CMEditViewLocalePicker/index.js +160 -0
- package/admin/src/components/CMEditViewLocalePicker/utils/addStatusColorToLocale.js +24 -0
- package/admin/src/components/CMEditViewLocalePicker/utils/createLocalesOption.js +20 -0
- package/admin/src/components/CMEditViewLocalePicker/utils/index.js +2 -0
- package/admin/src/components/CheckboxConfirmation/Wrapper.js +12 -0
- package/admin/src/components/CheckboxConfirmation/index.js +70 -0
- package/admin/src/components/DeleteModalAdditionalInfos/index.js +25 -0
- package/admin/src/components/LocaleList/index.js +101 -0
- package/admin/src/components/LocaleListCell/LocaleListCell.js +90 -0
- package/admin/src/components/LocaleListCell/tests/LocaleListCell.test.js +128 -0
- package/admin/src/components/LocalePicker/index.js +126 -0
- package/admin/src/components/LocaleRow/index.js +77 -0
- package/admin/src/components/ModalCreate/AdvancedForm.js +45 -0
- package/admin/src/components/ModalCreate/BaseForm.js +103 -0
- package/admin/src/components/ModalCreate/index.js +136 -0
- package/admin/src/components/ModalDelete/index.js +49 -0
- package/admin/src/components/ModalEdit/AdvancedForm.js +51 -0
- package/admin/src/components/ModalEdit/BaseForm.js +91 -0
- package/admin/src/components/ModalEdit/index.js +122 -0
- package/admin/src/components/SettingsModal.js +66 -0
- package/admin/src/components/index.js +2 -0
- package/admin/src/containers/Initializer.js +31 -0
- package/admin/src/containers/SettingsPage/LocaleSettingsPage.js +69 -0
- package/admin/src/containers/SettingsPage/index.js +33 -0
- package/admin/src/containers/SettingsPage/tests/SettingsPage.test.js +744 -0
- package/admin/src/containers/SettingsPage/tests/__snapshots__/SettingsPage.test.js.snap +241 -0
- package/admin/src/hooks/constants.js +6 -0
- package/admin/src/hooks/reducers.js +63 -0
- package/admin/src/hooks/tests/reducers.test.js +203 -0
- package/admin/src/hooks/useAddLocale/index.js +60 -0
- package/admin/src/hooks/useContentTypePermissions/index.js +16 -0
- package/admin/src/hooks/useDefaultLocales/index.js +27 -0
- package/admin/src/hooks/useDeleteLocale/index.js +45 -0
- package/admin/src/hooks/useEditLocale/index.js +46 -0
- package/admin/src/hooks/useHasI18n/index.js +13 -0
- package/admin/src/hooks/useLocales/index.js +35 -0
- package/admin/src/index.js +169 -0
- package/admin/src/middlewares/addCommonFieldsToInitialDataMiddleware.js +83 -0
- package/admin/src/middlewares/addLocaleColumnToListViewMiddleware.js +32 -0
- package/admin/src/middlewares/addLocaleToCollectionTypesMiddleware.js +25 -0
- package/admin/src/middlewares/addLocaleToSingleTypesMiddleware.js +25 -0
- package/admin/src/middlewares/extendCMEditViewLayoutMiddleware.js +159 -0
- package/admin/src/middlewares/extendCTBAttributeInitialDataMiddleware.js +58 -0
- package/admin/src/middlewares/extendCTBInitialDataMiddleware.js +33 -0
- package/admin/src/middlewares/index.js +21 -0
- package/admin/src/middlewares/localePermissionMiddleware.js +39 -0
- package/admin/src/middlewares/tests/addCommonFieldsToInitialDataMiddleware.test.js +97 -0
- package/admin/src/middlewares/tests/addLocaleColumnToListViewMiddleware.test.js +68 -0
- package/admin/src/middlewares/tests/addLocaleToCollectionTypesMiddleware.test.js +200 -0
- package/admin/src/middlewares/tests/addLocaleToSingleTypesMiddleware.test.js +193 -0
- package/admin/src/middlewares/tests/extendCMEditViewLayoutMiddleware.test.js +556 -0
- package/admin/src/middlewares/tests/extendCTBAttrributeInitialDataMiddleware.test.js +124 -0
- package/admin/src/middlewares/tests/extendCTBInitialDataMiddleware.test.js +92 -0
- package/admin/src/middlewares/tests/localePermissionMiddleware.test.js +150 -0
- package/admin/src/middlewares/utils/addLocaleToLinksSearch.js +56 -0
- package/admin/src/middlewares/utils/tests/addLocaleToLinksSearch.test.js +137 -0
- package/admin/src/permissions.js +9 -0
- package/admin/src/pluginId.js +5 -0
- package/admin/src/schemas.js +7 -0
- package/admin/src/selectors/selectCollectionTypesRelatedPermissions.js +4 -0
- package/admin/src/selectors/selectI18nLocales.js +3 -0
- package/admin/src/translations/en.json +60 -0
- package/admin/src/translations/fr.json +9 -0
- package/admin/src/translations/index.js +11 -0
- package/admin/src/translations/zh-Hans.json +60 -0
- package/admin/src/utils/getDefaultLocale.js +60 -0
- package/admin/src/utils/getInitialLocale.js +14 -0
- package/admin/src/utils/getLocaleFromQuery.js +7 -0
- package/admin/src/utils/getTrad.js +5 -0
- package/admin/src/utils/index.js +2 -0
- package/admin/src/utils/localizedFields.js +23 -0
- package/admin/src/utils/mutateCTBContentTypeSchema.js +66 -0
- package/admin/src/utils/tests/getDefaultLocale.test.js +337 -0
- package/admin/src/utils/tests/getInitialLocale.test.js +106 -0
- package/admin/src/utils/tests/mutateCTBContentTypeSchema.test.js +205 -0
- package/config/functions/bootstrap.js +57 -0
- package/config/functions/migrations/__tests__/content-type.test.js +255 -0
- package/config/functions/migrations/__tests__/field.test.js +150 -0
- package/config/functions/migrations/content-type/disable/index.js +34 -0
- package/config/functions/migrations/content-type/disable/migrate-for-bookshelf.js +58 -0
- package/config/functions/migrations/content-type/disable/migrate-for-mongoose.js +39 -0
- package/config/functions/migrations/content-type/enable/index.js +40 -0
- package/config/functions/migrations/content-type/utils/index.js +27 -0
- package/config/functions/migrations/field/__tests__/utils.test.js +53 -0
- package/config/functions/migrations/field/index.js +37 -0
- package/config/functions/migrations/field/migrate-for-bookshelf.js +72 -0
- package/config/functions/migrations/field/migrate-for-mongoose.js +24 -0
- package/config/functions/migrations/field/migrate.js +55 -0
- package/config/functions/migrations/field/utils.js +58 -0
- package/config/functions/register.js +46 -0
- package/config/policies/validateLocaleCreation.js +68 -0
- package/config/routes.json +64 -0
- package/constants/__tests__/index.test.js +27 -0
- package/constants/index.js +36 -0
- package/constants/iso-locales.json +2002 -0
- package/controllers/__tests__/content-types.test.js +113 -0
- package/controllers/__tests__/iso-locales.test.js +26 -0
- package/controllers/__tests__/locales.test.js +308 -0
- package/controllers/content-types.js +64 -0
- package/controllers/iso-locales.js +11 -0
- package/controllers/locales.js +104 -0
- package/domain/locale.js +10 -0
- package/middlewares/i18n/defaults.json +5 -0
- package/middlewares/i18n/index.js +28 -0
- package/models/Locale.settings.json +31 -0
- package/oas.yml +195 -0
- package/package.json +31 -0
- package/services/__tests__/__snapshots__/iso-locales.test.js.snap +2006 -0
- package/services/__tests__/content-types.test.js +545 -0
- package/services/__tests__/core-api.test.js +106 -0
- package/services/__tests__/entity-service-decorator.test.js +280 -0
- package/services/__tests__/iso-locales.test.js +11 -0
- package/services/__tests__/locales.test.js +237 -0
- package/services/__tests__/localizations.test.js +187 -0
- package/services/__tests__/metrics.test.js +90 -0
- package/services/content-types.js +200 -0
- package/services/core-api.js +296 -0
- package/services/entity-service-decorator.js +155 -0
- package/services/iso-locales.js +9 -0
- package/services/locales.js +97 -0
- package/services/localizations.js +65 -0
- package/services/metrics.js +24 -0
- package/services/permissions/actions.js +124 -0
- package/services/permissions/engine.js +63 -0
- package/services/permissions/sections-builder.js +48 -0
- package/services/permissions.js +11 -0
- package/tests/content-manager/list-relation.test.e2e.js +122 -0
- package/tests/graphql.test.e2e.js +120 -0
- package/tests/locales.test.e2e.js +414 -0
- package/utils/index.js +20 -0
- package/validation/content-types.js +30 -0
- 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
|
+
});
|