Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/[locale]/community/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ export async function generateMetadata({
const { locale } = params

const t = await getTranslations({ locale, namespace: "page-community" })

return await getMetadata({
locale,
slug: ["community"],
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { getRequestConfig } from "next-intl/server"

import { Lang } from "@/lib/types"

import { loadMessages } from "./loadMessages"
import { routing } from "./routing"

import { loadMessages } from "@/lib/i18n/loadMessages"

export default getRequestConfig(async ({ requestLocale }) => {
// This typically corresponds to the `[locale]` segment
let locale = await requestLocale
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function loadMessages(locale: string) {
const namespaces = getNamespaces(localePath)

for (const ns of namespaces) {
messages[ns] = (await import(`../intl/${locale}/${ns}.json`)).default
messages[ns] = (await import(`../../intl/${locale}/${ns}.json`)).default
}
}

Expand Down
30 changes: 30 additions & 0 deletions src/lib/i18n/pageTranslation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getPrimaryNamespaceForPath } from "../utils/translations"

import { areNamespacesTranslated } from "./translationStatus"

/**
* Determine if a page should be considered translated for a given locale.
*
* This checks only the primary namespace inferred from the provided path. When
* no primary namespace exists for the path, the page is assumed translated
* because it depends solely on globally available shared namespaces (like
* "common") rather than page-specific strings.
*
* @param locale - Locale code, e.g., "en", "es"
* @param slug - Page path or slug, e.g., "/wallets/"
* @returns Promise resolving to whether the page is translated
* @example
* await isPageTranslated("es", "/wallets/") // => true | false
*/
export async function isPageTranslated(
locale: string,
slug: string
): Promise<boolean> {
const primaryNamespace = getPrimaryNamespaceForPath(slug)

if (!primaryNamespace) {
return true
}

return areNamespacesTranslated(locale, [primaryNamespace])
}
19 changes: 19 additions & 0 deletions src/lib/i18n/translationStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { DEFAULT_LOCALE } from "@/lib/constants"

import { loadMessages } from "@/lib/i18n/loadMessages"

/**
* Determine whether all required i18n namespaces exist for a given locale.
* Default locale is always considered translated.
*/
export async function areNamespacesTranslated(
locale: string,
namespaces: string[]
): Promise<boolean> {
if (locale === DEFAULT_LOCALE) return true

const localeMessages = await loadMessages(locale)
return namespaces.every((ns) =>
Object.prototype.hasOwnProperty.call(localeMessages, ns)
)
}
6 changes: 4 additions & 2 deletions src/lib/md/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const getMdMetadata = async ({
}) => {
const slug = slugArray.join("/")

const { markdown } = await importMd(locale, slug)
const { markdown, isTranslated } = await importMd(locale, slug)
const { frontmatter } = await compile({
markdown,
slugArray: slug.split("/"),
Expand All @@ -28,12 +28,14 @@ export const getMdMetadata = async ({
const image = frontmatter.image
const author = frontmatter.author

return await getMetadata({
const metadata = await getMetadata({
locale,
slug: slugArray,
title: pageTitle,
description,
image,
author,
noIndex: !isTranslated,
})
return metadata
}
15 changes: 14 additions & 1 deletion src/lib/utils/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { getTranslations } from "next-intl/server"

import { DEFAULT_OG_IMAGE, SITE_URL } from "@/lib/constants"

import { isPageTranslated } from "../i18n/pageTranslation"

import { isLocaleValidISO639_1 } from "./translations"
import { getFullUrl } from "./url"

Expand Down Expand Up @@ -42,6 +44,7 @@ export const getMetadata = async ({
twitterDescription,
image,
author,
noIndex = false,
}: {
locale: string
slug: string[]
Expand All @@ -50,6 +53,7 @@ export const getMetadata = async ({
twitterDescription?: string
image?: string
author?: string
noIndex?: boolean
}): Promise<Metadata> => {
const slugString = slug.join("/")
const t = await getTranslations({ locale, namespace: "common" })
Expand All @@ -66,7 +70,7 @@ export const getMetadata = async ({
/* Set fallback ogImage based on path */
const ogImage = image || getOgImage(slug)

return {
const base: Metadata = {
title,
description,
metadataBase: new URL(SITE_URL),
Expand Down Expand Up @@ -110,4 +114,13 @@ export const getMetadata = async ({
"docsearch:description": description,
},
}

if (noIndex) {
return { ...base, robots: { index: false } }
}

const isTranslated = await isPageTranslated(locale, slugString)

// If the page is not translated, do not index the page
return isTranslated ? base : { ...base, robots: { index: false } }
}
168 changes: 97 additions & 71 deletions src/lib/utils/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,111 @@ export const getRequiredNamespacesForPage = (
const getRequiredNamespacesForPath = (relativePath: string) => {
const path = url.addSlashes(relativePath)

let primaryNamespace: string | undefined // the primary namespace for the page
const primaryNamespace = getPrimaryNamespaceForPath(path) // the primary namespace for the page
let requiredNamespaces: string[] = [] // any additional namespaces required for the page

if (path === "/") {
requiredNamespaces = [...requiredNamespaces, "page-10-year-anniversary"]
}

if (path.startsWith("/energy-consumption/")) {
requiredNamespaces = [...requiredNamespaces, "page-about"]
}

if (path.startsWith("/glossary/")) {
requiredNamespaces = [...requiredNamespaces, "glossary"]
}

if (path.startsWith("/developers/docs/scaling/")) {
requiredNamespaces = [...requiredNamespaces, "page-layer-2"]
}

if (path.startsWith("/roadmap/vision/")) {
requiredNamespaces = [
...requiredNamespaces,
"page-upgrades-index",
"page-roadmap-vision",
]
}

if (path.startsWith("/gas/")) {
requiredNamespaces = [...requiredNamespaces, "page-gas", "page-community"]
}

if (path.endsWith("/wallets/find-wallet/")) {
requiredNamespaces = [...requiredNamespaces, "page-wallets", "table"]
}

if (path.startsWith("/layer-2/networks/")) {
requiredNamespaces = [...requiredNamespaces, "table"]
}

if (path.startsWith("/start/")) {
requiredNamespaces = [...requiredNamespaces]
}

if (path.startsWith("/10years/")) {
requiredNamespaces = [...requiredNamespaces, "page-10-year-anniversary"]
}

// Glossary tooltips
if (
path.startsWith("/apps/") ||
path.startsWith("/layer-2/") ||
path.startsWith("/layer-2/learn/") ||
path.startsWith("/get-eth/") ||
path.startsWith("/stablecoins/") ||
path.startsWith("/staking/") ||
path.startsWith("/run-a-node/") ||
path.startsWith("/what-is-ethereum/") ||
path.startsWith("/eth/") ||
path.startsWith("/wallets/") ||
path.startsWith("/gas/")
) {
requiredNamespaces = [...requiredNamespaces, "glossary-tooltip"]
}

// Quizzes
// Note: Add any URL paths that have quizzes here
if (
path.startsWith("/defi/") ||
path.startsWith("/eth/") ||
path.startsWith("/gas/") ||
path.startsWith("/layer-2/") ||
path.startsWith("/layer-2/learn/") ||
path.startsWith("/nft/") ||
path.startsWith("/quizzes/") ||
path.startsWith("/roadmap/merge/") ||
path.startsWith("/roadmap/scaling/") ||
path.startsWith("/run-a-node/") ||
path.startsWith("/security/") ||
path.startsWith("/smart-contracts/") ||
path.startsWith("/stablecoins/") ||
path.startsWith("/staking/solo/") ||
path.startsWith("/wallets/") ||
path.startsWith("/web3/") ||
path.startsWith("/what-is-ethereum/")
) {
requiredNamespaces = [...requiredNamespaces, "learn-quizzes"]
}

// Ensures that the primary namespace is always the first item in the array
return primaryNamespace
? [primaryNamespace, ...requiredNamespaces]
: [...requiredNamespaces]
}

export const getPrimaryNamespaceForPath = (relativePath: string) => {
const path = url.addSlashes(relativePath)

let primaryNamespace: string | undefined

if (path === "/assets/") {
primaryNamespace = "page-assets"
}

if (path === "/") {
primaryNamespace = "page-index"
requiredNamespaces = [...requiredNamespaces, "page-10-year-anniversary"]
}

if (path === "/collectibles/") {
Expand All @@ -106,17 +201,12 @@ const getRequiredNamespacesForPath = (relativePath: string) => {

if (path.startsWith("/energy-consumption/")) {
primaryNamespace = "page-energy-consumption"
requiredNamespaces = [...requiredNamespaces, "page-about"]
}

if (path.startsWith("/eth/")) {
primaryNamespace = "page-eth"
}

if (path.startsWith("/glossary/")) {
requiredNamespaces = [...requiredNamespaces, "glossary"]
}

if (path.startsWith("/ethereum-forks/")) {
primaryNamespace = "page-history"
}
Expand Down Expand Up @@ -157,25 +247,12 @@ const getRequiredNamespacesForPath = (relativePath: string) => {
primaryNamespace = "page-developers-tutorials"
}

if (path.startsWith("/developers/docs/scaling/")) {
requiredNamespaces = [...requiredNamespaces, "page-layer-2"]
}

if (path === "/get-eth/") {
primaryNamespace = "page-get-eth"
}

if (path.startsWith("/roadmap/vision/")) {
requiredNamespaces = [
...requiredNamespaces,
"page-upgrades-index",
"page-roadmap-vision",
]
}

if (path.startsWith("/gas/")) {
primaryNamespace = "page-gas"
requiredNamespaces = [...requiredNamespaces, "page-gas", "page-community"]
}

if (path.startsWith("/what-is-ethereum/")) {
Expand All @@ -196,7 +273,6 @@ const getRequiredNamespacesForPath = (relativePath: string) => {

if (path.endsWith("/wallets/find-wallet/")) {
primaryNamespace = "page-wallets-find-wallet"
requiredNamespaces = [...requiredNamespaces, "page-wallets", "table"]
}

// TODO: Remove this when the page is translated
Expand All @@ -210,7 +286,6 @@ const getRequiredNamespacesForPath = (relativePath: string) => {

if (path.startsWith("/layer-2/networks/")) {
primaryNamespace = "page-layer-2-networks"
requiredNamespaces = [...requiredNamespaces, "table"]
}

if (path.startsWith("/roadmap/")) {
Expand All @@ -219,62 +294,13 @@ const getRequiredNamespacesForPath = (relativePath: string) => {

if (path.startsWith("/start/")) {
primaryNamespace = "page-start"
requiredNamespaces = [...requiredNamespaces]
}

if (path.startsWith("/contributing/translation-program/translatathon/")) {
primaryNamespace = "page-translatathon"
}

if (path.startsWith("/10years/")) {
requiredNamespaces = [...requiredNamespaces, "page-10-year-anniversary"]
}

// Glossary tooltips
if (
path.startsWith("/apps/") ||
path.startsWith("/layer-2/") ||
path.startsWith("/layer-2/learn/") ||
path.startsWith("/get-eth/") ||
path.startsWith("/stablecoins/") ||
path.startsWith("/staking/") ||
path.startsWith("/run-a-node/") ||
path.startsWith("/what-is-ethereum/") ||
path.startsWith("/eth/") ||
path.startsWith("/wallets/") ||
path.startsWith("/gas/")
) {
requiredNamespaces = [...requiredNamespaces, "glossary-tooltip"]
}

// Quizzes
// Note: Add any URL paths that have quizzes here
if (
path.startsWith("/defi/") ||
path.startsWith("/eth/") ||
path.startsWith("/gas/") ||
path.startsWith("/layer-2/") ||
path.startsWith("/layer-2/learn/") ||
path.startsWith("/nft/") ||
path.startsWith("/quizzes/") ||
path.startsWith("/roadmap/merge/") ||
path.startsWith("/roadmap/scaling/") ||
path.startsWith("/run-a-node/") ||
path.startsWith("/security/") ||
path.startsWith("/smart-contracts/") ||
path.startsWith("/stablecoins/") ||
path.startsWith("/staking/solo/") ||
path.startsWith("/wallets/") ||
path.startsWith("/web3/") ||
path.startsWith("/what-is-ethereum/")
) {
requiredNamespaces = [...requiredNamespaces, "learn-quizzes"]
}

// Ensures that the primary namespace is always the first item in the array
return primaryNamespace
? [primaryNamespace, ...requiredNamespaces]
: [...requiredNamespaces]
}

const getRequiredNamespacesForLayout = (layout?: string) => {
Expand Down