1
0
mirror of synced 2025-12-23 11:54:18 -05:00

ALL translations strings shipped with every page (#44284)

Co-authored-by: Robert Sese <734194+rsese@users.noreply.github.com>
This commit is contained in:
Peter Bengtsson
2023-10-31 14:26:14 -04:00
committed by GitHub
parent 09714c271a
commit 297ddc1b2c
44 changed files with 500 additions and 382 deletions

View File

@@ -28,7 +28,7 @@ export const DefaultLayout = (props: Props) => {
fullUrl,
status,
} = useMainContext()
const { t } = useTranslation(['errors', 'meta', 'scroll_button'])
const { t } = useTranslation(['meta', 'scroll_button'])
const router = useRouter()
const metaDescription = page.introPlainText ? page.introPlainText : t('default_description')
const { languages } = useLanguages()

View File

@@ -55,6 +55,9 @@ export type ProductTreeNode = {
childPages: Array<ProductTreeNode>
}
type UIString = Record<string, string>
export type UIStrings = UIString | { [key: string]: UIStrings }
export type EnterpriseDeprecation = {
version_was_deprecated: string
version_will_be_deprecated: string
@@ -67,12 +70,13 @@ type DataReusables = {
}
type DataT = {
ui: Record<string, any>
ui: UIStrings
reusables: DataReusables
variables: {
release_candidate: { version: string }
}
}
type EnterpriseServerReleases = {
isOldestReleaseDeprecated: boolean
oldestSupported: string
@@ -126,6 +130,38 @@ export type MainContextT = {
fullUrl: string
}
// Write down the namespaces from `data/ui.yml` that are used on all pages,
// they will always be available and don't need to be manually added.
// Order does not matter on these.
const DEFAULT_UI_NAMESPACES = [
'header',
'search',
'survey',
'toc',
'meta',
'scroll_button',
'pages',
'picker',
'footer',
'contribution_cta',
'support',
'rest',
]
export function addUINamespaces(req: any, ui: UIStrings, namespaces: string[]) {
const pool = req.context.site.data.ui
for (const namespace of namespaces) {
if (!(namespace in pool)) {
throw new Error(
`Invalid namespace "${namespace}". It's not present in data/ui.yml as a namespace. (not one of: ${Object.keys(
pool,
)})`,
)
}
ui[namespace] = pool[namespace]
}
}
export const getMainContext = async (req: any, res: any): Promise<MainContextT> => {
// Our current translation process adds 'ms.*' frontmatter properties to files
// it translates including when data/ui.yml is translated. We don't use these
@@ -140,6 +176,9 @@ export const getMainContext = async (req: any, res: any): Promise<MainContextT>
}
const { documentType } = req.context.page
const ui: UIStrings = {}
addUINamespaces(req, ui, DEFAULT_UI_NAMESPACES)
// Every product landing page has a listing of all articles.
// It's used by the <ProductArticlesList> component.
const includeFullProductTree = documentType === 'product'
@@ -171,7 +210,7 @@ export const getMainContext = async (req: any, res: any): Promise<MainContextT>
isHomepageVersion: req.context.page?.documentType === 'homepage',
error: req.context.error ? req.context.error.toString() : '',
data: {
ui: req.context.site.data.ui,
ui,
reusables,

View File

@@ -65,8 +65,8 @@ redirect_from:
- /admin/configuration/configuring-your-enterprise/configuring-data-encryption-for-your-enterprise
introLinks:
overview: '{% ifversion ghes %}/admin/overview/about-github-enterprise-server{% elsif ghae %}/admin/overview/about-github-ae{% elsif ghec %}/admin/overview/about-github-enterprise-cloud{% endif %}'
Releases: '{% ifversion ghes %}/admin/all-releases{% endif %}'
Try Enterprise Cloud for free: '{% ifversion ghec %}https://github.com/account/enterprises/new{% endif %}'
releases: '{% ifversion ghes %}/admin/all-releases{% endif %}'
try_ghec_for_free: '{% ifversion ghec %}https://github.com/account/enterprises/new{% endif %}'
changelog:
label: enterprise
featuredLinks:
@@ -127,4 +127,3 @@ children:
- /release-notes
- /all-releases
---

View File

@@ -4,7 +4,7 @@ shortTitle: Migrations
intro: "If you're moving to {% data variables.product.prodname_dotcom %} from another code hosting platform or moving between {% data variables.product.prodname_dotcom %} products, learn how to use our migration tooling to bring your work with you."
introLinks:
overview: /migrations/overview/about-githubs-migration-tooling
Plan your migration: /migrations/overview/planning-your-migration-to-github
plan_your_migration: /migrations/overview/planning-your-migration-to-github
featuredLinks:
startHere:
- /migrations/importing-source-code/using-github-importer/about-github-importer

View File

@@ -23,17 +23,13 @@ picker:
release_notes:
banner_text: GitHub began rolling these changes out to enterprises on
search:
need_help: Need help?
placeholder: Search GitHub Docs
search_results_for: Search results for
no_content: No content
matches_found: Results found
matches_displayed: Matches displayed
search_error: An error occurred trying to perform the search.
description: Enter a search term to find it in the GitHub Docs.
placeholder: Search GitHub Docs
label: Search GitHub Docs
search_results:
search_results_for: Search results for
matches_displayed: Matches displayed
n_results: '{n} results'
one_result: 1 result
search_validation_error: Validation error with search query
homepage:
explore_by_product: Explore by product
@@ -53,11 +49,6 @@ pages:
about_versions: About versions
permissions_statement: Who can use this feature
video_from_transcript: See video for this transcript
errors:
oops: Ooops!
something_went_wrong: It looks like something went wrong.
we_track_errors: We track these errors automatically, but if the problem persists please feel free to contact us.
page_doesnt_exist: It looks like this page doesn't exist.
support:
still_need_help: Still need help?
contact_support: Contact support
@@ -97,13 +88,9 @@ parameter_table:
see_preview_notices: See preview notices
type: Type
single_enum_description: Value
products:
audit_logs:
action: Action
description: Description
button_text:
copy_to_clipboard: Copy to clipboard
copied: Copied!
graphql:
reference:
implements: <code>{{ GraphQLItemTitle }}</code> Implements
@@ -145,7 +132,7 @@ products:
additionalPermissions: Additional permissions
uat: UAT
iat: IAT
reference:
rest_reference:
in: In
description: Description
notes: Notes
@@ -175,6 +162,9 @@ products:
ghcli: GitHub CLI
javascript: JavaScript
curl: cURL
button_text:
copy_to_clipboard: Copy to clipboard
copied: Copied!
webhooks:
action_type_switch_error: There was an error switching webhook action types.
action_type: Action type
@@ -202,13 +192,11 @@ product_landing:
quickstart: Quickstart
reference: Reference
overview: Overview
try_ghec_for_free: Try Enterprise Cloud for free
plan_your_migration: Plan your migration
releases: Releases
guides: Guides
explore_guides: Explore guides
code_examples: Code examples
search_code_examples: Search code examples
search_results_for: Search results for
matches_displayed: Matches displayed
show_more: Show more
explore_people_and_projects: Explore people and projects
sorry: Sorry, there is no result for
no_example: It looks like we don't have an example that fits your filter.
@@ -228,9 +216,6 @@ product_landing:
view: View all
view_transcript: View video transcript
all_docs: 'All {{ title }} docs'
code_example:
search_button: Search
search_examples: 'Search code examples:'
product_guides:
learning_paths_title: '{{ name }} learning paths'
start_path: Start learning path
@@ -258,11 +243,6 @@ learning_track_nav:
next_guide: Next
more_guides: More guides →
current_progress: '{i} of {n} in learning path'
toggle_images:
off: Images are off, click to show
on: Images are on, click to hide
hide_single: Hide image
show_single: Show image
scroll_button:
scroll_to_top: Scroll to top
popovers:

View File

@@ -14,7 +14,7 @@ type Props = {
}
export default function GroupedEvents({ auditLogEvents, category }: Props) {
const { t } = useTranslation('products')
const { t } = useTranslation('audit_logs')
const eventSlug = slug(category)
return (
<>
@@ -24,8 +24,8 @@ export default function GroupedEvents({ auditLogEvents, category }: Props) {
<table>
<thead>
<tr>
<th scope="col">{t('audit_logs.action')}</th>
<th scope="col">{t('audit_logs.description')}</th>
<th scope="col">{t('action')}</th>
<th scope="col">{t('description')}</th>
</tr>
</thead>
<tbody>

View File

@@ -1,6 +1,11 @@
import { GetServerSideProps } from 'next'
import { getMainContext, MainContext, MainContextT } from 'components/context/MainContext'
import {
addUINamespaces,
getMainContext,
MainContext,
MainContextT,
} from 'components/context/MainContext'
import {
getAutomatedPageContextFromRequest,
AutomatedPageContext,
@@ -50,6 +55,8 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
const url = context.req.url
const mainContext = await getMainContext(req, res)
addUINamespaces(req, mainContext.data.ui, ['audit_logs'])
const { miniTocItems } = getAutomatedPageContextFromRequest(req)
let auditLogEvents = {} as Record<string, Array<AuditLogEventT>>

View File

@@ -23,7 +23,7 @@ export function ChildBodyParametersRows({
childParamsGroups,
oneOfObject = false,
}: Props) {
const { t } = useTranslation(['parameter_table', 'products'])
const { t } = useTranslation(['parameter_table'])
return (
<tr className={cx(styles.childBodyParametersRows, 'color-bg-subtle border-top-0')}>
<td className="has-nested-table">

View File

@@ -43,7 +43,7 @@ export function ParameterRow({
bodyParamExpandCallback,
clickedBodyParameterName,
}: Props) {
const { t } = useTranslation(['parameter_table', 'products'])
const { t } = useTranslation(['parameter_table'])
// This will be true if `rowParams` does not have a key called `default`
// and it will be true if it does and its actual value is `undefined`.

View File

@@ -29,7 +29,7 @@ export function ParameterTable({
clickedBodyParameterName = '',
variant = 'rest',
}: Props) {
const { t } = useTranslation(['parameter_table', 'products'])
const { t } = useTranslation(['parameter_table'])
const queryParams = parameters.filter((param) => param.in === 'query')
const pathParams = parameters.filter((param) => param.in === 'path')

View File

@@ -61,7 +61,7 @@ export function PermissionsList({
currentVersion === DEFAULT_VERSION ? `/${locale}` : `/${locale}/${currentVersion}`
// Translated strings
const { t } = useTranslation('products')
const { t } = useTranslation('rest')
const ENDPOINTS_TH = t('rest.overview.permissions.endpoints')
const ACCESS_TH = t('rest.overview.permissions.access')
const TOKENS_TH = t('rest.overview.permissions.tokens')

View File

@@ -9,8 +9,8 @@ type Props = {
}
export function Enum({ item }: Props) {
const { t } = useTranslation('products')
const heading = t('graphql.reference.values').replace('{{ GraphQLItemTitle }}', item.name)
const { t } = useTranslation('graphql')
const heading = t('reference.values').replace('{{ GraphQLItemTitle }}', item.name)
return (
<GraphqlItem item={item} heading={heading}>

View File

@@ -8,8 +8,8 @@ type Props = {
}
export function InputObject({ item }: Props) {
const { t } = useTranslation('products')
const heading = t('graphql.reference.input_fields').replace('{{ GraphQLItemTitle }}', item.name)
const { t } = useTranslation('graphql')
const heading = t('reference.input_fields').replace('{{ GraphQLItemTitle }}', item.name)
return (
<GraphqlItem item={item} heading={heading}>
<Table fields={item.inputFields} />

View File

@@ -13,9 +13,9 @@ type Props = {
export function Interface({ item, objects }: Props) {
const { locale } = useRouter()
const { t } = useTranslation('products')
const heading = t('graphql.reference.implemented_by').replace('{{ GraphQLItemTitle }}', item.name)
const heading2 = t('graphql.reference.fields').replace('{{ GraphQLItemTitle }}', item.name)
const { t } = useTranslation('graphql')
const heading = t('reference.implemented_by').replace('{{ GraphQLItemTitle }}', item.name)
const heading2 = t('reference.fields').replace('{{ GraphQLItemTitle }}', item.name)
const implementedBy = objects.filter(
(object) =>

View File

@@ -14,9 +14,9 @@ type Props = {
export function Mutation({ item }: Props) {
const { locale } = useRouter()
const { t } = useTranslation('products')
const heading = t('graphql.reference.input_fields').replace('{{ GraphQLItemTitle }}', item.name)
const heading2 = t('graphql.reference.return_fields').replace('{{ GraphQLItemTitle }}', item.name)
const { t } = useTranslation('graphql')
const heading = t('reference.input_fields').replace('{{ GraphQLItemTitle }}', item.name)
const heading2 = t('reference.return_fields').replace('{{ GraphQLItemTitle }}', item.name)
return (
<GraphqlItem item={item} heading={heading}>

View File

@@ -12,11 +12,9 @@ type Props = {
export function Notice({ item, variant = 'preview' }: Props) {
const { locale } = useRouter()
const { t } = useTranslation('products')
const { t } = useTranslation('graphql')
const previewTitle =
variant === 'preview'
? t('graphql.reference.preview_notice')
: t('graphql.reference.deprecation_notice')
variant === 'preview' ? t('reference.preview_notice') : t('reference.deprecation_notice')
const noticeStyle =
variant === 'preview'
? 'ghd-spotlight-note color-border-accent-emphasis color-bg-accent'
@@ -32,7 +30,7 @@ export function Notice({ item, variant = 'preview' }: Props) {
<Link href={item.preview.href} locale={locale}>
{item.preview.title}
</Link>
. {t('graphql.reference.preview_period')}
. {t('reference.preview_period')}
</p>
) : item.deprecationReason ? (
<div>

View File

@@ -12,9 +12,9 @@ type Props = {
export function Object({ item }: Props) {
const { locale } = useRouter()
const { t } = useTranslation('products')
const heading1 = t('graphql.reference.implements').replace('{{ GraphQLItemTitle }}', item.name)
const heading2 = t('graphql.reference.fields').replace('{{ GraphQLItemTitle }}', item.name)
const { t } = useTranslation('graphql')
const heading1 = t('reference.implements').replace('{{ GraphQLItemTitle }}', item.name)
const heading2 = t('reference.fields').replace('{{ GraphQLItemTitle }}', item.name)
return (
<GraphqlItem item={item}>

View File

@@ -15,7 +15,7 @@ export function Previews({ schema }: Props) {
const previews = schema.map((item) => {
const slugger = new GithubSlugger()
const slug = slugger.slug(item.title)
const { t } = useTranslation('products')
const { t } = useTranslation('graphql')
return (
<div className={cx(styles.markdownBody)} key={slug}>
@@ -23,11 +23,11 @@ export function Previews({ schema }: Props) {
{item.title}
</HeadingLink>
<p>{item.description}</p>
<p>{t('graphql.overview.preview_header')}</p>
<p>{t('overview.preview_header')}</p>
<pre>
<code>{item.accept_header}</code>
</pre>
<p>{t('graphql.overview.preview_schema_members')}:</p>
<p>{t('overview.preview_schema_members')}:</p>
<ul>
{item.toggled_on.map((change) => (
<li key={change + slug}>

View File

@@ -12,7 +12,7 @@ type Props = {
export function Query({ item }: Props) {
const { locale } = useRouter()
const { t } = useTranslation('products')
const { t } = useTranslation('graphql')
return (
<GraphqlItem item={item} headingLevel={3}>
@@ -30,10 +30,7 @@ export function Query({ item }: Props) {
<>
<h4
dangerouslySetInnerHTML={{
__html: t('graphql.reference.arguments').replace(
'{{ GraphQLItemTitle }}',
item.name,
),
__html: t('reference.arguments').replace('{{ GraphQLItemTitle }}', item.name),
}}
/>
<Table fields={item.args} />

View File

@@ -12,9 +12,9 @@ type Props = {
export function Table({ fields }: Props) {
const { locale } = useRouter()
const { t } = useTranslation('products')
const tableName = t('graphql.reference.name')
const tableDescription = t('graphql.reference.description')
const { t } = useTranslation('graphql')
const tableName = t('reference.name')
const tableDescription = t('reference.description')
return (
<table className="fields width-full table-fixed">
@@ -65,7 +65,7 @@ export function Table({ fields }: Props) {
<p
className="pt-0 mt-0 h5"
dangerouslySetInnerHTML={{
__html: t('graphql.reference.arguments').replace(
__html: t('reference.arguments').replace(
'{{ GraphQLItemTitle }}',
field.name,
),

View File

@@ -11,8 +11,8 @@ type Props = {
export function Union({ item }: Props) {
const { locale } = useRouter()
const { t } = useTranslation('products')
const heading = t('graphql.reference.possible_types').replace('{{ GraphQLItemTitle }}', item.name)
const { t } = useTranslation('graphql')
const heading = t('reference.possible_types').replace('{{ GraphQLItemTitle }}', item.name)
return (
<GraphqlItem item={item} heading={heading}>

View File

@@ -2,7 +2,12 @@ import { GetServerSideProps } from 'next'
import React from 'react'
import { GraphqlPage } from 'src/graphql/components/GraphqlPage'
import { MainContextT, MainContext, getMainContext } from 'components/context/MainContext'
import {
MainContextT,
MainContext,
getMainContext,
addUINamespaces,
} from 'components/context/MainContext'
import type { ObjectT, GraphqlT } from 'src/graphql/components/types'
import { AutomatedPage } from 'src/automated-pipelines/components/AutomatedPage'
import {
@@ -65,9 +70,12 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
// Update the existing context to include the miniTocItems from GraphQL
automatedPageContext.miniTocItems.push(...graphqlMiniTocItems)
const mainContext = await getMainContext(req, res)
addUINamespaces(req, mainContext.data.ui, ['graphql'])
return {
props: {
mainContext: await getMainContext(req, res),
mainContext,
automatedPageContext,
schema,
language,

View File

@@ -1,7 +1,12 @@
import { GetServerSideProps } from 'next'
import React from 'react'
import { MainContextT, MainContext, getMainContext } from 'components/context/MainContext'
import {
MainContextT,
MainContext,
getMainContext,
addUINamespaces,
} from 'components/context/MainContext'
import { AutomatedPage } from 'src/automated-pipelines/components/AutomatedPage'
import {
AutomatedPageContext,
@@ -47,9 +52,12 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
// Update the existing context to include the miniTocItems from GraphQL
automatedPageContext.miniTocItems.push(...changelogMiniTocItems)
const mainContext = await getMainContext(req, res)
addUINamespaces(req, mainContext.data.ui, ['graphql'])
return {
props: {
mainContext: await getMainContext(req, res),
mainContext,
automatedPageContext,
schema,
},

View File

@@ -6,8 +6,8 @@ import { ArticleCard } from './ArticleCard'
import { ItemInput } from '@primer/react/lib/deprecated/ActionList/List'
export const ArticleCards = () => {
const { t } = useTranslation('product_guides')
const guideTypes: Record<string, string> = t('guide_types')
const { t, tObject } = useTranslation('product_guides')
const guideTypes = tObject('guide_types')
const { allTopics, includeGuides } = useProductGuidesContext()
const articleCardRef = useRef<HTMLUListElement>(null)
@@ -33,7 +33,7 @@ export const ArticleCards = () => {
? t('guides_found.none')
: guides.length === 1
? t('guides_found.one')
: t('guides_found.multiple').replace('{n}', guides.length)}
: t('guides_found.multiple').replace('{n}', `${guides.length}`)}
</div>
</div>
@@ -48,7 +48,7 @@ export const ArticleCards = () => {
tabIndex={-1}
key={card.href + i}
card={card}
typeLabel={guideTypes[card.type]}
typeLabel={guideTypes[card.type] as string}
/>
)
})}

View File

@@ -54,7 +54,7 @@ export const LandingHero = () => {
href={link}
className={cx('btn btn-large f4 mt-3 mr-3 ', i === 0 && 'btn-primary')}
>
{t(key) || key}
{t(key)}
</FullLink>
)
})}

View File

@@ -1,7 +1,12 @@
import React from 'react'
import type { GetServerSideProps } from 'next'
import { MainContextT, MainContext, getMainContext } from 'components/context/MainContext'
import {
MainContextT,
MainContext,
getMainContext,
addUINamespaces,
} from 'components/context/MainContext'
import { DefaultLayout } from 'components/DefaultLayout'
import { useTranslation } from 'src/languages/components/useTranslation'
@@ -59,11 +64,11 @@ function HomePage(props: HomePageProps) {
<div className="container-xl">
<div className="gutter gutter-xl-spacious clearfix">
<div className="col-12 col-lg-6 mb-md-4 mb-lg-0 float-left">
<ArticleList title={t('toc:getting_started')} articles={gettingStartedLinks} />
<ArticleList title={t('getting_started')} articles={gettingStartedLinks} />
</div>
<div className="col-12 col-lg-6 float-left">
<ArticleList title={t('toc:popular')} articles={popularLinks} />
<ArticleList title={t('popular')} articles={popularLinks} />
</div>
</div>
</div>
@@ -76,9 +81,12 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
const req = context.req as any
const res = context.res as any
const mainContext = await getMainContext(req, res)
addUINamespaces(req, mainContext.data.ui, ['homepage', 'product_landing'])
return {
props: {
mainContext: await getMainContext(req, res),
mainContext,
productGroups: req.context.productGroups,
gettingStartedLinks: req.context.featuredLinks.gettingStarted.map(
({ title, href, intro }: any) => ({ title, href, intro }),

View File

@@ -7,7 +7,12 @@ import copyCode from 'components/lib/copy-code'
import toggleAnnotation from 'components/lib/toggle-annotations'
import wrapCodeTerms from 'components/lib/wrap-code-terms'
import { MainContextT, MainContext, getMainContext } from 'components/context/MainContext'
import {
MainContextT,
MainContext,
getMainContext,
addUINamespaces,
} from 'components/context/MainContext'
import {
getProductLandingContextFromRequest,
@@ -111,16 +116,28 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
}
const { currentLayoutName, relativePath } = props.mainContext
const additionalUINamespaces: string[] = []
// This looks a little funky, but it's so we only send one context's data to the client
if (currentLayoutName === 'product-landing') {
props.productLandingContext = await getProductLandingContextFromRequest(req)
additionalUINamespaces.push('product_landing')
} else if (currentLayoutName === 'product-guides') {
props.productGuidesContext = getProductGuidesContextFromRequest(req)
additionalUINamespaces.push('product_guides')
} else if (relativePath?.endsWith('index.md')) {
props.tocLandingContext = getTocLandingContextFromRequest(req)
} else {
// All articles that might have hover cards needs this
additionalUINamespaces.push('popovers')
props.articleContext = getArticleContextFromRequest(req)
if (props.articleContext.currentLearningTrack?.trackName) {
additionalUINamespaces.push('learning_track_nav')
}
}
addUINamespaces(req, props.mainContext.data.ui, additionalUINamespaces)
return {
props,

View File

@@ -1,41 +1,113 @@
import type { UIStrings } from 'components/context/MainContext'
import { useMainContext } from 'components/context/MainContext'
import get from 'lodash/get'
// The idea of this component is to mimic a popular i18n library (i18next)
// so that we can set ourselves up to transition to it (or a similar library) in the future
class TranslationNamespaceError extends Error {}
class UngettableError extends Error {}
// Used to pull translation UI strings from the page props into
// React components. When you instantiate the hook you can pass
// the name or names of the namespaces you want to use. Then, when
// refererring to specific keys you don't have to say the namespace
// (or which of the namespaces) you refer to.
// Example use:
//
// const { t, tObject } = useTranslation(['football', 'quiz'])
//
// return <div>
// <b>{t('select')}</b> {/* shorthand now for 'football.select' */}
// <select>
// {['yes', 'no', 'maybe'].map((answer) => (
// <option key={answer} value={answer}>{tObject('choices')[answer]}</option>
// )}
// </select>
// </div>
//
// You can also use the TemplateStringArray version like:
//
// <b>{t`select`)}</b>
//
// Any namespace used when you initialize `useTranslation` that isn't
// recognized (prepared in the page props) will throw an error.
// And any key not recognized will also throw an error. For example:
//
// <button>{t('sav_changes')}</button>
//
// ...will throw because of the typo 'sav_changes' instead of 'save_changes'.
export const useTranslation = (namespaces: string | Array<string>) => {
const { data } = useMainContext()
// this can eventually be an object constructed from the input namespaces param above, but for now everything is already loaded
const loadedData: any = data.ui
const loadedData = data.ui
const namespacesArray = Array.isArray(namespaces) ? namespaces : [namespaces]
for (const namespace of namespacesArray) {
if (!(namespace in loadedData)) {
console.warn(
'The following namespaces in data.ui have been loaded: ' +
JSON.stringify(Object.keys(loadedData).sort()),
)
throw new TranslationNamespaceError(
`Namespace "${namespace}" not found in data. ` +
'Follow the stack trace to see which useTranslation(...) call is ' +
'causing this error. If the namespace is present in data/ui.yml ' +
'but this error is happening, find the related component ' +
'getServerSideProps() it goes through and make sure it calls ' +
`addUINamespaces() with "${namespace}".`,
)
}
}
function carefulGetWrapper(path: string) {
for (const namespace of namespacesArray) {
if (!(namespace in loadedData)) {
throw new TranslationNamespaceError(`Namespace "${namespace}" not found in data. `)
}
const deeper = loadedData[namespace]
if (typeof deeper === 'string') {
continue
}
try {
return carefuleGet(deeper, path)
} catch (error) {
if (!(error instanceof UngettableError)) {
throw error
}
}
}
return carefuleGet(loadedData, path)
}
return {
// The compiled string supports prefixing with a namespace such as `my-namespace:path.to.value`
tObject: (strings: TemplateStringsArray | string) => {
const key = typeof strings === 'string' ? strings : String.raw(strings)
return carefulGetWrapper(key) as UIStrings
},
t: (strings: TemplateStringsArray | string, ...values: Array<any>) => {
const key = typeof strings === 'string' ? strings : String.raw(strings, ...values)
const splitKey = key.split(':')
if (splitKey.length > 2) {
throw new Error('Multiple ":" not allowed in translation lookup path')
}
if (splitKey.length === 2) {
const [namespace, path] = splitKey
return get(loadedData[namespace], path)
}
const [path] = splitKey
if (Array.isArray(namespaces)) {
for (const namespace of namespaces) {
const val = get(loadedData[namespace], path)
if (val !== undefined) {
return val
}
}
return undefined
} else {
return get(loadedData[namespaces], path)
}
return carefulGetWrapper(key) as string
},
}
}
function carefuleGet(uiData: UIStrings, dottedPath: string) {
const splitPath = dottedPath.split('.')
const start = splitPath[0]
if (!(start in uiData)) {
throw new UngettableError(
`Namespace "${start}" not found in loaded data (not one of: ${Object.keys(uiData).sort()})`,
)
}
if (splitPath.length > 1) {
const deeper = uiData[start]
if (typeof deeper === 'string') {
throw new Error(`Namespace "${start}" is a string, not an object`)
}
return carefuleGet(deeper, splitPath.slice(1).join('.'))
} else {
if (!(start in uiData)) {
throw new UngettableError(`Key "${start}" not found in loaded data`)
}
return uiData[start]
}
}

View File

@@ -25,8 +25,8 @@ export function LearningTrackCard({ track }: Props) {
</h2>
<span className="f5 color-fg-muted">
{t('current_progress')
.replace('{n}', numberOfGuides)
.replace('{i}', currentGuideIndex + 1)}
.replace('{n}', `${numberOfGuides}`)
.replace('{i}', `${currentGuideIndex + 1}`)}
</span>
<hr />
<span className="h5 color-fg-default">

View File

@@ -11,7 +11,7 @@ type Props = {
export const LearningTrack = ({ track }: Props) => {
if (!track) return <div />
const { t } = useTranslation('product_guides')
const { t, tObject } = useTranslation('product_guides')
return (
<div data-testid="learning-track" className="col-12 col-md-6 my-3 px-4">
@@ -32,7 +32,7 @@ export const LearningTrack = ({ track }: Props) => {
{track.guides.map((guide) => (
<li key={guide.href + track.trackName}>
<span className="color-fg-muted mr-2">
{t('guide_types')[guide.page?.type || '']}
{tObject('guide_types')[guide.page?.type || ''] as string}
</span>
<Link
href={`${guide.href}?learn=${track.trackName}&learnProduct=${track.trackProduct}`}

View File

@@ -2,7 +2,12 @@ import { GetServerSideProps } from 'next'
import { Liquid } from 'liquidjs'
import pick from 'lodash/pick'
import { MainContextT, MainContext, getMainContext } from 'components/context/MainContext'
import {
MainContextT,
MainContext,
getMainContext,
addUINamespaces,
} from 'components/context/MainContext'
import { DefaultLayout } from 'components/DefaultLayout'
import { GHAEReleaseNotes } from 'src/release-notes/components/GHAEReleaseNotes'
import { GHESReleaseNotes } from 'src/release-notes/components/GHESReleaseNotes'
@@ -45,9 +50,13 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
])
const { latestPatch = '', latestRelease = '' } = req.context
const mainContext = await getMainContext(req, res)
addUINamespaces(req, mainContext.data.ui, ['release_notes'])
return {
props: {
mainContext: await getMainContext(req, res),
mainContext,
ghesContext:
currentVersion.plan === 'enterprise-server'
? {

View File

@@ -30,7 +30,7 @@ export const ApiVersionPicker = () => {
const router = useRouter()
const { currentVersion } = useVersion()
const { allVersions } = useMainContext()
const { t } = useTranslation(['products'])
const { t } = useTranslation('rest')
const basePath = router.asPath.split('#')[0].split('?')[0]
// Get current date from cookie, query path, or lastly set it to latest rest version date
const isValidApiVersion =

View File

@@ -34,7 +34,7 @@ const restRepoCategoryExceptionsTitles = {
export const RestBanner = () => {
const router = useRouter()
const { t } = useTranslation('products')
const { t } = useTranslation('rest')
// Having a productId === 'rest' and no router.query.category would mean a product landing page like http://docs.github.com/en/rest?apiVersion=2022-08-09
const isRestPage = router.query.productId === 'rest' || router.query.category
const restPage = router.query.category as string

View File

@@ -41,7 +41,7 @@ function getLanguageHighlight(selectedLanguage: string) {
}
export function RestCodeSamples({ operation, slug, heading }: Props) {
const { t } = useTranslation('products')
const { t } = useTranslation(['rest_reference'])
const { isEnterpriseServer } = useVersion()
// Refs to track the request example, response example
@@ -264,7 +264,7 @@ export function RestCodeSamples({ operation, slug, heading }: Props) {
}}
href="#"
>
{t(`rest.reference.code_sample_options.${optionKey}`)}
{t(`code_sample_options.${optionKey}`)}
</TabNav.Link>
))}
</TabNav>
@@ -311,7 +311,7 @@ export function RestCodeSamples({ operation, slug, heading }: Props) {
<h4
className="mt-5 mb-2 h5"
dangerouslySetInnerHTML={{
__html: displayedExample.response.description || t('rest.reference.response'),
__html: displayedExample.response.description || t('response'),
}}
></h4>
<div className="border rounded-1">
@@ -335,7 +335,7 @@ export function RestCodeSamples({ operation, slug, heading }: Props) {
}}
href="#"
>
{t(`rest.reference.response_options.${optionKey}`)}
{t(`response_options.${optionKey}`)}
</TabNav.Link>
))}
</TabNav>

View File

@@ -28,7 +28,7 @@ const DEFAULT_ACCEPT_HEADER = {
export function RestOperation({ operation }: Props) {
const titleSlug = slug(operation.title)
const { t } = useTranslation('products')
const { t } = useTranslation('rest_reference')
const router = useRouter()
const headers = [DEFAULT_ACCEPT_HEADER]
@@ -48,10 +48,8 @@ export function RestOperation({ operation }: Props) {
<CheckCircleFillIcon size={16} />
</span>
<span>
{t('rest.reference.works_with') + ' '}
<Link className="" href={`/${router.locale}/developers/apps`}>
GitHub Apps
</Link>
{t('works_with') + ' '}
<Link href={`/${router.locale}/developers/apps`}>GitHub Apps</Link>
</span>
</div>
)}
@@ -66,10 +64,7 @@ export function RestOperation({ operation }: Props) {
<ParameterTable
slug={titleSlug}
numPreviews={numPreviews}
heading={t('rest.reference.parameters').replace(
'{{ RESTOperationTitle }}',
operation.title,
)}
heading={t('parameters').replace('{{ RESTOperationTitle }}', operation.title)}
headers={headers}
parameters={operation.parameters}
bodyParameters={operation.bodyParameters}
@@ -80,10 +75,7 @@ export function RestOperation({ operation }: Props) {
<RestStatusCodes
statusCodes={operation.statusCodes}
slug={titleSlug}
heading={t('rest.reference.http_status_code').replace(
'{{ RESTOperationTitle }}',
operation.title,
)}
heading={t('http_status_code').replace('{{ RESTOperationTitle }}', operation.title)}
/>
)}
</div>
@@ -95,10 +87,7 @@ export function RestOperation({ operation }: Props) {
<RestCodeSamples
operation={operation}
slug={titleSlug}
heading={t('rest.reference.code_samples').replace(
'{{ RESTOperationTitle }}',
operation.title,
)}
heading={t('code_samples').replace('{{ RESTOperationTitle }}', operation.title)}
/>
)}
@@ -108,14 +97,8 @@ export function RestOperation({ operation }: Props) {
previews={operation.previews}
heading={
operation.previews.length > 1
? `${t('rest.reference.preview_notices').replace(
'{{ RESTOperationTitle }}',
operation.title,
)}`
: `${t('rest.reference.preview_notice').replace(
'{{ RESTOperationTitle }}',
operation.title,
)}`
? `${t('preview_notices').replace('{{ RESTOperationTitle }}', operation.title)}`
: `${t('preview_notice').replace('{{ RESTOperationTitle }}', operation.title)}`
}
/>
)}

View File

@@ -8,7 +8,7 @@ type Props = {
}
export function RestStatusCodes({ statusCodes, slug, heading }: Props) {
const { t } = useTranslation('products')
const { t } = useTranslation('rest_reference')
return (
<>
@@ -19,8 +19,8 @@ export function RestStatusCodes({ statusCodes, slug, heading }: Props) {
<table>
<thead>
<tr className="text-left">
<th>{t('rest.reference.status_code')}</th>
<th>{t('rest.reference.description')}</th>
<th>{t('status_code')}</th>
<th>{t('description')}</th>
</tr>
</thead>
<tbody>

View File

@@ -1,7 +1,12 @@
import { GetServerSideProps } from 'next'
import { Operation } from 'src/rest/components/types'
import { RestReferencePage } from 'src/rest/components/RestReferencePage'
import { getMainContext, MainContext, MainContextT } from 'components/context/MainContext'
import {
addUINamespaces,
getMainContext,
MainContext,
MainContextT,
} from 'components/context/MainContext'
import {
AutomatedPageContext,
AutomatedPageContextT,
@@ -77,10 +82,13 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
restOperationsMiniTocItems && miniTocItems.push(...restOperationsMiniTocItems)
}
const mainContext = await getMainContext(req, res)
addUINamespaces(req, mainContext.data.ui, ['parameter_table', 'rest_reference'])
return {
props: {
restOperations,
mainContext: await getMainContext(req, res),
mainContext,
automatedPageContext: getAutomatedPageContextFromRequest(req),
},
}

View File

@@ -45,12 +45,12 @@ function SearchResultHits({ hits, search }: { hits: SearchResultHitT[]; search:
}
function NoSearchResults() {
const { t } = useTranslation('search')
const { t } = useTranslation('search_results')
return (
<div className="d-flex flex-items-center flex-column my-6 border rounded-2">
<div className="d-flex flex-items-center flex-column p-4">
<SearchIcon size={24} />
<Text className="f2 mt-3">{t('n_results').replace('{n}', 0)}</Text>
<Text className="f2 mt-3">{t('n_results').replace('{n}', '0')}</Text>
</div>
</div>
)

View File

@@ -8,7 +8,7 @@ interface Props {
}
export function ValidationErrors({ errors }: Props) {
const { t } = useTranslation('search')
const { t } = useTranslation('search_results')
return (
<div>

View File

@@ -16,7 +16,7 @@ type Props = {
export function Search({ search }: Props) {
const { formatInteger } = useNumberFormatter()
const { t } = useTranslation('search')
const { t } = useTranslation('search_results')
const { currentVersion } = useVersion()
const { query } = search.search

View File

@@ -1,6 +1,11 @@
import type { GetServerSideProps } from 'next'
import { MainContextT, MainContext, getMainContext } from 'components/context/MainContext'
import {
MainContextT,
MainContext,
getMainContext,
addUINamespaces,
} from 'components/context/MainContext'
import { DefaultLayout } from 'components/DefaultLayout'
import type { SearchT } from 'src/search/components/types'
import { Search } from 'src/search/components/index'
@@ -37,6 +42,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
}
const mainContext = await getMainContext(req, res)
addUINamespaces(req, mainContext.data.ui, ['search_results'])
if (!req.context.search) {
// This should have been done by the middleware.

View File

@@ -5,7 +5,6 @@ import { useRouter } from 'next/router'
import { slug } from 'github-slugger'
import cx from 'classnames'
import { useMainContext } from 'components/context/MainContext'
import { useVersion } from 'src/versions/components/useVersion'
import { HeadingLink } from 'components/article/HeadingLink'
import { useTranslation } from 'src/languages/components/useTranslation'
@@ -48,15 +47,14 @@ function isScrapedGhesVersion(version: ReturnType<typeof useVersion>) {
export function Webhook({ webhook }: Props) {
// Get version for requests to switch webhook action type
const version = useVersion()
const { t } = useTranslation('products')
const { t, tObject } = useTranslation('webhooks')
const router = useRouter()
const context = useMainContext()
// Get more user friendly language for the different availability options in
// the webhook schema (we can't change it directly in the schema). Note that
// we specifically don't want to translate these strings with useTranslation()
// like we usually do with strings from data/ui.yml.
const rephraseAvailability = context.data.ui.products.webhooks.rephrase_availability
const rephraseAvailability = tObject('rephrase_availability')
// The param that was clicked so we can expand its property <details> element
const [clickedBodyParameterName, setClickedBodyParameterName] = useState<undefined | string>('')
@@ -150,10 +148,7 @@ export function Webhook({ webhook }: Props) {
<div dangerouslySetInnerHTML={{ __html: currentWebhookAction.summaryHtml }}></div>
<h3
dangerouslySetInnerHTML={{
__html: t('webhooks.availability').replace(
'{{ WebhookName }}',
currentWebhookAction.category,
),
__html: t('availability').replace('{{ WebhookName }}', currentWebhookAction.category),
}}
/>
<ul>
@@ -169,7 +164,9 @@ export function Webhook({ webhook }: Props) {
} else {
return (
<li key={`availability-${availability}`}>
{rephraseAvailability[availability] ?? availability}
{availability in rephraseAvailability
? (rephraseAvailability[availability] as string)
: availability}
</li>
)
}
@@ -177,7 +174,7 @@ export function Webhook({ webhook }: Props) {
</ul>
<h3
dangerouslySetInnerHTML={{
__html: t('webhooks.webhook_payload_object').replace(
__html: t('webhook_payload_object').replace(
'{{ WebhookName }}',
currentWebhookAction.category,
),
@@ -185,7 +182,7 @@ export function Webhook({ webhook }: Props) {
/>
{error && (
<Flash className="mb-5" variant="danger">
<p>{t('webhooks.action_type_switch_error')}</p>
<p>{t('action_type_switch_error')}</p>
<p>
<code className="f6" style={{ background: 'none' }}>
{error.toString()}
@@ -201,8 +198,7 @@ export function Webhook({ webhook }: Props) {
aria-label="Select a webhook action type"
className="text-normal"
>
{t('webhooks.action_type')}:{' '}
<span className="text-bold">{currentWebhookActionType}</span>
{t('action_type')}: <span className="text-bold">{currentWebhookActionType}</span>
</ActionMenu.Button>
<ActionMenu.Overlay>
<ActionList selectionVariant="single">
@@ -240,11 +236,8 @@ export function Webhook({ webhook }: Props) {
{webhook.data.payloadExample && (
<>
<h3>{t('webhooks.webhook_payload_example')}</h3>
<div
className={cx(styles.payloadExample, 'border rounded-1 my-0')}
data-highlight={'json'}
>
<h3>{t('webhook_payload_example')}</h3>
<div className={cx(styles.payloadExample, 'border rounded-1 my-0')} data-highlight="json">
<code>{JSON.stringify(webhook.data.payloadExample, null, 2)}</code>
</div>
</>

View File

@@ -2,7 +2,12 @@ import { GetServerSideProps } from 'next'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { getMainContext, MainContext, MainContextT } from 'components/context/MainContext'
import {
addUINamespaces,
getMainContext,
MainContext,
MainContextT,
} from 'components/context/MainContext'
import {
getAutomatedPageContextFromRequest,
AutomatedPageContext,
@@ -77,6 +82,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
const res = context.res as object
const currentVersion = context.query.versionId as string
const mainContext = await getMainContext(req, res)
addUINamespaces(req, mainContext.data.ui, ['parameter_table', 'webhooks'])
const { miniTocItems } = getAutomatedPageContextFromRequest(req)
// Get data for initial webhooks page (i.e. only 1 action type per webhook and

View File

@@ -23,17 +23,13 @@ picker:
release_notes:
banner_text: GitHub began rolling these changes out to enterprises on
search:
need_help: Need help?
placeholder: Search GitHub Docs
search_results_for: Search results for
no_content: No content
matches_found: Results found
matches_displayed: Matches displayed
search_error: An error occurred trying to perform the search.
description: Enter a search term to find it in the GitHub Docs.
placeholder: Search GitHub Docs
label: Search GitHub Docs
search_results:
search_results_for: Search results for
matches_displayed: Matches displayed
n_results: '{n} results'
one_result: 1 result
search_validation_error: Validation error with search query
homepage:
explore_by_product: Explore by product
@@ -53,11 +49,6 @@ pages:
about_versions: About versions
permissions_statement: Who can use this feature
video_from_transcript: See video for this transcript
errors:
oops: Ooops!
something_went_wrong: It looks like something went wrong.
we_track_errors: We track these errors automatically, but if the problem persists please feel free to contact us.
page_doesnt_exist: It looks like this page doesn't exist.
support:
still_need_help: Still need help?
contact_support: Contact support
@@ -97,13 +88,9 @@ parameter_table:
see_preview_notices: See preview notices
type: Type
single_enum_description: Value
products:
audit_logs:
action: Action
description: Description
button_text:
copy_to_clipboard: Copy to clipboard
copied: Copied!
graphql:
reference:
implements: <code>{{ GraphQLItemTitle }}</code> Implements
@@ -145,7 +132,7 @@ products:
additionalPermissions: Additional permissions
uat: UAT
iat: IAT
reference:
rest_reference:
in: In
description: Description
notes: Notes
@@ -175,6 +162,9 @@ products:
ghcli: GitHub CLI
javascript: JavaScript
curl: cURL
button_text:
copy_to_clipboard: Copy to clipboard
copied: Copied!
webhooks:
action_type_switch_error: There was an error switching webhook action types.
action_type: Action type
@@ -202,13 +192,11 @@ product_landing:
quickstart: Quickstart
reference: Reference
overview: Overview
try_ghec_for_free: Try Enterprise Cloud for free
plan_your_migration: Plan your migration
releases: Releases
guides: Guides
explore_guides: Explore guides
code_examples: Code examples
search_code_examples: Search code examples
search_results_for: Search results for
matches_displayed: Matches displayed
show_more: Show more
explore_people_and_projects: Explore people and projects
sorry: Sorry, there is no result for
no_example: It looks like we don't have an example that fits your filter.
@@ -228,9 +216,6 @@ product_landing:
view: View all
view_transcript: View video transcript
all_docs: 'All {{ title }} docs'
code_example:
search_button: Search
search_examples: 'Search code examples:'
product_guides:
learning_paths_title: '{{ name }} learning paths'
start_path: Start learning path
@@ -258,11 +243,6 @@ learning_track_nav:
next_guide: Next
more_guides: More guides →
current_progress: '{i} of {n} in learning path'
toggle_images:
off: Images are off, click to show
on: Images are on, click to hide
hide_single: Hide image
show_single: Show image
scroll_button:
scroll_to_top: Scroll to top
popovers: