Skip to content

Commit faeb450

Browse files
authored
Merge pull request github#16055 from github/repo-sync
repo sync
2 parents 66e31e5 + 3fcc644 commit faeb450

File tree

13 files changed

+205
-61
lines changed

13 files changed

+205
-61
lines changed

components/page-header/LanguagePicker.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { useRouter } from 'next/router'
2+
import Cookies from 'js-cookie'
3+
24
import { Link } from 'components/Link'
35
import { useLanguages } from 'components/context/LanguagesContext'
46
import { Picker } from 'components/ui/Picker'
57
import { useTranslation } from 'components/hooks/useTranslation'
68

9+
// This value is replicated in two places! See middleware/detect-language.js
10+
const PREFERRED_LOCALE_COOKIE_NAME = 'preferredlang'
11+
712
type Props = {
813
variant?: 'inline'
914
}
@@ -22,6 +27,22 @@ export const LanguagePicker = ({ variant }: Props) => {
2227
// in a "denormalized" way.
2328
const routerPath = router.asPath.split('#')[0]
2429

30+
function rememberPreferredLanguage(code: string) {
31+
try {
32+
Cookies.set(PREFERRED_LOCALE_COOKIE_NAME, code, {
33+
expires: 365,
34+
secure: document.location.protocol !== 'http:',
35+
})
36+
} catch (err) {
37+
// You can never be too careful because setting a cookie
38+
// can fail. For example, some browser
39+
// extensions disallow all setting of cookies and attempts
40+
// at the `document.cookie` setter could throw. Just swallow
41+
// and move on.
42+
console.warn('Unable to set preferred language cookie', err)
43+
}
44+
}
45+
2546
return (
2647
<Picker
2748
variant={variant}
@@ -33,7 +54,13 @@ export const LanguagePicker = ({ variant }: Props) => {
3354
text: lang.nativeName || lang.name,
3455
selected: lang === selectedLang,
3556
item: (
36-
<Link href={routerPath} locale={lang.code}>
57+
<Link
58+
href={routerPath}
59+
locale={lang.code}
60+
onClick={() => {
61+
rememberPreferredLanguage(lang.code)
62+
}}
63+
>
3764
{lang.nativeName ? (
3865
<>
3966
<span lang={lang.code}>{lang.nativeName}</span> (

content/admin/configuration/configuring-github-connect/enabling-automatic-user-license-sync-for-your-enterprise.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Enabling automatic user license sync for your enterprise
3-
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 %}.'
3+
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 %}.'
44
redirect_from:
55
- /enterprise/admin/installation/enabling-automatic-user-license-sync-between-github-enterprise-server-and-github-enterprise-cloud
66
- /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
1919
---
2020
## About license synchronization
2121

22-
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)."
22+
{% 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)."
23+
24+
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.
25+
26+
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 %}.
27+
28+
{% data reusables.enterprise-licensing.view-consumed-licenses %}
2329

2430
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)."
2531

32+
{% data reusables.enterprise-licensing.verified-domains-license-sync %}
33+
2634
## Enabling license synchronization
2735

2836
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)."

content/billing/managing-your-license-for-github-enterprise/syncing-license-usage-between-github-enterprise-server-and-github-enterprise-cloud.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ shortTitle: Sync license usage
1616

1717
{% data reusables.enterprise-licensing.about-license-sync %}
1818

19-
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 %}.
19+
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 %}
2020

21-
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)."
21+
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 %}.
22+
23+
{% data reusables.enterprise-licensing.view-consumed-licenses %}
24+
25+
{% data reusables.enterprise-licensing.verified-domains-license-sync %}
2226

2327
## Automatically syncing license usage
2428

25-
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 %}
29+
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 %}
2630

2731
## Manually syncing license usage
2832

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
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.
1+
{% 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.
2+
3+
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 %}
4+
5+
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 %}.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{% note %}
2+
3+
**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 %}
4+
5+
{% endnote %}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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)."

lib/get-redirect.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ const nonEnterpriseDefaultVersionPrefix = `/${nonEnterpriseDefaultVersion}`
99

1010
// Return the new URI if there is one, otherwise return undefined.
1111
export default function getRedirect(uri, context) {
12-
const { redirects, pages } = context
12+
const { redirects, userLanguage } = context
1313

14-
let language = 'en'
14+
let language = userLanguage || 'en'
1515
let withoutLanguage = uri
1616
if (languagePrefixRegex.test(uri)) {
1717
language = uri.match(languagePrefixRegex)[1]
@@ -109,12 +109,7 @@ export default function getRedirect(uri, context) {
109109
}
110110

111111
if (basicCorrection) {
112-
return (
113-
getRedirect(basicCorrection, {
114-
redirects,
115-
pages,
116-
}) || basicCorrection
117-
)
112+
return getRedirect(basicCorrection, context) || basicCorrection
118113
}
119114

120115
if (withoutLanguage.startsWith('/admin/')) {

lib/page-data.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,21 @@ const TRANSLATION_DRIFT_EXCEPTIONS = [
3030
* first since it's the most expensive work. This gets us a nested object with pages attached that we can use
3131
* as the basis for the siteTree after we do some versioning. We can also use it to derive the pageList.
3232
*/
33-
export async function loadUnversionedTree() {
33+
export async function loadUnversionedTree(languagesOnly = null) {
34+
if (languagesOnly && !Array.isArray(languagesOnly)) {
35+
throw new Error("'languagesOnly' has to be an array")
36+
}
3437
const unversionedTree = {}
3538

39+
const languagesValues = Object.entries(languages)
40+
.filter(([language]) => {
41+
return !languagesOnly || languagesOnly.includes(language)
42+
})
43+
.map(([, data]) => {
44+
return data
45+
})
3646
await Promise.all(
37-
Object.values(languages).map(async (langObj) => {
47+
languagesValues.map(async (langObj) => {
3848
const localizedContentPath = path.posix.join(__dirname, '..', langObj.dir, 'content')
3949
unversionedTree[langObj.code] = await createTree(localizedContentPath, langObj)
4050
})
@@ -129,7 +139,10 @@ export async function versionPages(obj, version, langCode, site) {
129139

130140
// Derive a flat array of Page objects in all languages.
131141
export async function loadPageList(unversionedTree, languagesOnly = null) {
132-
const rawTree = unversionedTree || (await loadUnversionedTree())
142+
if (languagesOnly && !Array.isArray(languagesOnly)) {
143+
throw new Error("'languagesOnly' has to be an array")
144+
}
145+
const rawTree = unversionedTree || (await loadUnversionedTree(languagesOnly))
133146
const pageList = []
134147

135148
await Promise.all(

middleware/detect-language.js

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
import libLanguages from '../lib/languages.js'
1+
import languages, { languageKeys } from '../lib/languages.js'
22
import parser from 'accept-language-parser'
3-
const languageCodes = Object.keys(libLanguages)
43

54
const chineseRegions = ['CN', 'HK']
65

6+
// This value is replicated in two places! See <LanguagePicker/> component.
7+
// Note, the only reason this is exported is to benefit the tests.
8+
export const PREFERRED_LOCALE_COOKIE_NAME = 'preferredlang'
9+
710
function translationExists(language) {
811
if (language.code === 'zh') {
912
return chineseRegions.includes(language.region)
1013
}
11-
return languageCodes.includes(language.code)
14+
return languageKeys.includes(language.code)
1215
}
1316

1417
function getLanguageCode(language) {
@@ -17,33 +20,41 @@ function getLanguageCode(language) {
1720

1821
function getUserLanguage(browserLanguages) {
1922
try {
20-
let userLanguage = getLanguageCode(browserLanguages[0])
2123
let numTopPreferences = 1
2224
for (let lang = 0; lang < browserLanguages.length; lang++) {
2325
// If language has multiple regions, Chrome adds the non-region language to list
2426
if (lang > 0 && browserLanguages[lang].code !== browserLanguages[lang - 1].code)
2527
numTopPreferences++
2628
if (translationExists(browserLanguages[lang]) && numTopPreferences < 3) {
27-
userLanguage = getLanguageCode(browserLanguages[lang])
28-
break
29+
return getLanguageCode(browserLanguages[lang])
2930
}
3031
}
31-
return userLanguage
3232
} catch {
3333
return undefined
3434
}
3535
}
3636

37+
function getUserLanguageFromCookie(req) {
38+
const value = req.cookies[PREFERRED_LOCALE_COOKIE_NAME]
39+
// But if it's a WIP language, reject it.
40+
if (value && languages[value] && !languages[value].wip) {
41+
return value
42+
}
43+
}
44+
3745
// determine language code from a path. Default to en if no valid match
3846
export function getLanguageCodeFromPath(path) {
3947
const maybeLanguage = (path.split('/')[path.startsWith('/_next/data/') ? 4 : 1] || '').slice(0, 2)
40-
return languageCodes.includes(maybeLanguage) ? maybeLanguage : 'en'
48+
return languageKeys.includes(maybeLanguage) ? maybeLanguage : 'en'
4149
}
4250

4351
export default function detectLanguage(req, res, next) {
4452
req.language = getLanguageCodeFromPath(req.path)
4553
// Detecting browser language by user preference
46-
const browserLanguages = parser.parse(req.headers['accept-language'])
47-
req.userLanguage = getUserLanguage(browserLanguages)
54+
req.userLanguage = getUserLanguageFromCookie(req)
55+
if (!req.userLanguage) {
56+
const browserLanguages = parser.parse(req.headers['accept-language'])
57+
req.userLanguage = getUserLanguage(browserLanguages)
58+
}
4859
return next()
4960
}

middleware/redirects/handle-redirects.js

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import patterns from '../../lib/patterns.js'
22
import { URL } from 'url'
3-
import languages, { pathLanguagePrefixed } from '../../lib/languages.js'
3+
import { pathLanguagePrefixed } from '../../lib/languages.js'
44
import getRedirect from '../../lib/get-redirect.js'
55
import { cacheControlFactory } from '../cache-control.js'
66

@@ -13,16 +13,7 @@ export default function handleRedirects(req, res, next) {
1313

1414
// blanket redirects for languageless homepage
1515
if (req.path === '/') {
16-
let language = 'en'
17-
18-
// if set, redirect to user's preferred language translation or else English
19-
if (
20-
req.context.userLanguage &&
21-
languages[req.context.userLanguage] &&
22-
!languages[req.context.userLanguage].wip
23-
) {
24-
language = req.context.userLanguage
25-
}
16+
const language = getLanguage(req)
2617

2718
// Undo the cookie setting that CSRF sets.
2819
res.removeHeader('set-cookie')
@@ -70,17 +61,12 @@ export default function handleRedirects(req, res, next) {
7061
// needs to become `/en/authentication/connecting-to-github-with-ssh`
7162
const possibleRedirectTo = `/en${req.path}`
7263
if (possibleRedirectTo in req.context.pages) {
73-
// As of Jan 2022 we always redirect to `/en` if the URL doesn't
74-
// specify a language. ...except for the root home page (`/`).
75-
// It's unfortunate but that's how it currently works.
76-
// It's tracked in #1145
77-
// Perhaps a more ideal solution would be to do something similar to
78-
// the code above for `req.path === '/'` where we look at the user
79-
// agent for a header and/or cookie.
64+
const language = getLanguage(req)
65+
8066
// Note, it's important to use `req.url` here and not `req.path`
8167
// because the full URL can contain query strings.
8268
// E.g. `/foo?json=breadcrumbs`
83-
redirect = `/en${req.url}`
69+
redirect = `/${language}${req.url}`
8470
}
8571
}
8672

@@ -112,6 +98,13 @@ export default function handleRedirects(req, res, next) {
11298
return res.redirect(permanent ? 301 : 302, redirect)
11399
}
114100

101+
function getLanguage(req, default_ = 'en') {
102+
// req.context.userLanguage, if it truthy, is always a valid supported
103+
// language. It's whatever was in the user's request but filtered
104+
// based on non-WIP languages in lib/languages.js
105+
return req.context.userLanguage || default_
106+
}
107+
115108
function usePermanentRedirect(req) {
116109
// If the redirect was to essentially swap `enterprise-server@latest`
117110
// for `[email protected]` then, we definitely don't want to

0 commit comments

Comments
 (0)