diff --git a/components/page-header/LanguagePicker.tsx b/components/page-header/LanguagePicker.tsx index f4ac20ef6f..a98362257b 100644 --- a/components/page-header/LanguagePicker.tsx +++ b/components/page-header/LanguagePicker.tsx @@ -1,9 +1,14 @@ import { useRouter } from 'next/router' +import Cookies from 'js-cookie' + import { Link } from 'components/Link' import { useLanguages } from 'components/context/LanguagesContext' import { Picker } from 'components/ui/Picker' import { useTranslation } from 'components/hooks/useTranslation' +// This value is replicated in two places! See middleware/detect-language.js +const PREFERRED_LOCALE_COOKIE_NAME = 'preferredlang' + type Props = { variant?: 'inline' } @@ -22,6 +27,22 @@ export const LanguagePicker = ({ variant }: Props) => { // in a "denormalized" way. const routerPath = router.asPath.split('#')[0] + function rememberPreferredLanguage(code: string) { + try { + Cookies.set(PREFERRED_LOCALE_COOKIE_NAME, code, { + expires: 365, + secure: document.location.protocol !== 'http:', + }) + } catch (err) { + // You can never be too careful because setting a cookie + // can fail. For example, some browser + // extensions disallow all setting of cookies and attempts + // at the `document.cookie` setter could throw. Just swallow + // and move on. + console.warn('Unable to set preferred language cookie', err) + } + } + return ( { text: lang.nativeName || lang.name, selected: lang === selectedLang, item: ( - + { + rememberPreferredLanguage(lang.code) + }} + > {lang.nativeName ? ( <> {lang.nativeName} ( diff --git a/content/admin/configuration/configuring-github-connect/enabling-automatic-user-license-sync-for-your-enterprise.md b/content/admin/configuration/configuring-github-connect/enabling-automatic-user-license-sync-for-your-enterprise.md index e559da075a..1fded4006e 100644 --- a/content/admin/configuration/configuring-github-connect/enabling-automatic-user-license-sync-for-your-enterprise.md +++ b/content/admin/configuration/configuring-github-connect/enabling-automatic-user-license-sync-for-your-enterprise.md @@ -1,6 +1,6 @@ --- title: Enabling automatic user license sync for your enterprise -intro: 'You can manage license usage across your {% data variables.product.prodname_enterprise %} deployments by automatically syncing user licenses from {% data variables.product.product_location %} to {% data variables.product.prodname_ghe_cloud %}.' +intro: 'You can manage license usage across your {% data variables.product.prodname_enterprise %} environments by automatically syncing user licenses from {% data variables.product.product_location %} to {% data variables.product.prodname_ghe_cloud %}.' redirect_from: - /enterprise/admin/installation/enabling-automatic-user-license-sync-between-github-enterprise-server-and-github-enterprise-cloud - /enterprise/admin/configuration/enabling-automatic-user-license-sync-between-github-enterprise-server-and-github-enterprise-cloud @@ -19,10 +19,18 @@ shortTitle: Automatic user license sync --- ## About license synchronization -After you enable license synchronization, you'll be able to view license usage for your entire enterprise across {% data variables.product.prodname_ghe_server %} and {% data variables.product.prodname_ghe_cloud %}. {% data variables.product.prodname_github_connect %} syncs license between {% data variables.product.prodname_ghe_server %} and {% data variables.product.prodname_ghe_cloud %} weekly. For more information, see "[Managing your license for {% data variables.product.prodname_enterprise %}](/billing/managing-your-license-for-github-enterprise)." +{% data reusables.enterprise-licensing.about-license-sync %} For more information, see "[About {% data variables.product.prodname_github_connect %}](/admin/configuration/configuring-github-connect/about-github-connect#data-transmission-for-github-connect)." + +If you enable automatic user license sync for your enterprise, {% data variables.product.prodname_github_connect %} will automatically synchronize license usage between {% data variables.product.prodname_ghe_server %} and {% data variables.product.prodname_ghe_cloud %} weekly. + +If you use multiple {% data variables.product.prodname_ghe_server %} instances, you can enable automatic license sync between each of your instances and the same organization or enterprise account on {% data variables.product.prodname_ghe_cloud %}. + +{% data reusables.enterprise-licensing.view-consumed-licenses %} You can also manually upload {% data variables.product.prodname_ghe_server %} user license information to {% data variables.product.prodname_ghe_cloud %}. For more information, see "[Syncing license usage between {% data variables.product.prodname_ghe_server %} and {% data variables.product.prodname_ghe_cloud %}](/billing/managing-your-license-for-github-enterprise/syncing-license-usage-between-github-enterprise-server-and-github-enterprise-cloud)." +{% data reusables.enterprise-licensing.verified-domains-license-sync %} + ## Enabling license synchronization Before enabling license synchronization on {% data variables.product.product_location %}, you must enable {% data variables.product.prodname_github_connect %}. For more information, see "[Managing {% data variables.product.prodname_github_connect %}](/admin/configuration/configuring-github-connect/managing-github-connect)." diff --git a/content/billing/managing-your-license-for-github-enterprise/syncing-license-usage-between-github-enterprise-server-and-github-enterprise-cloud.md b/content/billing/managing-your-license-for-github-enterprise/syncing-license-usage-between-github-enterprise-server-and-github-enterprise-cloud.md index 3e8e79b851..2fae8de7d2 100644 --- a/content/billing/managing-your-license-for-github-enterprise/syncing-license-usage-between-github-enterprise-server-and-github-enterprise-cloud.md +++ b/content/billing/managing-your-license-for-github-enterprise/syncing-license-usage-between-github-enterprise-server-and-github-enterprise-cloud.md @@ -16,13 +16,17 @@ shortTitle: Sync license usage {% data reusables.enterprise-licensing.about-license-sync %} -If you allow {% data variables.product.product_location_enterprise %} to connect to your enterprise account on {% data variables.product.prodname_dotcom_the_website %}, you can sync license usage between the environments automatically. Automatic synchronization ensures that you see up-to-date license details on {% data variables.product.prodname_dotcom_the_website %}. If you don't want to allow {% data variables.product.product_location %} to connect to {% data variables.product.prodname_dotcom_the_website %}, you can manually sync license usage by uploading a file from {% data variables.product.product_location %} to {% data variables.product.prodname_dotcom_the_website %}. +To ensure that you see up-to-date license details on {% data variables.product.prodname_dotcom_the_website %}, you can sync license usage between the environments automatically, using {% data variables.product.prodname_github_connect %}. For more information about {% data variables.product.prodname_github_connect %}, see "[About {% data variables.product.prodname_github_connect %}]({% ifversion ghec %}/enterprise-server@latest{% endif %}/admin/configuration/configuring-github-connect/about-github-connect){% ifversion ghec %}" in the {% data variables.product.prodname_ghe_server %} documentation.{% elsif ghes %}."{% endif %} -For more information about licenses and usage for {% data variables.product.prodname_ghe_server %}, see "[About licenses for {% data variables.product.prodname_enterprise %}](/billing/managing-your-license-for-github-enterprise/about-licenses-for-github-enterprise)." +If you don't want to enable {% data variables.product.prodname_github_connect %}, you can manually sync license usage by uploading a file from {% data variables.product.prodname_ghe_server %} to {% data variables.product.prodname_dotcom_the_website %}. + +{% data reusables.enterprise-licensing.view-consumed-licenses %} + +{% data reusables.enterprise-licensing.verified-domains-license-sync %} ## Automatically syncing license usage -You can use {% data variables.product.prodname_github_connect %} to automatically sync user license count and usage between {% data variables.product.prodname_ghe_server %} and {% data variables.product.prodname_ghe_cloud %}. For more information, see "[Enabling automatic user license sync for your enterprise]({% ifversion ghec %}/enterprise-server@latest{% endif %}/admin/configuration/configuring-github-connect/enabling-automatic-user-license-sync-for-your-enterprise){% ifversion ghec %}" in the {% data variables.product.prodname_ghe_server %} documentation.{% elsif ghes %}."{% endif %} +You can use {% data variables.product.prodname_github_connect %} to automatically synchronize user license count and usage between {% data variables.product.prodname_ghe_server %} and {% data variables.product.prodname_ghe_cloud %}. For more information, see "[Enabling automatic user license sync for your enterprise]({% ifversion ghec %}/enterprise-server@latest{% endif %}/admin/configuration/configuring-github-connect/enabling-automatic-user-license-sync-for-your-enterprise){% ifversion ghec %}" in the {% data variables.product.prodname_ghe_server %} documentation.{% elsif ghes %}."{% endif %} ## Manually syncing license usage diff --git a/data/reusables/enterprise-licensing/about-license-sync.md b/data/reusables/enterprise-licensing/about-license-sync.md index e841fae540..0899af752f 100644 --- a/data/reusables/enterprise-licensing/about-license-sync.md +++ b/data/reusables/enterprise-licensing/about-license-sync.md @@ -1 +1,5 @@ -You can allocate the user count for your {% data variables.product.prodname_enterprise %} license to members of both {% data variables.product.product_location_enterprise %} and an enterprise account on {% data variables.product.prodname_ghe_cloud %}. When you add a user to either environment, the user will consume one license. If a user has accounts in both environments, to consume only one license, the user's primary email address on {% data variables.product.product_location_enterprise %} must be the same as the user's verified email address on {% data variables.product.prodname_dotcom_the_website %}. You can sync license count and usage between the environments. +{% data variables.product.prodname_enterprise %} uses a unique-user licensing model, where each person only consumes one license, no matter how many {% data variables.product.prodname_ghe_server %} instances the person uses, or how many organizations the person is a member of on {% data variables.product.prodname_ghe_cloud %}. This model allows each person to use multiple {% data variables.product.prodname_enterprise %} environments without incurring extra costs. + +For a person using multiple {% data variables.product.prodname_enterprise %} environments to only consume a single license, you must synchronize license usage between environments. Then, {% data variables.product.company_short %} will deduplicate users based on the email addresses associated with their personal accounts. Multiple personal accounts will consume a single license when there is a match between an account's primary email address on {% data variables.product.prodname_ghe_server %} and/or an account's verified email address on {% data variables.product.prodname_dotcom_the_website %}. For more information about verification of email addresses on {% data variables.product.prodname_dotcom_the_website %}, see "[Verifying your email address](/enterprise-cloud@latest/get-started/signing-up-for-github/verifying-your-email-address){% ifversion not ghec %}" in the {% data variables.product.prodname_ghe_cloud %} documentation.{% else %}."{% endif %} + +When you synchronize license usage, only the user ID and email addresses for each personal account on {% data variables.product.prodname_ghe_server %} are transmitted to {% data variables.product.prodname_ghe_cloud %}. \ No newline at end of file diff --git a/data/reusables/enterprise-licensing/verified-domains-license-sync.md b/data/reusables/enterprise-licensing/verified-domains-license-sync.md new file mode 100644 index 0000000000..3a93c7b664 --- /dev/null +++ b/data/reusables/enterprise-licensing/verified-domains-license-sync.md @@ -0,0 +1,5 @@ +{% note %} + +**Note:** If you synchronize license usage and your enterprise account on {% data variables.product.prodname_dotcom_the_website %} does not use {% data variables.product.prodname_emus %}, we highly recommend enabling verified domains for your enterprise account on {% data variables.product.prodname_dotcom_the_website %}. For privacy reasons, your consumed license report only includes the email address associated with a personal account on {% data variables.product.prodname_dotcom_the_website %} if the address is hosted by a verified domain. If one person is erroneously consuming multiple licenses, having access to the email address that is being used for deduplication makes troubleshooting much easier. For more information. see "[Verifying or approving a domain for your enterprise](/enterprise-cloud@latest/admin/configuration/configuring-your-enterprise/verifying-or-approving-a-domain-for-your-enterprise)" and "[About {% data variables.product.prodname_emus %}](/enterprise-cloud@latest/admin/identity-and-access-management/managing-iam-with-enterprise-managed-users/about-enterprise-managed-users){% ifversion not ghec %}" in the {% data variables.product.prodname_ghe_cloud %} documentation.{% else %}."{% endif %} + +{% endnote %} \ No newline at end of file diff --git a/data/reusables/enterprise-licensing/view-consumed-licenses.md b/data/reusables/enterprise-licensing/view-consumed-licenses.md new file mode 100644 index 0000000000..79e443ac95 --- /dev/null +++ b/data/reusables/enterprise-licensing/view-consumed-licenses.md @@ -0,0 +1 @@ +After you synchronize license usage, you can see a report of consumed licenses across all your environments in the enterprise settings on {% data variables.product.prodname_dotcom_the_website %}. For more information, see "[Viewing license usage for {% data variables.product.prodname_enterprise %}](/enterprise-cloud@latest/billing/managing-your-license-for-github-enterprise/viewing-license-usage-for-github-enterprise)." diff --git a/lib/get-redirect.js b/lib/get-redirect.js index d9c5b5ec81..52810ac88d 100644 --- a/lib/get-redirect.js +++ b/lib/get-redirect.js @@ -9,9 +9,9 @@ const nonEnterpriseDefaultVersionPrefix = `/${nonEnterpriseDefaultVersion}` // Return the new URI if there is one, otherwise return undefined. export default function getRedirect(uri, context) { - const { redirects, pages } = context + const { redirects, userLanguage } = context - let language = 'en' + let language = userLanguage || 'en' let withoutLanguage = uri if (languagePrefixRegex.test(uri)) { language = uri.match(languagePrefixRegex)[1] @@ -109,12 +109,7 @@ export default function getRedirect(uri, context) { } if (basicCorrection) { - return ( - getRedirect(basicCorrection, { - redirects, - pages, - }) || basicCorrection - ) + return getRedirect(basicCorrection, context) || basicCorrection } if (withoutLanguage.startsWith('/admin/')) { diff --git a/lib/page-data.js b/lib/page-data.js index b1fa1c7ad1..6b471df1a9 100644 --- a/lib/page-data.js +++ b/lib/page-data.js @@ -30,11 +30,21 @@ const TRANSLATION_DRIFT_EXCEPTIONS = [ * first since it's the most expensive work. This gets us a nested object with pages attached that we can use * as the basis for the siteTree after we do some versioning. We can also use it to derive the pageList. */ -export async function loadUnversionedTree() { +export async function loadUnversionedTree(languagesOnly = null) { + if (languagesOnly && !Array.isArray(languagesOnly)) { + throw new Error("'languagesOnly' has to be an array") + } const unversionedTree = {} + const languagesValues = Object.entries(languages) + .filter(([language]) => { + return !languagesOnly || languagesOnly.includes(language) + }) + .map(([, data]) => { + return data + }) await Promise.all( - Object.values(languages).map(async (langObj) => { + languagesValues.map(async (langObj) => { const localizedContentPath = path.posix.join(__dirname, '..', langObj.dir, 'content') unversionedTree[langObj.code] = await createTree(localizedContentPath, langObj) }) @@ -129,7 +139,10 @@ export async function versionPages(obj, version, langCode, site) { // Derive a flat array of Page objects in all languages. export async function loadPageList(unversionedTree, languagesOnly = null) { - const rawTree = unversionedTree || (await loadUnversionedTree()) + if (languagesOnly && !Array.isArray(languagesOnly)) { + throw new Error("'languagesOnly' has to be an array") + } + const rawTree = unversionedTree || (await loadUnversionedTree(languagesOnly)) const pageList = [] await Promise.all( diff --git a/middleware/detect-language.js b/middleware/detect-language.js index 1d222ede5b..9472e8d36c 100644 --- a/middleware/detect-language.js +++ b/middleware/detect-language.js @@ -1,14 +1,17 @@ -import libLanguages from '../lib/languages.js' +import languages, { languageKeys } from '../lib/languages.js' import parser from 'accept-language-parser' -const languageCodes = Object.keys(libLanguages) const chineseRegions = ['CN', 'HK'] +// This value is replicated in two places! See component. +// Note, the only reason this is exported is to benefit the tests. +export const PREFERRED_LOCALE_COOKIE_NAME = 'preferredlang' + function translationExists(language) { if (language.code === 'zh') { return chineseRegions.includes(language.region) } - return languageCodes.includes(language.code) + return languageKeys.includes(language.code) } function getLanguageCode(language) { @@ -17,33 +20,41 @@ function getLanguageCode(language) { function getUserLanguage(browserLanguages) { try { - let userLanguage = getLanguageCode(browserLanguages[0]) let numTopPreferences = 1 for (let lang = 0; lang < browserLanguages.length; lang++) { // If language has multiple regions, Chrome adds the non-region language to list if (lang > 0 && browserLanguages[lang].code !== browserLanguages[lang - 1].code) numTopPreferences++ if (translationExists(browserLanguages[lang]) && numTopPreferences < 3) { - userLanguage = getLanguageCode(browserLanguages[lang]) - break + return getLanguageCode(browserLanguages[lang]) } } - return userLanguage } catch { return undefined } } +function getUserLanguageFromCookie(req) { + const value = req.cookies[PREFERRED_LOCALE_COOKIE_NAME] + // But if it's a WIP language, reject it. + if (value && languages[value] && !languages[value].wip) { + return value + } +} + // determine language code from a path. Default to en if no valid match export function getLanguageCodeFromPath(path) { const maybeLanguage = (path.split('/')[path.startsWith('/_next/data/') ? 4 : 1] || '').slice(0, 2) - return languageCodes.includes(maybeLanguage) ? maybeLanguage : 'en' + return languageKeys.includes(maybeLanguage) ? maybeLanguage : 'en' } export default function detectLanguage(req, res, next) { req.language = getLanguageCodeFromPath(req.path) // Detecting browser language by user preference - const browserLanguages = parser.parse(req.headers['accept-language']) - req.userLanguage = getUserLanguage(browserLanguages) + req.userLanguage = getUserLanguageFromCookie(req) + if (!req.userLanguage) { + const browserLanguages = parser.parse(req.headers['accept-language']) + req.userLanguage = getUserLanguage(browserLanguages) + } return next() } diff --git a/middleware/redirects/handle-redirects.js b/middleware/redirects/handle-redirects.js index d799f161a5..22f81c9705 100644 --- a/middleware/redirects/handle-redirects.js +++ b/middleware/redirects/handle-redirects.js @@ -1,6 +1,6 @@ import patterns from '../../lib/patterns.js' import { URL } from 'url' -import languages, { pathLanguagePrefixed } from '../../lib/languages.js' +import { pathLanguagePrefixed } from '../../lib/languages.js' import getRedirect from '../../lib/get-redirect.js' import { cacheControlFactory } from '../cache-control.js' @@ -13,16 +13,7 @@ export default function handleRedirects(req, res, next) { // blanket redirects for languageless homepage if (req.path === '/') { - let language = 'en' - - // if set, redirect to user's preferred language translation or else English - if ( - req.context.userLanguage && - languages[req.context.userLanguage] && - !languages[req.context.userLanguage].wip - ) { - language = req.context.userLanguage - } + const language = getLanguage(req) // Undo the cookie setting that CSRF sets. res.removeHeader('set-cookie') @@ -70,17 +61,12 @@ export default function handleRedirects(req, res, next) { // needs to become `/en/authentication/connecting-to-github-with-ssh` const possibleRedirectTo = `/en${req.path}` if (possibleRedirectTo in req.context.pages) { - // As of Jan 2022 we always redirect to `/en` if the URL doesn't - // specify a language. ...except for the root home page (`/`). - // It's unfortunate but that's how it currently works. - // It's tracked in #1145 - // Perhaps a more ideal solution would be to do something similar to - // the code above for `req.path === '/'` where we look at the user - // agent for a header and/or cookie. + const language = getLanguage(req) + // Note, it's important to use `req.url` here and not `req.path` // because the full URL can contain query strings. // E.g. `/foo?json=breadcrumbs` - redirect = `/en${req.url}` + redirect = `/${language}${req.url}` } } @@ -112,6 +98,13 @@ export default function handleRedirects(req, res, next) { return res.redirect(permanent ? 301 : 302, redirect) } +function getLanguage(req, default_ = 'en') { + // req.context.userLanguage, if it truthy, is always a valid supported + // language. It's whatever was in the user's request but filtered + // based on non-WIP languages in lib/languages.js + return req.context.userLanguage || default_ +} + function usePermanentRedirect(req) { // If the redirect was to essentially swap `enterprise-server@latest` // for `enterprise-server@3.x` then, we definitely don't want to diff --git a/tests/rendering/pages-with-learning-tracks.js b/tests/rendering/pages-with-learning-tracks.js index 9311402022..17a7311804 100644 --- a/tests/rendering/pages-with-learning-tracks.js +++ b/tests/rendering/pages-with-learning-tracks.js @@ -1,21 +1,16 @@ -import { jest, beforeAll, expect } from '@jest/globals' +import { jest, expect } from '@jest/globals' import { getDOM } from '../helpers/supertest.js' import { loadPages } from '../../lib/page-data.js' describe('process learning tracks', () => { - let pageList - // Because calling `loadPages` will trigger a warmup, this can potentially // be very slow in CI. So we need a timeout. jest.setTimeout(60 * 1000) - beforeAll(async () => { - // Only doing English because they're the only files we do PRs for. - pageList = (await loadPages()).filter((page) => page.languageCode === 'en') - }) - test('pages with learningTracks ', async () => { + const pageList = await loadPages(undefined, ['en']) + for (const page of pageList) { if (page.learningTracks && page.learningTracks.length > 0) { for (const permalink of page.permalinks) { diff --git a/tests/routing/redirects.js b/tests/routing/redirects.js index 27dd42623b..fdc9340f04 100644 --- a/tests/routing/redirects.js +++ b/tests/routing/redirects.js @@ -2,12 +2,14 @@ import { fileURLToPath } from 'url' import path from 'path' import { isPlainObject } from 'lodash-es' import supertest from 'supertest' +import { jest } from '@jest/globals' + import createApp from '../../lib/app.js' import enterpriseServerReleases from '../../lib/enterprise-server-releases.js' import Page from '../../lib/page.js' import { get } from '../helpers/supertest.js' import versionSatisfiesRange from '../../lib/version-satisfies-range.js' -import { jest } from '@jest/globals' +import { PREFERRED_LOCALE_COOKIE_NAME } from '../../middleware/detect-language.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -132,6 +134,28 @@ describe('redirects', () => { expect(res.headers.location).toBe('/ja') expect(res.headers['cache-control']).toBe('private, no-store') }) + test('homepage redirects to preferred language by cookie', async () => { + const res = await get('/', { + headers: { + Cookie: `${PREFERRED_LOCALE_COOKIE_NAME}=ja`, + 'Accept-Language': 'es', // note how this is going to be ignored + }, + }) + expect(res.statusCode).toBe(302) + expect(res.headers.location).toBe('/ja') + expect(res.headers['cache-control']).toBe('private, no-store') + }) + test('homepage redirects to preferred language by cookie if valid', async () => { + const res = await get('/', { + headers: { + Cookie: `${PREFERRED_LOCALE_COOKIE_NAME}=xy`, + 'Accept-Language': 'ja', // note how this is going to be ignored + }, + }) + expect(res.statusCode).toBe(302) + expect(res.headers.location).toBe('/ja') + expect(res.headers['cache-control']).toBe('private, no-store') + }) }) describe('external redirects', () => { @@ -149,13 +173,63 @@ describe('redirects', () => { }) describe('localized redirects', () => { + const redirectFrom = + '/desktop/contributing-to-projects/changing-a-remote-s-url-from-github-desktop' + const redirectTo = + '/desktop/contributing-and-collaborating-using-github-desktop/working-with-your-remote-repository-on-github-or-github-enterprise/changing-a-remotes-url-from-github-desktop' + test('redirect_from for renamed pages', async () => { - const { res } = await get( - '/ja/desktop/contributing-to-projects/changing-a-remote-s-url-from-github-desktop' - ) + const { res } = await get(`/ja${redirectFrom}`) expect(res.statusCode).toBe(301) - const expected = - '/ja/desktop/contributing-and-collaborating-using-github-desktop/working-with-your-remote-repository-on-github-or-github-enterprise/changing-a-remotes-url-from-github-desktop' + const expected = `/ja${redirectTo}` + expect(res.headers.location).toBe(expected) + }) + + test('redirect_from for renamed pages by Accept-Language header', async () => { + const { res } = await get(redirectFrom, { + headers: { + 'Accept-Language': 'ja', + }, + }) + expect(res.statusCode).toBe(302) + const expected = `/ja${redirectTo}` + expect(res.headers.location).toBe(expected) + }) + + test('redirect_from for renamed pages but ignore Accept-Language header if not recognized', async () => { + const { res } = await get(redirectFrom, { + headers: { + // None of these are recognized + 'Accept-Language': 'sv,fr,gr', + }, + }) + expect(res.statusCode).toBe(302) + const expected = `/en${redirectTo}` + expect(res.headers.location).toBe(expected) + }) + + test('redirect_from for renamed pages but ignore unrecognized Accept-Language header values', async () => { + const { res } = await get(redirectFrom, { + headers: { + // Only the last one is recognized + 'Accept-Language': 'sv,ja', + }, + }) + expect(res.statusCode).toBe(302) + const expected = `/ja${redirectTo}` + expect(res.headers.location).toBe(expected) + }) + + test('will inject the preferred language from cookie', async () => { + const { res } = await get(redirectFrom, { + headers: { + Cookie: `${PREFERRED_LOCALE_COOKIE_NAME}=ja`, + 'Accept-Language': 'es', // note how this is going to be ignored + }, + }) + // 302 because the redirect depended on cookie + expect(res.statusCode).toBe(302) + const expected = `/ja${redirectTo}` expect(res.headers.location).toBe(expected) }) }) diff --git a/tests/unit/get-redirect.js b/tests/unit/get-redirect.js index 5ad969f6ed..ad425e0be3 100644 --- a/tests/unit/get-redirect.js +++ b/tests/unit/get-redirect.js @@ -148,4 +148,18 @@ describe('getRedirect basics', () => { // it already has the enterprise-server prefix. expect(getRedirect('/enterprise-server/foo', ctx)).toBe(`/en/enterprise-server@${latest}/bar`) }) + + it('should redirect according to the req.context.userLanguage', () => { + const ctx = { + pages: {}, + redirects: { + '/foo': '/bar', + }, + userLanguage: 'ja', + } + expect(getRedirect('/foo', ctx)).toBe(`/ja/bar`) + // falls back to 'en' if it's falsy + ctx.userLanguage = null + expect(getRedirect('/foo', ctx)).toBe(`/en/bar`) + }) })