1
0
mirror of synced 2025-12-19 09:57:42 -05:00

Refactor languages module: separate client and server code (#57949)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Kevin Heis
2025-10-23 12:30:06 -07:00
committed by GitHub
parent f4ea74a1f6
commit 841f6b1718
63 changed files with 230 additions and 297 deletions

View File

@@ -4,14 +4,9 @@ import type { NextConfig } from 'next'
import frontmatter from '@gr2m/gray-matter'
import { getLogLevelNumber } from '@/observability/logger/lib/log-levels'
import { languageKeys } from '@/languages/lib/languages'
const ROOT = process.env.ROOT || '.'
// Language keys are defined here because Next.js config compilation doesn't resolve the @/ path alias
// Importing from src/languages/lib/languages.ts would fail when it tries to import @/frame/lib/constants
// This must match the languages defined in src/languages/lib/languages.ts
const languageKeys = ['en', 'es', 'ja', 'pt', 'zh', 'ru', 'fr', 'ko', 'de']
const homepage = path.posix.join(ROOT, 'content/index.md')
const { data } = frontmatter(fs.readFileSync(homepage, 'utf8'))
const productIds = data.children as string[]

View File

@@ -10,7 +10,7 @@ import { initializeEvents } from '@/events/components/events'
import { CTAPopoverProvider } from '@/frame/components/context/CTAContext'
import { SharedUIContextProvider } from '@/frame/components/context/SharedUIContext'
import { LanguagesContext, LanguagesContextT } from '@/languages/components/LanguagesContext'
import { clientLanguages, type ClientLanguageCode } from '@/languages/lib/client-languages'
import { languages, type LanguageCode } from '@/languages/lib/languages'
import { MainContextProvider } from '@/app/components/MainContextProvider'
import { createMinimalMainContext } from '@/app/lib/main-context-adapter'
import type { AppRouterContext } from '@/app/lib/app-router-context'
@@ -31,16 +31,11 @@ interface ClientLayoutProps {
export function ClientLayout({ children, appContext, pageData }: ClientLayoutProps): JSX.Element {
const { theme } = useTheme()
const locale: ClientLanguageCode = useDetectLocale()
const locale: LanguageCode = useDetectLocale()
const [isInitialized, setIsInitialized] = useState(false)
const [initializationError, setInitializationError] = useState<Error | null>(null)
const languagesContext: LanguagesContextT = useMemo(
() => ({
languages: clientLanguages,
}),
[],
)
const languagesContext: LanguagesContextT = useMemo(() => ({ languages }), [])
// Create MainContext-compatible data for App Router
const mainContext = useMemo(

View File

@@ -1,7 +1,7 @@
'use client'
import { createContext, useContext } from 'react'
import { clientLanguages, type ClientLanguageCode } from '@/languages/lib/client-languages'
import { languages, type LanguageCode } from '@/languages/lib/languages'
export type AppRouterLanguageItem = {
name: string
@@ -12,7 +12,7 @@ export type AppRouterLanguageItem = {
export type AppRouterLanguagesContextT = {
languages: Record<string, AppRouterLanguageItem>
currentLanguage?: ClientLanguageCode
currentLanguage?: LanguageCode
}
export const AppRouterLanguagesContext = createContext<AppRouterLanguagesContextT | null>(null)
@@ -34,7 +34,7 @@ export const useAppRouterLanguages = (): AppRouterLanguagesContextT => {
*/
interface AppRouterLanguagesProviderProps {
children: React.ReactNode
currentLanguage?: ClientLanguageCode
currentLanguage?: LanguageCode
}
export function AppRouterLanguagesProvider({
@@ -42,7 +42,7 @@ export function AppRouterLanguagesProvider({
currentLanguage,
}: AppRouterLanguagesProviderProps) {
const value: AppRouterLanguagesContextT = {
languages: clientLanguages,
languages,
currentLanguage,
}

View File

@@ -1,10 +1,10 @@
import { getUIDataMerged } from '@/data-directory/lib/get-data'
import { createTranslationFunctions } from '@/languages/lib/translation-utils'
import { LinkExternalIcon } from '@primer/octicons-react'
import type { ClientLanguageCode } from '@/languages/lib/client-languages'
import type { LanguageCode } from '@/languages/lib/languages'
interface ServerFooterProps {
currentLanguage: ClientLanguageCode
currentLanguage: LanguageCode
}
export function ServerFooter({ currentLanguage }: ServerFooterProps) {

View File

@@ -1,10 +1,10 @@
import { getUIDataMerged } from '@/data-directory/lib/get-data'
import { type ClientLanguageCode } from '@/languages/lib/client-languages'
import { type LanguageCode } from '@/languages/lib/languages'
import { translate } from '@/languages/lib/translation-utils'
import { extractLanguageFromPath } from '@/app/lib/language-utils'
export interface AppRouterContext {
currentLanguage: ClientLanguageCode
currentLanguage: LanguageCode
currentVersion: string
sitename: string
site: {
@@ -19,7 +19,7 @@ export interface AppRouterContext {
*/
export function createAppRouterContext(
pathname: string = '/',
fallbackLanguage?: ClientLanguageCode,
fallbackLanguage?: LanguageCode,
): AppRouterContext {
let language = extractLanguageFromPath(pathname)

View File

@@ -1,16 +1,16 @@
import { clientLanguageKeys, type ClientLanguageCode } from '@/languages/lib/client-languages'
import { languageKeys, type LanguageCode } from '@/languages/lib/languages'
/**
* Extract language from URL path
* Handles paths like /en/something, /es/articles, etc.
*/
export function extractLanguageFromPath(path: string): ClientLanguageCode {
export function extractLanguageFromPath(path: string): LanguageCode {
try {
const pathSegments = path.split('/')
const firstSegment = pathSegments[1]
if (firstSegment && clientLanguageKeys.includes(firstSegment)) {
return firstSegment as ClientLanguageCode
if (firstSegment && languageKeys.includes(firstSegment)) {
return firstSegment as LanguageCode
}
} catch (error) {
console.warn('Failed to extract language from path:', error)
@@ -24,7 +24,7 @@ export function extractLanguageFromPath(path: string): ClientLanguageCode {
export function hasLanguagePrefix(path: string): boolean {
const pathSegments = path.split('/')
const firstSegment = pathSegments[1]
return Boolean(firstSegment && clientLanguageKeys.includes(firstSegment))
return Boolean(firstSegment && languageKeys.includes(firstSegment))
}
/**
@@ -43,7 +43,7 @@ export function stripLanguagePrefix(path: string): string {
* Add language prefix to path if it doesn't have one
* e.g., /articles/example + 'es' -> /es/articles/example
*/
export function addLanguagePrefix(path: string, language: ClientLanguageCode): string {
export function addLanguagePrefix(path: string, language: LanguageCode): string {
if (hasLanguagePrefix(path)) {
return path
}

View File

@@ -1,55 +1,51 @@
'use client'
import { createContext, useContext, ReactNode, useMemo } from 'react'
import {
clientLanguages,
clientLanguageKeys,
type ClientLanguageCode,
} from '@/languages/lib/client-languages'
import { languages, languageKeys, type LanguageCode } from '@/languages/lib/languages'
interface LocaleContextType {
readonly locale: ClientLanguageCode
readonly isValidLocale: (locale: string) => locale is ClientLanguageCode
readonly getSupportedLocales: () => readonly ClientLanguageCode[]
readonly getLocaleDisplayName: (locale: ClientLanguageCode) => string
readonly getLocaleNativeName: (locale: ClientLanguageCode) => string
readonly locale: LanguageCode
readonly isValidLocale: (locale: string) => locale is LanguageCode
readonly getSupportedLocales: () => readonly LanguageCode[]
readonly getLocaleDisplayName: (locale: LanguageCode) => string
readonly getLocaleNativeName: (locale: LanguageCode) => string
}
const LocaleContext = createContext<LocaleContextType | null>(null)
interface LocaleProviderProps {
readonly children: ReactNode
readonly locale: ClientLanguageCode
readonly locale: LanguageCode
}
// Use client languages as the source of truth for supported locales
const SUPPORTED_LOCALES: readonly ClientLanguageCode[] = clientLanguageKeys as ClientLanguageCode[]
const SUPPORTED_LOCALES: readonly LanguageCode[] = languageKeys as LanguageCode[]
/**
* Validates if a string is a supported locale
*/
function isValidLocale(locale: string): locale is ClientLanguageCode {
return clientLanguageKeys.includes(locale)
function isValidLocale(locale: string): locale is LanguageCode {
return languageKeys.includes(locale)
}
/**
* Gets display name for a locale from client languages data
* Gets display name for a locale from languages module
*/
function getLocaleDisplayName(locale: ClientLanguageCode): string {
return clientLanguages[locale]?.name || locale
function getLocaleDisplayName(locale: LanguageCode): string {
return languages[locale]?.name || locale
}
/**
* Gets native name for a locale from client languages data
* Gets native name for a locale from languages module
*/
function getLocaleNativeName(locale: ClientLanguageCode): string {
return clientLanguages[locale]?.nativeName || clientLanguages[locale]?.name || locale
function getLocaleNativeName(locale: LanguageCode): string {
return languages[locale]?.nativeName || languages[locale]?.name || locale
}
/**
* Gets browser language preference as a valid locale
*/
function getBrowserLocale(): ClientLanguageCode {
function getBrowserLocale(): LanguageCode {
if (typeof window === 'undefined') return 'en'
const browserLang = window.navigator.language.split('-')[0]
@@ -77,7 +73,7 @@ export function LocaleProvider({ children, locale }: LocaleProviderProps): JSX.E
/**
* Hook to get current locale with enhanced error handling
*/
export function useLocale(): ClientLanguageCode {
export function useLocale(): LanguageCode {
const context = useContext(LocaleContext)
if (context) {
@@ -118,4 +114,4 @@ export function useLocaleContext(): LocaleContextType {
}
export { isValidLocale, getLocaleDisplayName, getLocaleNativeName }
export type { LocaleContextType, ClientLanguageCode }
export type { LocaleContextType, LanguageCode }

View File

@@ -1,11 +1,11 @@
import { extractLanguageFromPath } from '@/app/lib/language-utils'
import { extractVersionFromPath } from '@/app/lib/version-utils'
import { getUIDataMerged } from '@/data-directory/lib/get-data'
import { type ClientLanguageCode } from '@/languages/lib/client-languages'
import { type LanguageCode } from '@/languages/lib/languages'
import { createTranslationFunctions, translate } from '@/languages/lib/translation-utils'
export interface ServerAppRouterContext {
currentLanguage: ClientLanguageCode
currentLanguage: LanguageCode
currentVersion: string
sitename: string
site: { data: { ui: any } }
@@ -33,7 +33,7 @@ export function createServerAppRouterContext(pathname: string): ServerAppRouterC
/**
* Create server-side footer with translations
*/
export function createServerFooterContent(language: ClientLanguageCode) {
export function createServerFooterContent(language: LanguageCode) {
const uiData = getUIDataMerged(language)
const { t } = createTranslationFunctions(uiData, 'footer')

View File

@@ -2,24 +2,24 @@
import { usePathname } from 'next/navigation'
import { useMemo, useEffect, useState } from 'react'
import { clientLanguageKeys, type ClientLanguageCode } from '@/languages/lib/client-languages'
import { languageKeys, type LanguageCode } from '@/languages/lib/languages'
import Cookies from '@/frame/components/lib/cookies'
import { USER_LANGUAGE_COOKIE_NAME } from '@/frame/lib/constants'
/**
* Validates if a string is a supported locale using client languages
*/
function isValidLocale(locale: string): locale is ClientLanguageCode {
return clientLanguageKeys.includes(locale)
function isValidLocale(locale: string): locale is LanguageCode {
return languageKeys.includes(locale)
}
/**
* Hook to detect locale from various sources with fallback logic
*/
export function useDetectLocale(): ClientLanguageCode {
export function useDetectLocale(): LanguageCode {
const pathname = usePathname()
const [cookieLanguage, setCookieLanguage] = useState<ClientLanguageCode | null>(null)
const [browserLanguage, setBrowserLanguage] = useState<ClientLanguageCode | null>(null)
const [cookieLanguage, setCookieLanguage] = useState<LanguageCode | null>(null)
const [browserLanguage, setBrowserLanguage] = useState<LanguageCode | null>(null)
// Read cookie and browser language on client-side mount
useEffect(() => {
@@ -71,7 +71,7 @@ export function useDetectLocale(): ClientLanguageCode {
/**
* Utility function to detect locale from pathname (for server-side use)
*/
export function detectLocaleFromPath(pathname: string): ClientLanguageCode {
export function detectLocaleFromPath(pathname: string): LanguageCode {
const pathSegments = pathname.split('/')
const firstSegment = pathSegments[1]
@@ -82,8 +82,8 @@ export function detectLocaleFromPath(pathname: string): ClientLanguageCode {
return 'en'
}
export function getSupportedLocales(): readonly ClientLanguageCode[] {
return clientLanguageKeys as ClientLanguageCode[]
export function getSupportedLocales(): readonly LanguageCode[] {
return languageKeys as LanguageCode[]
}
export { isValidLocale }

View File

@@ -2,14 +2,14 @@
* Enhanced type definitions for the app router with strict validation
*/
import type { ClientLanguageCode } from '@/languages/lib/client-languages'
import type { LanguageCode } from '@/languages/lib/languages'
// Core theme types with strict validation
export type Theme = 'light' | 'dark' | 'auto'
export type ColorMode = 'light' | 'dark'
// Re-export locale type from client-languages for consistency
export type Locale = ClientLanguageCode
// Re-export locale type from languages.ts for consistency
export type Locale = LanguageCode
// Version and product identifiers with validation
export type VersionId = string

View File

@@ -14,7 +14,7 @@ import { isArchivedVersion } from '@/archives/lib/is-archived-version'
import { setFastlySurrogateKey, SURROGATE_ENUMS } from '@/frame/middleware/set-fastly-surrogate-key'
import { readCompressedJsonFileFallbackLazily } from '@/frame/lib/read-json-file'
import { archivedCacheControl, languageCacheControl } from '@/frame/middleware/cache-control'
import { pathLanguagePrefixed, languagePrefixPathRegex } from '@/languages/lib/languages'
import { pathLanguagePrefixed, languagePrefixPathRegex } from '@/languages/lib/languages-server'
import getRedirect, { splitPathByLanguage } from '@/redirects/lib/get-redirect'
import getRemoteJSON from '@/frame/lib/get-remote-json'
import { ExtendedRequest } from '@/types'

View File

@@ -30,7 +30,7 @@ import { brotliCompressSync } from 'zlib'
import chalk from 'chalk'
import { program, Option } from 'commander'
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import { loadPages, loadUnversionedTree } from '@/frame/lib/page-data'
import { CACHE_FILE_PATH, getPageInfo } from '../middleware/article-pageinfo'

View File

@@ -11,7 +11,7 @@ import { program } from 'commander'
import walk from 'walk-sync'
import walkFiles from '@/workflows/walk-files'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
const EXCEPTIONS = new Set([
'assets/images/site/favicon.ico',

View File

@@ -7,7 +7,7 @@ import { visitParents } from 'unist-util-visit-parents'
import { visit, SKIP } from 'unist-util-visit'
import { remove } from 'unist-util-remove'
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import { MARKDOWN_OPTIONS } from '../../content-linter/lib/helpers/unified-formatter-options'
interface Config {

View File

@@ -2,7 +2,7 @@
import { filterTokens } from 'markdownlint-rule-helpers'
import { addFixErrorDetail, getRange } from '../helpers/utils'
import { allLanguageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages'
import type { RuleParams, RuleErrorCallback, Rule } from '../../types'
export const internalLinksNoLang: Rule = {
@@ -28,7 +28,7 @@ export const internalLinksNoLang: Rule = {
.filter((attr: [string, string]) => attr[1].startsWith('/') || !attr[1].startsWith('//'))
// Filter out link paths that start with language code
.filter((attr: [string, string]) =>
allLanguageKeys.some((lang) => attr[1].split('/')[1] === lang),
languageKeys.some((lang) => attr[1].split('/')[1] === lang),
)
// Get the link path from the attribute
.map((attr: [string, string]) => attr[1])

View File

@@ -15,7 +15,7 @@ import { defaultConfig } from '../lib/default-markdownlint-options'
import { prettyPrintResults } from './pretty-print-results'
import { getLintableYml } from '@/content-linter/lib/helpers/get-lintable-yml'
import { printAnnotationResults } from '../lib/helpers/print-annotations'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import { shouldIncludeResult } from '../lib/helpers/should-include-result'
program

View File

@@ -9,7 +9,7 @@ import walk from 'walk-sync'
import { zip } from 'lodash-es'
import { beforeAll, describe, expect, test } from 'vitest'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import { getDiffFiles } from '../lib/diff-files'
const __dirname = path.dirname(fileURLToPath(import.meta.url))

View File

@@ -43,7 +43,7 @@ import { writeFileSync, statSync } from 'fs'
import { program, Option } from 'commander'
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import { allVersions } from '@/versions/lib/all-versions'
import { allDocuments, POSSIBLE_FIELDS, type AllDocument } from './lib'

View File

@@ -1,7 +1,7 @@
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
import Page from '@/frame/lib/page'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-version'
import { DataDirectory } from '@/tests/helpers/data-directory'

View File

@@ -1,7 +1,7 @@
import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'
import { liquid } from '@/content-render/index'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import { DataDirectory } from '@/tests/helpers/data-directory'
describe('liquid helper tags', () => {

View File

@@ -5,7 +5,7 @@ import yaml from 'js-yaml'
import matter from '@gr2m/gray-matter'
import { merge, get } from 'lodash-es'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import { correctTranslatedContentStrings } from '@/languages/lib/correct-translation-content'
interface YAMLException extends Error {

View File

@@ -2,7 +2,7 @@ import fs from 'fs'
import path from 'path'
import chalk from 'chalk'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
type Options = {
verbose?: boolean

View File

@@ -38,7 +38,7 @@ import type { Page } from '@/types'
import warmServer from '@/frame/lib/warm-server'
import { getDeepDataByLanguage } from '@/data-directory/lib/get-data'
import { getLiquidTokens } from '@/content-linter/lib/helpers/liquid-utils'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import { correctTranslatedContentStrings } from '@/languages/lib/correct-translation-content'
const EXCEPTIONS = new Set([

View File

@@ -3,7 +3,7 @@ import path from 'path'
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import {
getDataByLanguage,
getDeepDataByLanguage,

View File

@@ -2,7 +2,7 @@ import { expect, test, vi } from 'vitest'
import { get, getDOM } from '@/tests/helpers/e2etest'
import { describeIfDocsEarlyAccess } from '@/tests/helpers/conditional-runs'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
const VALID_EARLY_ACCESS_URI = '/early-access/github/save-time-with-slash-commands'

View File

@@ -1,4 +1,4 @@
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import { allVersionKeys } from '@/versions/lib/all-versions'
import { productIds } from '@/products/lib/all-products'
import { allTools } from '@/tools/lib/all-tools'

View File

@@ -1,9 +1,9 @@
import path from 'path'
import languages from '@/languages/lib/languages-server'
import type { Language } from '@/languages/lib/languages'
import type { UnversionedTree, UnversionLanguageTree, SiteTree, Tree } from '@/types'
import languages from '@/languages/lib/languages'
import { allVersions } from '@/versions/lib/all-versions'
import createTree from './create-tree'
import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-version'

View File

@@ -2,7 +2,7 @@ import type { NextFunction, Response } from 'express'
import type { ExtendedRequest, Context } from '@/types'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import enterpriseServerReleases from '@/versions/lib/enterprise-server-releases'
import { allVersions } from '@/versions/lib/all-versions'
import { productMap } from '@/products/lib/all-products'

View File

@@ -3,7 +3,7 @@ import type { Response, NextFunction } from 'express'
import type { ExtendedRequest } from '@/types'
import { getProductGroups } from '@/products/lib/get-product-groups'
import warmServer from '@/frame/lib/warm-server'
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import { allVersionKeys } from '@/versions/lib/all-versions'
const isHomepage = (path: string) => {

View File

@@ -4,7 +4,7 @@ import type { Response, NextFunction } from 'express'
import { ROOT } from '@/frame/lib/constants'
import Page from '@/frame/lib/page'
import { languagePrefixPathRegex } from '@/languages/lib/languages'
import { languagePrefixPathRegex } from '@/languages/lib/languages-server'
import type { ExtendedRequest } from '@/types'
interface FindPageOptions {

View File

@@ -1,6 +1,6 @@
import { shouldUseAppRouter, isVersionedPath } from '@/app/lib/routing-patterns'
import { isArchivedVersion } from '@/archives/lib/is-archived-version'
import { languagePrefixPathRegex } from '@/languages/lib/languages'
import { languagePrefixPathRegex } from '@/languages/lib/languages-server'
import versionSatisfiesRange from '@/versions/lib/version-satisfies-range'
import type { NextFunction, Request, Response } from 'express'
import helmet from 'helmet'

View File

@@ -5,7 +5,7 @@ import type { ExtendedRequest } from '@/types'
import { defaultCacheControl } from '@/frame/middleware/cache-control'
import catchMiddlewareError from '@/observability/middleware/catch-middleware-error'
import statsd from '@/observability/lib/statsd'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import { allVersions } from '@/versions/lib/all-versions'
const router = express.Router()

View File

@@ -19,7 +19,7 @@ import path from 'path'
import type { Response, NextFunction } from 'express'
import type { ExtendedRequest, UnversionedTree, SiteTree } from '@/types'
import languages, { languageKeys } from '@/languages/lib/languages'
import languages, { languageKeys } from '@/languages/lib/languages-server'
import createTree from '@/frame/lib/create-tree'
import warmServer from '@/frame/lib/warm-server'
import { loadSiteTree, loadPages, loadPageMap } from '@/frame/lib/page-data'

View File

@@ -6,7 +6,7 @@ import { decode } from 'html-entities'
import { chain, pick } from 'lodash-es'
import { loadPages } from '@/frame/lib/page-data'
import libLanguages from '@/languages/lib/languages'
import libLanguages from '@/languages/lib/languages-server'
import { liquid } from '@/content-render/index'
import patterns from '@/frame/lib/patterns'
import removeFPTFromPath from '@/versions/lib/remove-fpt-from-path'

View File

@@ -17,7 +17,7 @@ import createApp from '@/frame/lib/app'
import EnterpriseServerReleases from '@/versions/lib/enterprise-server-releases'
import loadRedirects from '@/redirects/lib/precompile'
import { loadPageMap, loadPages } from '@/frame/lib/page-data'
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import { RewriteAssetPathsPlugin } from '@/ghes-releases/scripts/deprecate/rewrite-asset-paths'
import Page from '@/frame/lib/page'

View File

@@ -3,7 +3,7 @@ import {
readCompressedJsonFileFallback,
} from '@/frame/lib/read-json-file'
import { getAutomatedPageMiniTocItems } from '@/frame/lib/get-mini-toc-items'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import { allVersions } from '@/versions/lib/all-versions'
interface GraphqlContext {
currentLanguage: string

View File

@@ -1,67 +0,0 @@
import type { LanguageItem } from '@/languages/components/LanguagesContext'
/**
* Client-safe language data extracted from src/languages/lib/languages.ts.
* Only used by frontend components.
* Does not include server-side logic or Node.js-specific fs or path operations.
*/
export const clientLanguages: Record<string, LanguageItem> = {
en: {
name: 'English',
code: 'en',
nativeName: 'English',
hreflang: 'en',
},
es: {
name: 'Spanish',
code: 'es',
nativeName: 'Español',
hreflang: 'es',
},
ja: {
name: 'Japanese',
code: 'ja',
nativeName: '日本語',
hreflang: 'ja',
},
pt: {
name: 'Portuguese',
code: 'pt',
nativeName: 'Português do Brasil',
hreflang: 'pt',
},
zh: {
name: 'Simplified Chinese',
code: 'zh',
nativeName: '简体中文',
hreflang: 'zh-Hans',
},
ru: {
name: 'Russian',
code: 'ru',
nativeName: 'Русский',
hreflang: 'ru',
},
fr: {
name: 'French',
code: 'fr',
nativeName: 'Français',
hreflang: 'fr',
},
ko: {
name: 'Korean',
code: 'ko',
nativeName: '한국어',
hreflang: 'ko',
},
de: {
name: 'German',
code: 'de',
nativeName: 'Deutsch',
hreflang: 'de',
},
}
export const clientLanguageKeys: string[] = Object.keys(clientLanguages)
export type ClientLanguageCode = keyof typeof clientLanguages

View File

@@ -1,7 +1,7 @@
import fs from 'fs/promises'
import path from 'path'
import yaml from 'js-yaml'
import languages from './languages'
import languages from './languages-server'
const cache: Record<string, any> = {}

View File

@@ -0,0 +1,97 @@
/*
This file adds the following properties to languages in ./languages.ts:
- dir: string
This file will also remove languages for local development and tests
that have not be specified by ENABLED_LANGUAGES
*/
import path from 'path'
import fs from 'fs'
import dotenv from 'dotenv'
import { ROOT, TRANSLATIONS_ROOT, TRANSLATIONS_FIXTURE_ROOT } from '@/frame/lib/constants'
import { languages as baseLanguages, type Language as BaseLanguage } from './languages'
dotenv.config({ quiet: true })
// Server-side language extends base language with required dir property
export interface Language extends BaseLanguage {
dir: string
}
export interface Languages {
[code: string]: Language
}
function getRoot(languageCode: string): string {
if (languageCode === 'en') return ROOT
// This one trumps anything else. This makes it possible, and convenient,
// for running tests that depends on testing translations based on
// fixtures exclusively.
if (TRANSLATIONS_FIXTURE_ROOT) {
return path.join(TRANSLATIONS_FIXTURE_ROOT, languageCode)
}
// example: process.env.TRANSLATIONS_ROOT_ES_ES
const possibleEnvVar =
process.env[`TRANSLATIONS_ROOT_${languageCode.toUpperCase().replace(/-/g, '_')}`]
if (possibleEnvVar) {
return possibleEnvVar
}
// Default
return path.join(TRANSLATIONS_ROOT, languageCode)
}
// Build server languages with directory paths
const allLanguagesWithDirs: Languages = {}
for (const [code, lang] of Object.entries(baseLanguages)) {
allLanguagesWithDirs[code] = {
...lang,
dir: getRoot(lang.locale || code),
}
}
Object.freeze(allLanguagesWithDirs)
const languages: Languages = { ...allLanguagesWithDirs }
if (TRANSLATIONS_FIXTURE_ROOT) {
// Keep all languages that have a directory in the fixture root.
Object.entries(languages).forEach(([code, { dir }]) => {
if (code !== 'en' && !fs.existsSync(dir)) {
delete languages[code]
}
})
} else if (process.env.ENABLED_LANGUAGES) {
if (process.env.ENABLED_LANGUAGES.toLowerCase() !== 'all') {
Object.keys(languages).forEach((code) => {
if (!process.env.ENABLED_LANGUAGES!.includes(code)) {
delete languages[code]
}
})
// This makes the translation health report not valid JSON
// console.log(`ENABLED_LANGUAGES: ${process.env.ENABLED_LANGUAGES}`)
}
} else if (process.env.NODE_ENV === 'test') {
// Unless explicitly set, when running tests default to just English
Object.keys(languages).forEach((code) => {
if (code !== 'en') delete languages[code]
})
}
export const languageKeys: string[] = Object.keys(languages)
export const languagePrefixPathRegex: RegExp = new RegExp(`^/(${languageKeys.join('|')})(/|$)`)
/** Return true if the URL is something like /en/foo or /ja but return false
* if it's something like /foo or /foo/bar or /fr (because French (fr)
* is currently not an active language)
*/
export function pathLanguagePrefixed(path: string): boolean {
return languagePrefixPathRegex.test(path)
}
export default languages

View File

@@ -1,26 +1,15 @@
// See also languages-schema.ts
// Nota bene: If you are adding a new language,
// change accept-language handling in CDN config as well.
import path from 'path'
import fs from 'fs'
import dotenv from 'dotenv'
import { ROOT, TRANSLATIONS_ROOT, TRANSLATIONS_FIXTURE_ROOT } from '@/frame/lib/constants'
dotenv.config({ quiet: true })
export interface Language {
name: string
nativeName?: string
code: string
hreflang: string
redirectPatterns?: RegExp[]
dir: string
}
/**
* Client-safe language definitions without server-side dependencies.
* For server-side usage with fs/path operations, import from './languages-server.ts'
*/
export type LanguageCode = 'en' | 'es' | 'ja' | 'pt' | 'zh' | 'ru' | 'fr' | 'ko' | 'de'
export type LocaleCode =
| 'en'
| 'es-es'
| 'ja-jp'
| 'pt-br'
@@ -30,57 +19,35 @@ export type LocaleCode =
| 'ko-kr'
| 'de-de'
export interface Language {
name: string
nativeName?: string
code: LanguageCode
hreflang: string
locale: LocaleCode
redirectPatterns?: RegExp[]
dir?: string
}
export interface Languages {
[code: string]: Language
}
const possibleEnvVars: Record<LocaleCode, string | undefined> = {
'es-es': process.env.TRANSLATIONS_ROOT_ES_ES,
'ja-jp': process.env.TRANSLATIONS_ROOT_JA_JP,
'pt-br': process.env.TRANSLATIONS_ROOT_PT_BR,
'zh-cn': process.env.TRANSLATIONS_ROOT_ZH_CN,
'ru-ru': process.env.TRANSLATIONS_ROOT_RU_RU,
'fr-fr': process.env.TRANSLATIONS_ROOT_FR_FR,
'ko-kr': process.env.TRANSLATIONS_ROOT_KO_KR,
'de-de': process.env.TRANSLATIONS_ROOT_DE_DE,
}
function getRoot(languageCode: string): string {
if (languageCode === 'en') return ROOT
// This one trumps anything else. This makes it possible, and convenient,
// for running tests that depends on testing translations based on
// fixtures exclusively.
if (TRANSLATIONS_FIXTURE_ROOT) {
return path.join(TRANSLATIONS_FIXTURE_ROOT, languageCode)
}
if (languageCode in possibleEnvVars) {
const possibleEnvVar = possibleEnvVars[languageCode as LocaleCode]
if (possibleEnvVar) {
return possibleEnvVar
}
} else {
console.warn(`Not recognized languageCode '${languageCode}'`)
}
// Default
return path.join(TRANSLATIONS_ROOT, languageCode)
}
// Languages in order of accept-language header frequency
const allLanguages: Languages = {
// Note: 'dir' is omitted here as it requires server-side path resolution
export const languages: Languages = {
en: {
name: 'English',
code: 'en',
hreflang: 'en',
dir: getRoot('en'),
locale: 'en',
},
es: {
name: 'Spanish',
nativeName: 'Español',
code: 'es',
hreflang: 'es',
dir: getRoot('es-es'),
locale: 'es-es',
},
ja: {
name: 'Japanese',
@@ -88,7 +55,7 @@ const allLanguages: Languages = {
code: 'ja',
hreflang: 'ja',
redirectPatterns: [/^\/jp/],
dir: getRoot('ja-jp'),
locale: 'ja-jp',
},
pt: {
name: 'Portuguese',
@@ -96,7 +63,7 @@ const allLanguages: Languages = {
code: 'pt',
hreflang: 'pt',
redirectPatterns: [/^\/br/],
dir: getRoot('pt-br'),
locale: 'pt-br',
},
zh: {
name: 'Simplified Chinese',
@@ -104,21 +71,21 @@ const allLanguages: Languages = {
code: 'zh',
hreflang: 'zh-Hans',
redirectPatterns: [/^\/cn/, /^\/zh-\w{2}/],
dir: getRoot('zh-cn'),
locale: 'zh-cn',
},
ru: {
name: 'Russian',
nativeName: 'Русский',
code: 'ru',
hreflang: 'ru',
dir: getRoot('ru-ru'),
locale: 'ru-ru',
},
fr: {
name: 'French',
nativeName: 'Français',
code: 'fr',
hreflang: 'fr',
dir: getRoot('fr-fr'),
locale: 'fr-fr',
},
ko: {
name: 'Korean',
@@ -126,59 +93,16 @@ const allLanguages: Languages = {
code: 'ko',
hreflang: 'ko',
redirectPatterns: [/^\/kr/],
dir: getRoot('ko-kr'),
locale: 'ko-kr',
},
de: {
name: 'German',
nativeName: 'Deutsch',
code: 'de',
hreflang: 'de',
dir: getRoot('de-de'),
locale: 'de-de',
},
}
// Some markdownlint tests depend on having access to all
// language keys. Not modifying the original object makes
// it possible to export all keys, even when those directories
// don't exist on disk.
Object.freeze(allLanguages)
export const allLanguageKeys: string[] = Object.keys(allLanguages)
const languages: Languages = { ...allLanguages }
if (TRANSLATIONS_FIXTURE_ROOT) {
// Keep all languages that have a directory in the fixture root.
Object.entries(languages).forEach(([code, { dir }]) => {
if (code !== 'en' && !fs.existsSync(dir)) {
delete languages[code]
}
})
} else if (process.env.ENABLED_LANGUAGES) {
if (process.env.ENABLED_LANGUAGES.toLowerCase() !== 'all') {
Object.keys(languages).forEach((code) => {
if (!process.env.ENABLED_LANGUAGES!.includes(code)) {
delete languages[code]
}
})
// This makes the translation health report not valid JSON
// console.log(`ENABLED_LANGUAGES: ${process.env.ENABLED_LANGUAGES}`)
}
} else if (process.env.NODE_ENV === 'test') {
// Unless explicitly set, when running tests default to just English
Object.keys(languages).forEach((code) => {
if (code !== 'en') delete languages[code]
})
}
Object.freeze(languages)
export const languageKeys: string[] = Object.keys(languages)
export const languagePrefixPathRegex: RegExp = new RegExp(`^/(${languageKeys.join('|')})(/|$)`)
/** Return true if the URL is something like /en/foo or /ja but return false
* if it's something like /foo or /foo/bar or /fr (because French (fr)
* is currently not an active language)
*/
export function pathLanguagePrefixed(path: string): boolean {
return languagePrefixPathRegex.test(path)
}
export default languages

View File

@@ -2,7 +2,7 @@ import type { Request, Response, NextFunction } from 'express'
import parser from 'accept-language-parser'
import type { Language as parserLanguage } from 'accept-language-parser'
import languages, { languageKeys } from '@/languages/lib/languages'
import languages, { languageKeys } from '@/languages/lib/languages-server'
import { USER_LANGUAGE_COOKIE_NAME } from '@/frame/lib/constants'
import type { ExtendedRequest, Languages } from '@/types'
import { updateLoggerContext } from '@/observability/logger/lib/logger-context'

View File

@@ -7,7 +7,7 @@ import { TokenizationError } from 'liquidjs'
import walk from 'walk-sync'
import { getLiquidTokens } from '@/content-linter/lib/helpers/liquid-utils'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import warmServer from '@/frame/lib/warm-server'
import type { Site } from '@/types'
import { correctTranslatedContentStrings } from '@/languages/lib/correct-translation-content'

View File

@@ -1,4 +1,4 @@
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import { makeLanguageSurrogateKey } from '@/frame/middleware/set-fastly-surrogate-key'
import purgeEdgeCache from '@/workflows/purge-edge-cache'

View File

@@ -1,4 +1,4 @@
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import { describe, expect, test, vi } from 'vitest'
describe('files', () => {

View File

@@ -1,6 +1,6 @@
import { describe, expect, test, vi } from 'vitest'
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import { blockIndex } from '@/frame/middleware/block-robots'
import { get, getDOMCached as getDOM } from '@/tests/helpers/e2etest'
import Page from '@/frame/lib/page'

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest'
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import { getDOM } from '@/tests/helpers/e2etest'
const langs = languageKeys.filter((lang) => lang !== 'en')

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest'
import { get } from '@/tests/helpers/e2etest'
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
const langs = languageKeys.filter((lang) => lang !== 'en')

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest'
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import { get } from '@/tests/helpers/e2etest'
import { USER_LANGUAGE_COOKIE_NAME } from '@/frame/lib/constants'

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest'
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import { get } from '@/tests/helpers/e2etest'
const langs = languageKeys.filter((lang) => lang !== 'en')

View File

@@ -6,7 +6,7 @@ import { productMap, data } from '@/products/lib/all-products'
import { renderContentWithFallback } from '@/languages/lib/render-with-fallback'
import removeFPTFromPath from '@/versions/lib/remove-fpt-from-path'
import frontmatter from '@/frame/lib/read-frontmatter'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
type PageMap = Record<string, Page>

View File

@@ -1,4 +1,4 @@
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-version'
import { allVersions } from '@/versions/lib/all-versions'
import {

View File

@@ -1,7 +1,7 @@
import type { NextFunction, Response } from 'express'
import patterns from '@/frame/lib/patterns'
import { pathLanguagePrefixed } from '@/languages/lib/languages'
import { pathLanguagePrefixed } from '@/languages/lib/languages-server'
import { deprecatedWithFunctionalRedirects } from '@/versions/lib/enterprise-server-releases'
import getRedirect from '../lib/get-redirect'
import { defaultCacheControl, languageCacheControl } from '@/frame/middleware/cache-control'

View File

@@ -1,6 +1,6 @@
import type { NextFunction, Response } from 'express'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import { defaultCacheControl } from '@/frame/middleware/cache-control'
import { ExtendedRequest } from '@/types'

View File

@@ -4,7 +4,7 @@ import path from 'path'
import { readCompressedJsonFileFallback } from '@/frame/lib/read-json-file'
import { getAutomatedPageMiniTocItems } from '@/frame/lib/get-mini-toc-items'
import { allVersions, getOpenApiVersion } from '@/versions/lib/all-versions'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import type { Context } from '@/types'
import type { Operation } from '@/rest/components/types'

View File

@@ -1,4 +1,4 @@
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import { utcTimestamp } from '@/search/lib/helpers/time'
import { allIndexVersionKeys, versionToIndexVersionMap } from '@/search/lib/elasticsearch-versions'

View File

@@ -3,7 +3,7 @@
we need to validate and parse the parameters. This file contains the configuration for which parameters
to expect based on the type of search request "e.g. general search vs autocomplete search" and how to validate them.
*/
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import { allIndexVersionKeys, versionToIndexVersionMap } from '@/search/lib/elasticsearch-versions'
import { SearchTypes } from '@/search/types'

View File

@@ -10,7 +10,7 @@ import { Command, Option } from 'commander'
import chalk from 'chalk'
import dotenv from 'dotenv'
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import { allVersions } from '@/versions/lib/all-versions'
import type { IndicesAnalyzeAnalyzeToken } from '@elastic/elasticsearch/lib/api/types'

View File

@@ -2,7 +2,7 @@ import { program, Option, Command, InvalidArgumentError } from 'commander'
import { errors } from '@elastic/elasticsearch'
import dotenv from 'dotenv'
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import { indexGeneralSearch } from './lib/index-general-search'
import {

View File

@@ -1,6 +1,6 @@
import { Client } from '@elastic/elasticsearch'
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import { getElasticSearchIndex } from '@/search/lib/elasticsearch-indexes'
import { getElasticsearchClient } from '@/search/lib/helpers/get-client'
import {

View File

@@ -3,7 +3,7 @@ import chalk from 'chalk'
import dotenv from 'dotenv'
import boxen from 'boxen'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import parsePageSectionsIntoRecords from '@/search/scripts/scrape/lib/parse-page-sections-into-records'
import getPopularPages from '@/search/scripts/scrape/lib/popular-pages'
import domwaiter from '@/search/scripts/scrape/lib/domwaiter'

View File

@@ -1,6 +1,6 @@
import chalk from 'chalk'
import languages from '@/languages/lib/languages'
import languages from '@/languages/lib/languages-server'
import buildRecords from '@/search/scripts/scrape/lib/build-records'
import findIndexablePages from '@/search/scripts/scrape/lib/find-indexable-pages'
import { writeIndexRecords } from '@/search/scripts/scrape/lib/search-index-records'

View File

@@ -4,7 +4,7 @@
import { existsSync, statSync, readdirSync } from 'fs'
import { program, Option } from 'commander'
import { languageKeys } from '@/languages/lib/languages'
import { languageKeys } from '@/languages/lib/languages-server'
import scrapeIntoIndexJson from '@/search/scripts/scrape/lib/scrape-into-index-json'
import {
allIndexVersionOptions,

View File

@@ -3,6 +3,7 @@ import type { Failbot } from '@github/failbot'
import type enterpriseServerReleases from '@/versions/lib/enterprise-server-releases.d'
import type { ValidOcticon } from '@/landings/types'
import type { Language, Languages } from '@/languages/lib/languages-server'
import type { MiniTocItem } from '@/frame/lib/get-mini-toc-items'
// Shared type for resolved article information used across landing pages and carousels
@@ -325,16 +326,8 @@ export type SecretScanningData = {
isduplicate: boolean
}
type Language = {
name: string
code: string
hreflang: string
dir: string
}
export type Languages = {
[key: string]: Language
}
// Language and Languages types are imported at the top from languages-server
export type { Language, Languages }
export type Permalink = {
languageCode: string