UNPKG

apostrophe

Version:
858 lines (757 loc) 24.6 kB
const cheerio = require('cheerio'); const assert = require('node:assert/strict'); const t = require('../test-lib/test.js'); describe('static i18n', function() { this.timeout(t.timeout); let apos; before(async function() { apos = await t.create({ root: module, modules: { '@apostrophecms/i18n': { options: { locales: { en: {}, fr: { prefix: '/fr', intlMapping: 'fr-FR' }, he: { label: 'Hebrew', prefix: '/he', direction: 'rtl' } } } }, 'i18n-test-page': {}, example: { options: { i18n: {} } }, 'apos-fr': { options: { i18n: { // Legacy technique must work ns: 'apostrophe' } } }, // A base class that contributes some namespaced phrases in the new // style way (subdirs) 'base-type': { instantiate: false }, // Also contributes namespaced phrases in the new style way (subdirs) // plus default locale phrases in the root i18n folder subtype: { extend: 'base-type' } } }); }); after(async function() { this.timeout(t.timeout); return t.destroy(apos); }); it('should should exist on the apos object', async function() { assert(apos.i18n); assert(apos.i18n.i18next); }); it('should preserve intlMapping property on locales', async function() { assert.equal(apos.i18n.locales.fr.intlMapping, 'fr-FR'); assert.equal(apos.i18n.locales.en.intlMapping, undefined); }); it('should set the lang and dir attributes by default', async function() { const req = apos.task.getReq(); const result = await apos.modules['i18n-test-page'].renderPage(req, 'page'); const $ = cheerio.load(result); const $html = $('html'); const lang = $html.attr('lang'); const dir = $html.attr('dir'); assert.equal(lang, 'en'); assert.equal(dir, 'ltr'); }); it('should set the lang and dir attributes to the current locale', async function() { { const req = apos.task.getReq({ locale: 'fr' }); const result = await apos.modules['i18n-test-page'].renderPage(req, 'page'); const $ = cheerio.load(result); const $html = $('html'); const lang = $html.attr('lang'); const dir = $html.attr('dir'); assert.equal(lang, 'fr', 'fr locale should set lang="fr"'); assert.equal(dir, 'ltr', 'fr locale should set dir="ltr"'); } { const req = apos.task.getReq({ locale: 'he' }); const result = await apos.modules['i18n-test-page'].renderPage(req, 'page'); const $ = cheerio.load(result); const $html = $('html'); const lang = $html.attr('lang'); const dir = $html.attr('dir'); assert.equal(lang, 'he', 'he locale should set lang="he"'); assert.equal(dir, 'rtl', 'he locale should set dir="rtl"'); } }); it('should set `data.i18n` by default', async function() { const req = apos.task.getReq(); const result = await apos.modules['i18n-test-page'].renderPage(req, 'page'); const $ = cheerio.load(result); const locale = $('#locale').text(); const label = $('#label').text(); const direction = $('#direction').text(); assert.equal(locale, 'en'); assert.equal(label, 'en'); assert.equal(direction, 'ltr'); }); it('should set `data.i18n` when direction is not specified', async function() { const req = apos.task.getReq({ locale: 'fr' }); const result = await apos.modules['i18n-test-page'].renderPage(req, 'page'); const $ = cheerio.load(result); const locale = $('#locale').text(); const label = $('#label').text(); const direction = $('#direction').text(); assert.equal(locale, 'fr'); assert.equal(label, 'fr'); assert.equal(direction, 'ltr'); }); it('should set `data.i18n` when direction is specified', async function() { const req = apos.task.getReq({ locale: 'he' }); const result = await apos.modules['i18n-test-page'].renderPage(req, 'page'); const $ = cheerio.load(result); const locale = $('#locale').text(); const label = $('#label').text(); const direction = $('#direction').text(); assert.equal(locale, 'he'); assert.equal(label, 'Hebrew'); assert.equal(direction, 'rtl'); }); it('should localize apostrophe namespace phrases in the default locale', function() { assert.strictEqual(apos.task.getReq().t('apostrophe:notFoundPageTitle'), '404 - Page not found'); }); it('should localize default namespace phrases contributed by a project level module', function() { assert.strictEqual(apos.task.getReq().t('projectLevelPhrase'), 'Project Level Phrase'); }); it('should merge translations in different languages of the same phrases from @apostrophecms/i18n and a different module', function() { assert.strictEqual(apos.task.getReq().t('apostrophe:richTextAlignCenter'), 'Align Center'); }); it('should merge translations in different languages of the same phrases from @apostrophecms/i18n and a different module (fr)', function() { // je suis désolé re: Google Translate-powered French test, feel free to PR // better example assert.strictEqual(apos.task.getReq({ locale: 'fr' }).t('apostrophe:richTextAlignCenter'), 'Aligner Le Centre'); }); it('should fetch default locale phrases from main i18n dir with no i18n option necessary', function() { assert.strictEqual(apos.task.getReq().t('defaultTestOne'), 'Default Test One'); }); it('should fetch custom locale phrases from corresponding subdir', function() { assert.strictEqual(apos.task.getReq().t('custom:customTestTwo'), 'Custom Test Two From Base Type'); assert.strictEqual(apos.task.getReq().t('custom:customTestThree'), 'Custom Test Three From Subtype'); }); it('last appearance in inheritance + configuration order wins', function() { assert.strictEqual(apos.task.getReq().t('custom:customTestOne'), 'Custom Test One From Subtype'); }); it('should honor the browser: true flag in the i18n section of an index.js file', function() { const browserData = apos.i18n.getBrowserData(apos.task.getReq()); assert.strictEqual(browserData.i18n.en.custom.customTestOne, 'Custom Test One From Subtype'); }); it('should not offer adminLocale by default', function () { assert.deepStrictEqual(apos.i18n.adminLocales, []); assert.strictEqual(apos.i18n.defaultAdminLocale, null); assert.strictEqual( apos.user.schema.some((field) => field.name === 'adminLocale'), false ); const browserData = apos.i18n.getBrowserData(apos.task.getReq()); assert.strictEqual(browserData.locale, 'en'); assert.strictEqual(browserData.adminLocale, 'en'); }); it('should add user.adminLocale when configured', async function () { await t.destroy(apos); apos = await t.create({ root: module, modules: { '@apostrophecms/i18n': { options: { locales: { en: {}, fr: { prefix: '/fr' } }, adminLocales: [ { label: 'English', value: 'en' }, { label: 'French', value: 'fr' } ] } } } }); assert.deepStrictEqual( apos.i18n.adminLocales, [ { label: 'English', value: 'en' }, { label: 'French', value: 'fr' } ] ); assert.strictEqual(apos.i18n.defaultAdminLocale, null); const field = apos.user.schema.find((field) => field.name === 'adminLocale'); assert(field); assert.strictEqual(field.type, 'select'); assert.strictEqual(field.choices.length, 3); assert.strictEqual(field.choices[0].value, ''); assert.strictEqual(field.choices[1].value, 'en'); assert.strictEqual(field.choices[2].value, 'fr'); assert.strictEqual(field.def, ''); { const browserData = apos.i18n.getBrowserData(apos.task.getReq({ locale: 'en', user: { adminLocale: 'fr' } })); assert.strictEqual(browserData.locale, 'en'); assert.strictEqual(browserData.adminLocale, 'fr'); } { const browserData = apos.i18n.getBrowserData(apos.task.getReq({ locale: 'fr', user: { adminLocale: 'en' } })); assert.strictEqual(browserData.locale, 'fr'); assert.strictEqual(browserData.adminLocale, 'en'); } { // When user sets Same as Website const browserData = apos.i18n.getBrowserData(apos.task.getReq({ locale: 'fr', user: { adminLocale: '' } })); assert.strictEqual(browserData.locale, 'fr'); assert.strictEqual(browserData.adminLocale, 'fr'); } }); it('should respect defaultAdminLocale when adminLocales are configured', async function () { await t.destroy(apos); apos = await t.create({ root: module, modules: { '@apostrophecms/i18n': { options: { adminLocales: [ { label: 'English', value: 'en' }, { label: 'French', value: 'fr' } ], defaultAdminLocale: 'fr' } } } }); // Correct default value assert.strictEqual(apos.i18n.defaultAdminLocale, 'fr'); const field = apos.user.schema.find((field) => field.name === 'adminLocale'); assert(field); assert.strictEqual(field.def, 'fr'); { // adminLocale is defaultAdminLocale even if user.adminLocale is not // present const browserData = apos.i18n.getBrowserData(apos.task.getReq()); assert.strictEqual(browserData.locale, 'en'); assert.strictEqual(browserData.adminLocale, 'fr'); } { // adminLocale is set to Same as Website const browserData = apos.i18n.getBrowserData(apos.task.getReq({ locale: 'en', user: { adminLocale: '' } })); assert.strictEqual(browserData.locale, 'en'); assert.strictEqual(browserData.adminLocale, 'en'); } }); it('should respect defaultAdminLocale when adminLocales are NOT configured', async function () { await t.destroy(apos); apos = await t.create({ root: module, modules: { '@apostrophecms/i18n': { options: { defaultAdminLocale: 'fr' } } } }); assert.strictEqual(apos.i18n.defaultAdminLocale, 'fr'); assert.strictEqual(apos.user.schema.some((field) => field.name === 'adminLocale'), false); // adminLocale is defaultAdminLocale even if user.adminLocale is not present const browserData = apos.i18n.getBrowserData(apos.task.getReq()); assert.strictEqual(browserData.locale, 'en'); assert.strictEqual(browserData.adminLocale, 'fr'); }); it('should replace accented characters in slugs when configured', async function () { await t.destroy(apos); apos = await t.create({ root: module, modules: { '@apostrophecms/i18n': { options: { stripUrlAccents: true, locales: { en: {}, fr: { prefix: '/fr' } } } }, 'default-page': { extend: '@apostrophecms/page-type' }, 'test-piece': { extend: '@apostrophecms/piece-type' } } }); await apos.doc.db.deleteMany({ type: { $in: [ 'default-page', 'test-piece' ] } }); // Create content while accents are NOT stripped so we have accented slugs apos.i18n.options.stripUrlAccents = false; const req = apos.task.getReq(); // Add a page with accented characters in its title so slug preserves accents await apos.doc.insert(req, { type: 'default-page', visibility: 'public', title: 'C\'est déjà l\'été' }); // Add a piece with accented characters in its title so slug preserves accents await apos.doc.insert(req, { type: 'test-piece', visibility: 'public', title: 'Café au lait' }); // Sanity check: created content has accented slugs/names const pageBefore = await apos.doc.db.findOne({ type: 'default-page', title: 'C\'est déjà l\'été' }); assert(pageBefore); assert.equal(pageBefore.slug, '/c-est-déjà-l-été'); const pieceBefore = await apos.doc.db.findOne({ type: 'test-piece', title: 'Café au lait' }); assert(pieceBefore); assert.equal(pieceBefore.slug, 'café-au-lait'); // Now enable accent stripping and run the task to update existing content apos.i18n.options.stripUrlAccents = true; await apos.task.invoke('@apostrophecms/i18n:strip-slug-accents'); // Verify that the slugs and attachment names have been updated correctly const pageAfter = await apos.doc.db.findOne({ _id: pageBefore._id }); assert(pageAfter); assert.equal(pageAfter.slug, '/c-est-deja-l-ete'); const pieceAfter = await apos.doc.db.findOne({ _id: pieceBefore._id }); assert(pieceAfter); assert.equal(pieceAfter.slug, 'cafe-au-lait'); // Restore default for other tests apos.i18n.options.stripUrlAccents = false; }); it('should report duplicated slug errors', async function () { await t.destroy(apos); apos = await t.create({ root: module, modules: { '@apostrophecms/i18n': { options: { stripUrlAccents: true, locales: { en: {}, fr: { prefix: '/fr' } } } }, 'default-page': { extend: '@apostrophecms/page-type' }, 'test-piece': { extend: '@apostrophecms/piece-type' } } }); await apos.doc.db.deleteMany({ type: { $in: [ 'default-page', 'test-piece' ] } }); apos.i18n.options.stripUrlAccents = false; const req = apos.task.getReq(); await apos.doc.insert(req, { type: 'default-page', visibility: 'public', title: 'C\'est déjà l\'été' }); const nonAccentedExisting = await apos.doc.insert(req, { type: 'default-page', visibility: 'public', title: 'C\'est deja l\'ete', slug: '/c-est-deja-l-ete' }); const pageBefore = await apos.doc.db.findOne({ type: 'default-page', title: 'C\'est déjà l\'été' }); assert(pageBefore); assert.equal(pageBefore.slug, '/c-est-déjà-l-été'); const nonAccentedPageBefore = await apos.doc.db.findOne({ _id: nonAccentedExisting._id }); assert(nonAccentedPageBefore); assert.equal(nonAccentedPageBefore.slug, '/c-est-deja-l-ete'); apos.i18n.options.stripUrlAccents = true; await assert.rejects( async () => { await apos.task.invoke('@apostrophecms/i18n:strip-slug-accents'); }, { message: 'Some documents failed to update their slugs.' } ); apos.i18n.options.stripUrlAccents = false; }); it('should redirect accent-preserving URLs to their stripped versions after running the accent task', async function () { await t.destroy(apos); apos = await t.create({ root: module, modules: { '@apostrophecms/i18n': { options: { stripUrlAccents: true, locales: { en: {}, fr: { prefix: '/fr' } } } }, 'default-page': { extend: '@apostrophecms/page-type' }, 'test-piece': { extend: '@apostrophecms/piece-type' }, 'test-piece-page': { extend: '@apostrophecms/piece-page-type', options: { pieceType: 'test-piece' } } } }); await apos.doc.db.deleteMany({ type: { $in: [ 'default-page', 'test-piece', 'test-piece-page' ] } }); apos.i18n.options.stripUrlAccents = false; const req = apos.task.getReq(); const jar = apos.http.jar(); const parentId = '_home'; const accentedPage = await apos.page.insert(req, parentId, 'lastChild', { type: 'default-page', visibility: 'public', title: 'C\'est déjà l\'été' }); const pieceIndexPage = await apos.page.insert(req, parentId, 'lastChild', { type: 'test-piece-page', visibility: 'public', title: 'Test Piece Page', slug: '/test-piece' }); const piece = await apos.doc.insert(req, { type: 'test-piece', visibility: 'public', title: 'Café au lait' }); const encodedOldPageUrl = encodeURI(accentedPage.slug); const oldPieceSlug = piece.slug; const pieceIndexSlug = pieceIndexPage.slug; const encodedOldPieceUrl = encodeURI(`${pieceIndexSlug}/${oldPieceSlug}`); // Visit the legacy URLs before stripping accents so historic // redirects exist. await apos.http.get(encodedOldPageUrl, { followRedirect: false, fullResponse: true, redirect: 'manual', jar }); await apos.http.get(encodedOldPieceUrl, { followRedirect: false, fullResponse: true, redirect: 'manual', jar }); apos.i18n.options.stripUrlAccents = true; await apos.task.invoke('@apostrophecms/i18n:strip-slug-accents'); const updatedPage = await apos.doc.db.findOne({ _id: accentedPage._id }); const updatedPiece = await apos.doc.db.findOne({ _id: piece._id }); assert(updatedPage); assert(updatedPiece); const pageResponse = await apos.http.get(encodedOldPageUrl, { followRedirect: false, fullResponse: true, redirect: 'manual', jar }); assert.strictEqual(pageResponse.status, 302); assert.strictEqual(pageResponse.headers.location, `${apos.http.getBase()}${updatedPage.slug}`); const pieceResponse = await apos.http.get(encodedOldPieceUrl, { followRedirect: false, fullResponse: true, redirect: 'manual', jar }); assert.strictEqual(pieceResponse.status, 302); assert.strictEqual( pieceResponse.headers.location, `${apos.http.getBase()}${pieceIndexSlug}/${updatedPiece.slug}` ); apos.i18n.options.stripUrlAccents = false; }); }); describe('private locales', function() { this.timeout(t.timeout); let apos; before(async function() { apos = await t.create({ root: module, baseUrl: 'http://localhost:3000', modules: { '@apostrophecms/i18n': { options: { locales: { en: {}, sk: { prefix: '/sk', private: true } } } } } }); }); after(async function() { this.timeout(t.timeout); return t.destroy(apos); }); this.timeout(t.timeout); it('should return a 404 HTTP error code when a logged out user tries to access to a content in a private locale', async function() { try { await apos.http.get('/sk'); } catch (error) { assert(error.status === 404); return; } throw new Error('should have thrown 404 error'); }); }); describe('redirection to first locale', function() { this.timeout(t.timeout); let apos; before(async function() { apos = await t.create({ root: module, baseUrl: 'http://localhost:3000', modules: { '@apostrophecms/page': { options: { park: [ { parkedId: 'child', title: 'Child', slug: '/child', type: 'default-page' } ] } }, 'default-page': {}, '@apostrophecms/i18n': { options: { redirectToFirstLocale: true, locales: { en: { label: 'English', hostname: 'en.localhost:3000', prefix: '/en' }, 'en-CA': { label: 'Canada', hostname: 'ca.localhost:3000', prefix: '/en-ca' }, 'fr-CA': { label: 'French Canada', hostname: 'ca.localhost:3000', prefix: '/fr-ca' }, es: { label: 'Spain', prefix: '/es' }, it: { label: 'Italy', prefix: '/it' } } } } } }); }); after(async function() { this.timeout(t.timeout); return t.destroy(apos); }); this.timeout(t.timeout); it('should not redirect to the first prefixed locale when the page requested is not the homepage', async function() { const response = await apos.http.get('/child', { followRedirect: false, fullResponse: true, redirect: 'manual' }); assert.strictEqual(response.status, 200); assert.strictEqual(response.headers.location, undefined); }); it('should redirect to the first prefixed locale', async function() { const response = await apos.http.get('/', { followRedirect: false, fullResponse: true, redirect: 'manual' }); assert.strictEqual(response.headers.location, `${apos.http.getBase()}/es/`); }); it('should redirect to the first prefixed locale that matches the requested hostname', async function() { const server = apos.modules['@apostrophecms/express'].server; const response = await apos.http.get(`http://ca.localhost:${server.address().port}`, { followRedirect: false, fullResponse: true, redirect: 'manual' }); assert.strictEqual(response.headers.location, `http://ca.localhost:${server.address().port}/en-ca/`); }); }); describe('no redirection to first locale', function() { this.timeout(t.timeout); let apos; before(async function() { apos = await t.create({ root: module, baseUrl: 'http://localhost:3000', modules: { '@apostrophecms/i18n': { options: { redirectToFirstLocale: true, locales: { en: { label: 'English', hostname: 'en.localhost:3000', prefix: '/en' }, 'en-CA': { label: 'Canada', hostname: 'ca.localhost:3000', prefix: '/en-ca' }, 'fr-CA': { label: 'French Canada', hostname: 'ca.localhost:3000' // no prefix }, es: { label: 'Spain', prefix: '/es' }, it: { label: 'Italy' // no prefix } } } } } }); }); after(async function() { this.timeout(t.timeout); return t.destroy(apos); }); this.timeout(t.timeout); it('should not redirect to the first prefixed locale when at least one locale has no prefix', async function() { const response = await apos.http.get('/', { followRedirect: false, fullResponse: true, redirect: 'manual' }); assert.strictEqual(response.status, 200); assert.strictEqual(response.headers.location, undefined); }); it('should not redirect to the first prefixed locale that matches the requested hostname when at least one locale has no prefix', async function() { const server = apos.modules['@apostrophecms/express'].server; const response = await apos.http.get(`http://ca.localhost:${server.address().port}`, { followRedirect: false, fullResponse: true, redirect: 'manual' }); assert.strictEqual(response.status, 200); assert.strictEqual(response.headers.location, undefined); }); });