Skip to content

Commit b7799f6

Browse files
authored
PRO-8154: Strip Latin accents (#5137)
* Add stripUrlAccents option to control accent stripping in slugs and attachment names
1 parent 6029860 commit b7799f6

10 files changed

Lines changed: 382 additions & 9276 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Ignore unison sync files
44
.unison*
55
package-lock.json
6+
pnpm-lock.yaml
67
npm-debug.log
78
*.DS_Store
89
*.npmignore

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Adds
66

7+
* Adds `stripUrlAccents` option in `@apostrophecms/i18n` module to globally control whether accents are stripped from URLs. When set to `true`, all URLs (slugs) will have accents from Latin characters removed on document creation and updates. No existing documents are modified automatically; this only affects new or updated documents. A new task `node app @apostrophecms/i18n:strip-slug-accents` is provided to update existing document slugs in the database when needed.
78
* Add `@apostrophecms/migration:add-missing-schema-fields` task. This task does not run database migrations.
89
* Translation strings added for the layout- and layout-column-widgets.
910
* New `box` schema field type

‎modules/@apostrophecms/doc/index.js‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,11 @@ module.exports = {
205205
'@apostrophecms/doc-type:beforePublish': {
206206
testPermissions(req, info) {
207207
if (info.options.permissions !== false) {
208-
if (!self.apos.permission.can(req, info.options.autopublishing ? 'edit' : 'publish', info.draft)) {
208+
if (!self.apos.permission.can(
209+
req,
210+
info.options.autopublishing ? 'edit' : 'publish',
211+
info.draft
212+
)) {
209213
throw self.apos.error('forbidden');
210214
}
211215
}
@@ -217,7 +221,11 @@ module.exports = {
217221
manager.ensureSlug(doc);
218222
_.each(manager.schema, function (field) {
219223
if (field.sortify) {
220-
doc[field.name + 'Sortified'] = self.apos.util.sortify(doc[field.name] ? doc[field.name] : '');
224+
doc[field.name + 'Sortified'] = self.apos.util.sortify(
225+
doc[field.name]
226+
? doc[field.name]
227+
: ''
228+
);
221229
}
222230
});
223231
if (options.setUpdatedAtAndBy !== false) {

‎modules/@apostrophecms/i18n/index.js‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ module.exports = {
7070
i18n: {
7171
ns: 'apostrophe',
7272
browser: true
73-
}
73+
},
74+
// If true, slugifying will strip accents from Latin characters
75+
stripUrlAccents: false
7476
},
7577
async init(self) {
7678
self.defaultNamespace = 'default';
@@ -677,7 +679,8 @@ module.exports = {
677679
debug: self.debug,
678680
show: self.show,
679681
action: self.action,
680-
crossDomainClipboard: req.session && req.session.aposCrossDomainClipboard
682+
crossDomainClipboard: req.session && req.session.aposCrossDomainClipboard,
683+
stripUrlAccents: self.options.stripUrlAccents
681684
};
682685
if (req.session && req.session.aposCrossDomainClipboard) {
683686
req.session.aposCrossDomainClipboard = null;
@@ -734,6 +737,9 @@ module.exports = {
734737
}
735738
return locale;
736739
},
740+
shouldStripAccents() {
741+
return self.options.stripUrlAccents === true;
742+
},
737743
addLocalizeModal() {
738744
self.apos.modal.add(
739745
`${self.__meta.name}:localize`,
@@ -1251,6 +1257,38 @@ module.exports = {
12511257
console.log(`Due to conflicts, kept ${kept} documents from ${keep}`);
12521258
}
12531259
}
1260+
},
1261+
'strip-slug-accents': {
1262+
usage: 'Remove Latin accent characters from all document slugs. Usage: node app @apostrophecms/i18n:strip-slug-accents',
1263+
async task() {
1264+
let docChanged = 0;
1265+
1266+
await self.apos.migration.eachDoc({}, 5, async doc => {
1267+
const slug = doc.slug;
1268+
const req = self.apos.task.getAdminReq({
1269+
locale: doc.aposLocale?.split(':')[0] || self.defaultLocale
1270+
});
1271+
if (!self.shouldStripAccents()) {
1272+
return;
1273+
}
1274+
1275+
doc.slug = _.deburr(doc.slug);
1276+
if (slug === doc.slug) {
1277+
return;
1278+
}
1279+
const manager = self.apos.doc.getManager(doc.type);
1280+
if (!manager) {
1281+
return;
1282+
}
1283+
await manager.update(req, doc, { permissions: false });
1284+
docChanged++;
1285+
self.apos.util.log(`Updated doc [${req.locale}] "${slug}" -> "${doc.slug}"`);
1286+
});
1287+
1288+
self.apos.util.log(
1289+
`Updated ${docChanged} document slug(s).`
1290+
);
1291+
}
12541292
}
12551293
};
12561294
}

‎modules/@apostrophecms/schema/ui/apos/logic/AposInputSlug.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// errors.
44
import { klona } from 'klona';
55
import sluggo from 'sluggo';
6+
import { deburr } from 'lodash';
67
import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
78
import { debounceAsync } from 'Modules/@apostrophecms/ui/utils';
89

@@ -48,6 +49,9 @@ export default {
4849
},
4950
localePrefix() {
5051
return this.field.page && apos.i18n.locales[apos.i18n.locale].prefix;
52+
},
53+
stripAccents() {
54+
return apos.i18n.stripUrlAccents === true;
5155
}
5256
},
5357
watch: {
@@ -192,6 +196,9 @@ export default {
192196
}
193197

194198
let slug = sluggo(s, options);
199+
if (this.stripAccents) {
200+
slug = deburr(slug);
201+
}
195202
if (preserveDash) {
196203
slug += '-';
197204
}

‎modules/@apostrophecms/util/index.js‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,18 @@ module.exports = {
291291
// ONE punctuation character normally forbidden in slugs may
292292
// optionally be permitted by specifying it via options.allow.
293293
// The separator may be changed via options.separator.
294+
// By default, the i18n.options.stripUrlAccents option is honored;
295+
// having stripAccents passed as an option takes precedence.
294296
slugify(s, options) {
295-
return require('sluggo')(s, options);
297+
const { stripAccents, ...opts } = options || {};
298+
const slug = require('sluggo')(s, opts);
299+
const shouldStripAccents = (typeof stripAccents !== 'undefined')
300+
? stripAccents
301+
: self.apos.i18n.options.stripUrlAccents;
302+
if (shouldStripAccents) {
303+
return _.deburr(slug);
304+
}
305+
return slug;
296306
},
297307
// Returns a string that, when used for indexes, behaves
298308
// similarly to MySQL's default behavior for sorting, plus a little
@@ -938,7 +948,8 @@ module.exports = {
938948
// ONE punctuation character normally forbidden in slugs may
939949
// optionally be permitted by specifying it via options.allow.
940950
// The separator may be changed via options.separator.
941-
951+
// By default, the i18n.options.stripUrlAccents option is honored;
952+
// having stripAccents passed as an option takes precedence.
942953
slugify: function(string, options) {
943954
return self.slugify(string, options);
944955
},

0 commit comments

Comments
 (0)