diff --git a/.github/workflows/docsearch-crawl.yml b/.github/workflows/docsearch-crawl.yml index c99955f2222..b209af89e76 100644 --- a/.github/workflows/docsearch-crawl.yml +++ b/.github/workflows/docsearch-crawl.yml @@ -11,6 +11,11 @@ jobs: steps: - name: Checkout codebase uses: actions/checkout@v2 + - name: Install node and ts-node + uses: actions/setup-node@v2 + with: + node-version: "14" + - run: npm install -g ts-node - name: Crawl the site env: APPLICATION_ID: ${{ secrets.DOCSEARCH_APP_ID }} @@ -18,5 +23,5 @@ jobs: run: | docker run \ -e APPLICATION_ID -e API_KEY \ - -e CONFIG="$(node .github/workflows/docsearchConfigScript.js | cat .github/workflows/docsearchConfig.json)" \ + -e CONFIG="$(ts-node -O '{\"module\": \"commonjs\"}' .github/workflows/docsearchConfigScript.js | cat .github/workflows/docsearchConfig.json)" \ algolia/docsearch-scraper:v1.6.0 diff --git a/.github/workflows/docsearchConfigScript.js b/.github/workflows/docsearchConfigScript.js index 90b15153ea3..f6c4fe44c93 100644 --- a/.github/workflows/docsearchConfigScript.js +++ b/.github/workflows/docsearchConfigScript.js @@ -1,5 +1,5 @@ const fs = require("fs") -const translations = require("../../src/data/translations.json") +const translations = require("../../src/utils/languages").default var config = { index_name: "prod-ethereum-org", diff --git a/.prettierignore b/.prettierignore index 58d06c368a2..557dcfdb3ab 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,4 @@ .cache package.json package-lock.json -public +public \ No newline at end of file diff --git a/gatsby-browser.js b/gatsby-browser.tsx similarity index 84% rename from gatsby-browser.js rename to gatsby-browser.tsx index 04cb2762509..904ebb3e16f 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.tsx @@ -11,9 +11,9 @@ import Prism from "prism-react-renderer/prism" ;(typeof global !== "undefined" ? global : window).Prism = Prism // FormatJS Polyfill imports - Used for intl number formatting -require("@formatjs/intl-locale/polyfill") -require("@formatjs/intl-numberformat/polyfill") -require("@formatjs/intl-numberformat/locale-data/en") +import "@formatjs/intl-locale/polyfill" +import "@formatjs/intl-numberformat/polyfill" +import "@formatjs/intl-numberformat/locale-data/en" // Default languages included: // https://github.com/FormidableLabs/prism-react-renderer/blob/master/src/vendor/prism/includeLangs.js diff --git a/gatsby-config.js b/gatsby-config.ts similarity index 91% rename from gatsby-config.js rename to gatsby-config.ts index cb944720097..f3f1c2916bf 100644 --- a/gatsby-config.js +++ b/gatsby-config.ts @@ -1,8 +1,14 @@ -require("dotenv").config() +import "dotenv/config" +import path from "path" -const { supportedLanguages, allLanguages } = require("./src/utils/translations") +import type { GatsbyConfig } from "gatsby" + +import { + supportedLanguages, + defaultLanguage, + ignoreLanguages, +} from "./src/utils/languages" -const defaultLanguage = `en` const siteUrl = `https://ethereum.org` const ignoreContent = (process.env.IGNORE_CONTENT || "") @@ -11,11 +17,11 @@ const ignoreContent = (process.env.IGNORE_CONTENT || "") const isPreviewDeploy = process.env.IS_PREVIEW_DEPLOY === "true" -const ignoreTranslations = Object.keys(allLanguages) - .filter((lang) => !supportedLanguages.includes(lang)) - .map((lang) => `**/translations\/${lang}`) +const ignoreTranslations = ignoreLanguages.map( + (lang) => `**/translations\/${lang}` +) -const config = { +const config: GatsbyConfig = { siteMetadata: { // `title` & `description` pulls from respective ${lang}.json files in PageMetadata.js title: `ethereum.org`, @@ -33,7 +39,7 @@ const config = { resolve: `gatsby-plugin-intl`, options: { // language JSON resource path - path: `${__dirname}/src/intl`, + path: path.resolve(`src/intl`), // supported language languages: supportedLanguages, // language file path @@ -125,7 +131,7 @@ const config = { gatsbyRemarkPlugins: [ { // Local plugin to adjust the images urls of the translated md files - resolve: require.resolve(`./plugins/gatsby-remark-image-urls`), + resolve: path.resolve(`./plugins/gatsby-remark-image-urls`), }, { resolve: `gatsby-remark-autolink-headers`, @@ -181,7 +187,7 @@ const config = { resolve: `gatsby-source-filesystem`, options: { name: `assets`, - path: `${__dirname}/src/assets`, + path: path.resolve(`src/assets`), }, }, // Process files from /src/content/ (used in gatsby-node.js) @@ -189,7 +195,7 @@ const config = { resolve: `gatsby-source-filesystem`, options: { name: `content`, - path: `${__dirname}/src/content`, + path: path.resolve(`src/content`), ignore: [...ignoreContent, ...ignoreTranslations], }, }, @@ -198,13 +204,13 @@ const config = { resolve: `gatsby-source-filesystem`, options: { name: `data`, - path: `${__dirname}/src/data`, + path: path.resolve(`src/data`), }, }, { resolve: `gatsby-source-filesystem`, options: { - path: `${__dirname}/src/data/translation-reports`, + path: path.resolve(`src/data/translation-reports`), }, }, // Process files within /src/data/ @@ -250,4 +256,4 @@ if (!isPreviewDeploy) { ] } -module.exports = config +export default config diff --git a/gatsby-node.js b/gatsby-node.ts similarity index 78% rename from gatsby-node.js rename to gatsby-node.ts index 9c7e7e6adf2..c577a9d7fc8 100644 --- a/gatsby-node.js +++ b/gatsby-node.ts @@ -1,71 +1,48 @@ // https://www.gatsbyjs.org/docs/node-apis/ -const fs = require("fs") -const path = require(`path`) -const util = require("util") -const child_process = require("child_process") -const { createFilePath } = require(`gatsby-source-filesystem`) -const gatsbyConfig = require(`./gatsby-config.js`) -const redirects = require(`./redirects.json`) +import fs from "fs" +import path from "path" +import util from "util" +import child_process from "child_process" +import { createFilePath } from "gatsby-source-filesystem" +import type { GatsbyNode } from "gatsby" + +import type { Context } from "./src/types" +import type { AllMdxQuery } from "./src/interfaces" + +import mergeTranslations from "./src/scripts/mergeTranslations" +import copyContributors from "./src/scripts/copyContributors" + +import { + supportedLanguages, + defaultLanguage, + Lang, +} from "./src/utils/languages" +import getMessages from "./src/utils/getMessages" +import redirects from "./redirects.json" const exec = util.promisify(child_process.exec) -const supportedLanguages = gatsbyConfig.siteMetadata.supportedLanguages -const defaultLanguage = gatsbyConfig.siteMetadata.defaultLanguage - -// same function from 'gatsby-plugin-intl' -const flattenMessages = (nestedMessages, prefix = "") => { - return Object.keys(nestedMessages).reduce((messages, key) => { - let value = nestedMessages[key] - let prefixedKey = prefix ? `${prefix}.${key}` : key - - if (typeof value === "string") { - messages[prefixedKey] = value - } else { - Object.assign(messages, flattenMessages(value, prefixedKey)) - } - - return messages - }, {}) -} - -// same function from 'gatsby-plugin-intl' -const getMessages = (path, language) => { - try { - const messages = require(`${path}/${language}.json`) - - return flattenMessages(messages) - } catch (error) { - if (error.code === "MODULE_NOT_FOUND") { - process.env.NODE_ENV !== "test" && - console.error( - `[gatsby-plugin-intl] couldn't find file "${path}/${language}.json"` - ) - } - - throw error - } -} - /** * Markdown isOutdated check * Parse header ids in markdown file (both translated and english) and compare their info structure. * If this structure is not the same, then the file isOutdated. * If there is not english file, return true - * @param {string} path filepath for translated mdx file + * @param {string} filePath filepath for translated mdx file * @returns boolean for if file is outdated or not */ -const checkIsMdxOutdated = (path) => { - const splitPath = path.split(__dirname) +const checkIsMdxOutdated = (filePath) => { + const dirname = path.resolve("./") + const splitPath = filePath.split(dirname) const tempSplitPath = splitPath[1] const tempSplit = tempSplitPath.split("/") tempSplit.splice(3, 2) - const englishPath = `${__dirname}${tempSplit.join("/")}` + const englishPath = path.resolve(`${tempSplit.join("/")}`) const re = /([#]+) [^\{]+\{#([^\}]+)\}/gim let translatedData, englishData try { - translatedData = fs.readFileSync(path, "utf-8") + translatedData = fs.readFileSync(filePath, "utf-8") englishData = fs.readFileSync(englishPath, "utf-8") } catch { return true @@ -93,14 +70,14 @@ const checkIsMdxOutdated = (path) => { * Checks if translation JSON file exists. * If translation file exists, checks that all translations are present (checks keys), and that all the keys are the same. * If translation file exists, isContentEnglish will be false - * @param {*} path url path used to derive file path from + * @param {*} urlPath url path used to derive file path from * @param {*} lang language abbreviation for language path * @returns {{isOutdated: boolean, isContentEnglish: boolean}} */ -const checkIsPageOutdated = async (path, lang) => { +const checkIsPageOutdated = async (urlPath, lang) => { // Files that need index appended on the end. Ex page-index.json, page-developers-index.json, page-upgrades-index.json const indexFilePaths = ["", "developers", "upgrades"] - const filePath = path.split("/").filter((text) => text !== "") + const filePath = urlPath.split("/").filter((text) => text !== "") if ( indexFilePaths.includes(filePath[filePath.length - 1]) || @@ -110,8 +87,8 @@ const checkIsPageOutdated = async (path, lang) => { } const joinedFilepath = filePath.join("-") - const srcPath = `${__dirname}/src/intl/${lang}/page-${joinedFilepath}.json` - const englishPath = `${__dirname}/src/intl/en/page-${joinedFilepath}.json` + const srcPath = path.resolve(`src/intl/${lang}/page-${joinedFilepath}.json`) + const englishPath = path.resolve(`src/intl/en/page-${joinedFilepath}.json`) // If no file exists, default to english if (!fs.existsSync(srcPath)) { @@ -122,8 +99,8 @@ const checkIsPageOutdated = async (path, lang) => { } else { let translatedData, englishData, translatedKeys, englishKeys try { - translatedData = JSON.parse(fs.readFileSync(srcPath)) - englishData = JSON.parse(fs.readFileSync(englishPath)) + translatedData = JSON.parse(fs.readFileSync(srcPath).toString()) + englishData = JSON.parse(fs.readFileSync(englishPath).toString()) translatedKeys = Object.keys(translatedData) englishKeys = Object.keys(englishData) } catch (err) { @@ -160,7 +137,11 @@ const checkIsPageOutdated = async (path, lang) => { // Loops through all the files dictated by Gatsby (building pages folder), as well as // folders flagged through the gatsby-source-filesystem plugin in gatsby-config -exports.onCreateNode = async ({ node, getNode, actions }) => { +export const onCreateNode: GatsbyNode["onCreateNode"] = async ({ + node, + getNode, + actions, +}) => { const { createNodeField } = actions // Edit markdown nodes @@ -178,7 +159,7 @@ exports.onCreateNode = async ({ node, getNode, actions }) => { slug = `/${defaultLanguage}${slug}` } - const absolutePath = node.fileAbsolutePath + const absolutePath = node.fileAbsolutePath as string const relativePathStart = absolutePath.lastIndexOf("src/") const relativePath = absolutePath.substring(relativePathStart) @@ -203,7 +184,11 @@ exports.onCreateNode = async ({ node, getNode, actions }) => { } } -exports.createPages = async ({ graphql, actions, reporter }) => { +export const createPages: GatsbyNode["createPages"] = async ({ + graphql, + actions, + reporter, +}) => { const { createPage, createRedirect } = actions redirects.forEach((redirect) => { @@ -215,8 +200,8 @@ exports.createPages = async ({ graphql, actions, reporter }) => { }) }) - const result = await graphql(` - query { + const result = await graphql<{ allMdx: { edges: Array } }>(` + query getAllMdx { allMdx { edges { node { @@ -258,7 +243,7 @@ exports.createPages = async ({ graphql, actions, reporter }) => { slug.includes(`/terms-of-use/`) || slug.includes(`/contributing/`) || slug.includes(`/style-guide/`) - const language = node.frontmatter.lang + const language = node.frontmatter.lang as Lang if (!language) { throw `Missing 'lang' frontmatter property. All markdown pages must have a lang property. Page slug: ${slug}` } @@ -279,7 +264,7 @@ exports.createPages = async ({ graphql, actions, reporter }) => { const langSlug = splitSlug.join("/") createPage({ path: langSlug, - component: path.resolve(`./src/templates/${template}.js`), + component: path.resolve(`src/templates/${template}.js`), context: { slug: langSlug, ignoreTranslationBanner: isLegal, @@ -304,9 +289,9 @@ exports.createPages = async ({ graphql, actions, reporter }) => { } } - createPage({ + createPage({ path: slug, - component: path.resolve(`./src/templates/${template}.js`), + component: path.resolve(`src/templates/${template}.js`), context: { slug, isOutdated: node.fields.isOutdated, @@ -333,7 +318,9 @@ exports.createPages = async ({ graphql, actions, reporter }) => { const outdatedMarkdown = [`eth`, `dapps`, `wallets`, `what-is-ethereum`] outdatedMarkdown.forEach((page) => { supportedLanguages.forEach(async (lang) => { - const markdownPath = `${__dirname}/src/content/translations/${lang}/${page}/index.md` + const markdownPath = path.resolve( + `src/content/translations/${lang}/${page}/index.md` + ) const langHasOutdatedMarkdown = fs.existsSync(markdownPath) if (!langHasOutdatedMarkdown) { // Check if json strings exists for language, if not mark `isContentEnglish` as true @@ -345,8 +332,8 @@ exports.createPages = async ({ graphql, actions, reporter }) => { path: `/${lang}/${page}/`, component: path.resolve( page === "wallets" - ? `./src/pages-conditional/${page}/index.js` - : `./src/pages-conditional/${page}.js` + ? `src/pages-conditional/${page}/index.js` + : `src/pages-conditional/${page}.js` ), context: { slug: `/${lang}/${page}/`, @@ -371,7 +358,10 @@ exports.createPages = async ({ graphql, actions, reporter }) => { // Add additional context to translated pages // Only ran when creating component pages // https://www.gatsbyjs.com/docs/creating-and-modifying-pages/#pass-context-to-pages -exports.onCreatePage = async ({ page, actions }) => { +export const onCreatePage: GatsbyNode["onCreatePage"] = async ({ + page, + actions, +}) => { const { createPage, deletePage } = actions const isTranslated = page.context.language !== defaultLanguage @@ -383,7 +373,7 @@ exports.onCreatePage = async ({ page, actions }) => { page.context.language ) deletePage(page) - createPage({ + createPage({ ...page, context: { ...page.context, @@ -395,9 +385,10 @@ exports.onCreatePage = async ({ page, actions }) => { } } -exports.createSchemaCustomization = ({ actions }) => { - const { createTypes } = actions - const typeDefs = ` +export const createSchemaCustomization: GatsbyNode["createSchemaCustomization"] = + ({ actions }) => { + const { createTypes } = actions + const typeDefs = ` type Mdx implements Node { frontmatter: Frontmatter } @@ -435,18 +426,27 @@ exports.createSchemaCustomization = ({ actions }) => { score: Int } ` - createTypes(typeDefs) + createTypes(typeDefs) - // Optimization. Ref: https://www.gatsbyjs.com/docs/scaling-issues/#switch-off-type-inference-for-sitepagecontext - createTypes(` + // Optimization. Ref: https://www.gatsbyjs.com/docs/scaling-issues/#switch-off-type-inference-for-sitepagecontext + createTypes(` type SitePage implements Node @dontInfer { path: String! } `) + } + +export const onPreBootstrap: GatsbyNode["onPreBootstrap"] = ({ reporter }) => { + mergeTranslations() + reporter.info(`Merged translations saved`) + copyContributors() + reporter.info(`Contributors copied`) } // Build lambda functions when the build is complete and the `/public` folder exists -exports.onPostBuild = async (gatsbyNodeHelpers) => { +export const onPostBuild: GatsbyNode["onPostBuild"] = async ( + gatsbyNodeHelpers +) => { const { reporter } = gatsbyNodeHelpers const reportOut = (report) => { diff --git a/gatsby-ssr.js b/gatsby-ssr.tsx similarity index 75% rename from gatsby-ssr.js rename to gatsby-ssr.tsx index 7de8e83cd38..7dbc3a3d757 100644 --- a/gatsby-ssr.js +++ b/gatsby-ssr.tsx @@ -5,10 +5,16 @@ */ import React from "react" + +import type { GatsbySSR } from "gatsby" + import Layout from "./src/components/Layout" // Prevents from unmounting on page transitions // https://www.gatsbyjs.com/docs/layout-components/#how-to-prevent-layout-components-from-unmounting -export const wrapPageElement = ({ element, props }) => { +export const wrapPageElement: GatsbySSR["wrapPageElement"] = ({ + element, + props, +}) => { return {element} } diff --git a/package.json b/package.json index f195e4a40fb..4f5135469d1 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,9 @@ "unist-util-visit-parents": "^2.1.2" }, "devDependencies": { + "@types/node": "^17.0.23", + "@types/react": "^17.0.39", + "@types/react-dom": "^17.0.11", "babel-jest": "^26.6.3", "babel-preset-gatsby": "^1.2.0", "github-slugger": "^1.3.0", @@ -74,23 +77,22 @@ "jest": "^26.6.3", "prettier": "^2.2.1", "pretty-quick": "^3.1.0", - "react-test-renderer": "^17.0.1" + "react-test-renderer": "^17.0.1", + "typescript": "^4.6.3" }, "scripts": { - "postinstall": "yarn prebuild", - "prebuild": "yarn merge-translations && yarn copy-contributors", - "build": "yarn prebuild && yarn build:app", - "build:app": "gatsby build", + "build": "gatsby build", "build:lambda": "netlify-lambda build src/lambda", "copy-contributors": "node src/scripts/copy-contributors.js", "format": "prettier --write \"**/*.{js,jsx,json,md}\"", "generate-heading-ids": "node src/scripts/generate-heading-ids.js", "merge-translations": "node src/scripts/merge-translations.js", - "start": "yarn prebuild && gatsby develop", + "start": "gatsby develop", "start:lambda": "netlify-lambda serve src/lambda", "start:static": "gatsby build && gatsby serve", "serve": "gatsby serve", - "test": "jest" + "test": "jest", + "type-check": "tsc --noEmit" }, "husky": { "hooks": { diff --git a/src/components/Breadcrumbs.js b/src/components/Breadcrumbs.js index 09ea34e6681..c91e4b56842 100644 --- a/src/components/Breadcrumbs.js +++ b/src/components/Breadcrumbs.js @@ -3,7 +3,8 @@ import styled from "styled-components" import { useIntl } from "gatsby-plugin-intl" import Link from "./Link" -import { translateMessageId, supportedLanguages } from "../utils/translations" +import { supportedLanguages } from "../utils/languages" +import { translateMessageId } from "../utils/translations" const ListContainer = styled.nav` margin-bottom: 2rem; diff --git a/src/components/LegacyPageHome.js b/src/components/LegacyPageHome.js deleted file mode 100644 index 74656554a9c..00000000000 --- a/src/components/LegacyPageHome.js +++ /dev/null @@ -1,233 +0,0 @@ -import React from "react" -import { useIntl } from "gatsby-plugin-intl" -import { useStaticQuery, graphql } from "gatsby" -import { GatsbyImage, getImage } from "gatsby-plugin-image" -import styled from "styled-components" -import { translateMessageId } from "../utils/translations" -import Morpher from "./Morpher" -import PageMetadata from "./PageMetadata" -import Translation from "./Translation" -import Link from "./Link" -import { Divider } from "./SharedStyledComponents" - -const Hero = styled(GatsbyImage)` - width: 100%; - min-height: 380px; - max-height: 500px; - background-size: cover; - background: no-repeat 50px; -` - -const Page = styled.div` - display: flex; - flex-direction: column; - align-items: center; - width: 100%; - margin: 0 auto; -` - -const Content = styled.div` - width: 100%; - padding: 1rem 2rem; -` - -const Header = styled.header` - display: flex; - flex-direction: column; -` - -const Title = styled.div` - display: flex; - justify-content: space-between; - width: 100%; - max-width: 100%; -` - -const H1 = styled.h1` - line-height: 1.4; - font-weight: 400; - font-size: 1.5rem; - margin: 1.5rem 0; - max-width: 80%; - @media (max-width: ${(props) => props.theme.breakpoints.m}) { - max-width: 100%; - } -` - -const Description = styled.p` - color: ${(props) => props.theme.colors.text200}; - max-width: 55ch; -` - -const SectionContainer = styled.div` - display: flex; - justify-content: space-between; - @media (max-width: ${(props) => props.theme.breakpoints.l}) { - flex-wrap: wrap; - } -` - -const Section = styled.div` - flex: 1 1 300px; - margin-bottom: 2rem; - margin-right: 3rem; - @media (max-width: ${(props) => props.theme.breakpoints.m}) { - margin-right: 2rem; - } - @media (max-width: ${(props) => props.theme.breakpoints.s}) { - margin-right: 0; - } - & > h2 { - margin-top: 1rem; - font-size: 1.25rem; - } - & > p { - color: ${(props) => props.theme.colors.text200}; - max-width: 400px; - } -` - -const H3 = styled.h3` - margin-top: 1.5rem; - margin-bottom: 1.5rem; - @media (max-width: ${(props) => props.theme.breakpoints.m}) { - display: none; - } -` - -const LegacyPageHome = () => { - const intl = useIntl() - const data = useStaticQuery(graphql` - { - hero: file(relativePath: { eq: "home/hero.png" }) { - childImageSharp { - gatsbyImageData( - layout: FULL_WIDTH - placeholder: BLURRED - quality: 100 - ) - } - } - individuals: file(relativePath: { eq: "doge-computer.png" }) { - childImageSharp { - gatsbyImageData( - height: 200 - layout: FIXED - placeholder: BLURRED - quality: 100 - ) - } - } - developers: file(relativePath: { eq: "developers-eth-blocks.png" }) { - childImageSharp { - gatsbyImageData( - height: 200 - layout: FIXED - placeholder: BLURRED - quality: 100 - ) - } - } - enterprise: file(relativePath: { eq: "enterprise-eth.png" }) { - childImageSharp { - gatsbyImageData( - height: 200 - layout: FIXED - placeholder: BLURRED - quality: 100 - ) - } - } - } - `) - - const sections = [ - { - img: { - src: data.individuals, - alt: "page-index-sections-individuals-image-alt", - }, - title: "page-index-sections-individuals-title", - desc: "page-index-sections-individuals-desc", - link: { - text: "page-index-sections-individuals-link-text", - to: "/what-is-ethereum/", - }, - }, - { - img: { - src: data.developers, - alt: "page-index-sections-developers-image-alt", - }, - title: "page-index-sections-developers-title", - desc: "page-index-sections-developers-desc", - link: { - text: "page-index-sections-developers-link-text", - to: "/developers/", - }, - }, - { - img: { - src: data.enterprise, - alt: "page-index-sections-enterprise-image-alt", - }, - title: "page-index-sections-enterprise-title", - desc: "page-index-sections-enterprise-desc", - link: { - text: "page-index-sections-enterprise-link-text", - to: "/enterprise/", - }, - }, - ] - - return ( - - - - -
- - <H1> - <Translation id="page-index-title" /> - </H1> - <H3> - <Morpher /> - </H3> - - - - -
- - - {sections.map((section, idx) => ( -
- -

- -

-

- -

- - - -
- ))} -
-
-
- ) -} - -export default LegacyPageHome diff --git a/src/components/Link.js b/src/components/Link.js index 51e76207909..1b9469c398f 100644 --- a/src/components/Link.js +++ b/src/components/Link.js @@ -4,7 +4,7 @@ import { Link as IntlLink } from "gatsby-plugin-intl" import styled from "styled-components" import Icon from "./Icon" -import { languageMetadata } from "../utils/translations" +import { languageMetadata } from "../utils/languages" import { trackCustomEvent } from "../utils/matomo" const HASH_PATTERN = /^#.*/ diff --git a/src/components/PageMetadata.js b/src/components/PageMetadata.js index ae29cf3a931..59ffac9f94a 100644 --- a/src/components/PageMetadata.js +++ b/src/components/PageMetadata.js @@ -6,7 +6,8 @@ import { useIntl } from "gatsby-plugin-intl" import { Location } from "@reach/router" import { getSrc } from "gatsby-plugin-image" -import { translateMessageId, languageMetadata } from "../utils/translations" +import { languageMetadata } from "../utils/languages" +import { translateMessageId } from "../utils/translations" const supportedLanguages = Object.keys(languageMetadata) diff --git a/src/components/SideNavMobile.js b/src/components/SideNavMobile.js index 61603aefacf..ad15a205294 100644 --- a/src/components/SideNavMobile.js +++ b/src/components/SideNavMobile.js @@ -5,7 +5,7 @@ import { motion, AnimatePresence } from "framer-motion" import Icon from "./Icon" import Link from "./Link" import Translation from "./Translation" -import { supportedLanguages } from "../utils/translations" +import { supportedLanguages } from "../utils/languages" import { dropdownIconContainerVariant } from "./SharedStyledComponents" import docLinks from "../data/developer-docs-links.yaml" diff --git a/src/components/WalletCompare.js b/src/components/WalletCompare.js index 639a3dfedab..d2854ec77d7 100644 --- a/src/components/WalletCompare.js +++ b/src/components/WalletCompare.js @@ -482,7 +482,6 @@ const WalletCompare = ({ location }) => { {selectedFeatures.map((feature) => ( { ))} {remainingFeatures.map((feature) => ( { const [activeCode, setActiveCode] = useState(0) const dir = isLangRightToLeft(language) ? "rtl" : "ltr" - if (legacyHomepageLanguages.includes(language)) return - const toggleCodeExample = (id) => { setActiveCode(id) setModalOpen(true) diff --git a/src/pages/languages.js b/src/pages/languages.js index 28b7db21bb9..9db95029087 100644 --- a/src/pages/languages.js +++ b/src/pages/languages.js @@ -7,7 +7,8 @@ import Translation from "../components/Translation" import Link from "../components/Link" import { Page, Content } from "../components/SharedStyledComponents" -import { languageMetadata, translateMessageId } from "../utils/translations" +import { languageMetadata } from "../utils/languages" +import { translateMessageId } from "../utils/translations" import { CardItem as LangItem } from "../components/SharedStyledComponents" import Icon from "../components/Icon" import NakedButton from "../components/NakedButton" diff --git a/src/scripts/__tests__/merge-translations.js b/src/scripts/__tests__/merge-translations.js index 1cd93ce9755..8b642db97e9 100644 --- a/src/scripts/__tests__/merge-translations.js +++ b/src/scripts/__tests__/merge-translations.js @@ -1,4 +1,4 @@ -import { mergeObjects } from "../merge-translations" +import mergeObjects from "../../utils/mergeObjects" const x = { a: 1, b: 2 } const y = { c: 3, d: 4 } diff --git a/src/scripts/copy-contributors.js b/src/scripts/copy-contributors.js deleted file mode 100644 index 5dff50c7065..00000000000 --- a/src/scripts/copy-contributors.js +++ /dev/null @@ -1,22 +0,0 @@ -const fs = require("fs") -const path = require("path") - -async function main() { - const pathToProjectRoot = __dirname.split("/").slice(0, -2).join("/") - const pathToFile = path.join(pathToProjectRoot, ".all-contributorsrc") - const pathToDestination = path.join( - pathToProjectRoot, - "src", - "data", - "contributors.json" - ) - - fs.copyFileSync(pathToFile, pathToDestination) -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error) - process.exit(1) - }) diff --git a/src/scripts/copyContributors.ts b/src/scripts/copyContributors.ts new file mode 100644 index 00000000000..147a73a709d --- /dev/null +++ b/src/scripts/copyContributors.ts @@ -0,0 +1,17 @@ +import fs from "fs" +import path from "path" + +async function copyContributors() { + const pathToProjectRoot = path.resolve("./") + const pathToFile = path.join(pathToProjectRoot, ".all-contributorsrc") + const pathToDestination = path.join( + pathToProjectRoot, + "src", + "data", + "contributors.json" + ) + + fs.copyFileSync(pathToFile, pathToDestination) +} + +export default copyContributors diff --git a/src/scripts/merge-translations.js b/src/scripts/merge-translations.js deleted file mode 100644 index 6c2dacbe31c..00000000000 --- a/src/scripts/merge-translations.js +++ /dev/null @@ -1,55 +0,0 @@ -const fs = require("fs") -const path = require("path") -require("dotenv").config() - -const { supportedLanguages } = require("../utils/translations") - -// Wrapper on `Object.assign` to throw error if keys clash -const mergeObjects = (target, newObject) => { - const targetKeys = Object.keys(target) - for (const key of Object.keys(newObject)) { - if (targetKeys.includes(key)) { - throw new Error(`target object already has key: ${key}`) - } - } - return Object.assign(target, newObject) -} - -// Iterate over each supported language and generate /intl/${lang}.json -// by merging all /intl/${lang}/${page}.json files -for (const lang of supportedLanguages) { - try { - const currentTranslation = lang - const pathToProjectSrc = __dirname - .split(process.platform === "win32" ? "\\" : "/") - .slice(0, -1) - .join("/") - const pathToTranslations = path.join( - pathToProjectSrc, - "intl", - currentTranslation - ) - - const result = {} - - fs.readdirSync(pathToTranslations).forEach((file) => { - const pathToFile = `${pathToTranslations}/${file}` - const json = fs.readFileSync(pathToFile, "utf-8") - // console.log(`Merging: ${pathToFile}`) - const obj = JSON.parse(json) - mergeObjects(result, obj) - }) - - const outputFilename = `src/intl/${currentTranslation}.json` - - fs.writeFileSync( - outputFilename, - JSON.stringify(result, null, 2).concat("\n") - ) - // console.log(`Merged translations saved: ${outputFilename}`) - } catch (e) { - console.error(e) - } -} - -module.exports.mergeObjects = mergeObjects diff --git a/src/scripts/mergeTranslations.ts b/src/scripts/mergeTranslations.ts new file mode 100644 index 00000000000..758de264507 --- /dev/null +++ b/src/scripts/mergeTranslations.ts @@ -0,0 +1,42 @@ +import fs from "fs" +import path from "path" + +import { supportedLanguages } from "../utils/languages" +import mergeObjects from "../utils/mergeObjects" + +// Iterate over each supported language and generate /intl/${lang}.json +// by merging all /intl/${lang}/${page}.json files +const mergeTranslations = (): void => { + for (const lang of supportedLanguages) { + try { + const currentTranslation = lang + const pathToProjectSrc = path.resolve("src") + const pathToTranslations = path.join( + pathToProjectSrc, + "intl", + currentTranslation + ) + + const result = {} + + fs.readdirSync(pathToTranslations).forEach((file) => { + const pathToFile = `${pathToTranslations}/${file}` + const json = fs.readFileSync(pathToFile, "utf-8") + // console.log(`Merging: ${pathToFile}`) + const obj = JSON.parse(json) + mergeObjects(result, obj) + }) + + const outputFilename = `src/intl/${currentTranslation}.json` + + fs.writeFileSync( + outputFilename, + JSON.stringify(result, null, 2).concat("\n") + ) + } catch (e) { + console.error(e) + } + } +} + +export default mergeTranslations diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000000..f957a782ce0 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,21 @@ +import type { Messages } from "./interfaces" +import type { Lang } from "./utils/languages" + +export type Intl = { + language: Lang + languages: Array + defaultLanguage: Lang + messages: Messages + routed: boolean + originalPath: string + redirect: boolean +} + +export type Context = { + slug: string + relativePath: string + intl: Intl + language?: string + isOutdated: boolean + isContentEnglish?: boolean +} diff --git a/src/utils/flattenMessages.ts b/src/utils/flattenMessages.ts new file mode 100644 index 00000000000..a6f9da15f58 --- /dev/null +++ b/src/utils/flattenMessages.ts @@ -0,0 +1,19 @@ +import type { Messages } from "../interfaces" + +// same function from 'gatsby-plugin-intl' +const flattenMessages = (nestedMessages: Messages, prefix = ""): Messages => { + return Object.keys(nestedMessages).reduce((messages, key) => { + let value = nestedMessages[key] + let prefixedKey = prefix ? `${prefix}.${key}` : key + + if (typeof value === "string") { + messages[prefixedKey] = value + } else { + Object.assign(messages, flattenMessages(value, prefixedKey)) + } + + return messages + }, {}) +} + +export default flattenMessages diff --git a/src/utils/getMessages.ts b/src/utils/getMessages.ts new file mode 100644 index 00000000000..c5499bd2569 --- /dev/null +++ b/src/utils/getMessages.ts @@ -0,0 +1,28 @@ +import fs from "fs" + +import flattenMessages from "./flattenMessages" + +import type { Messages } from "../interfaces" +import type { Lang } from "./languages" + +// same function from 'gatsby-plugin-intl' +const getMessages = (path: string, language: Lang): Messages => { + try { + const messages = JSON.parse( + fs.readFileSync(`${path}/${language}.json`, "utf8") + ) + + return flattenMessages(messages) + } catch (error: any) { + if (error.code === "MODULE_NOT_FOUND") { + process.env.NODE_ENV !== "test" && + console.error( + `[gatsby-plugin-intl] couldn't find file "${path}/${language}.json"` + ) + } + + throw error + } +} + +export default getMessages diff --git a/src/utils/languages.ts b/src/utils/languages.ts new file mode 100644 index 00000000000..4a1594736fb --- /dev/null +++ b/src/utils/languages.ts @@ -0,0 +1,219 @@ +export type Lang = + | "en" + | "ar" + | "az" + | "bg" + | "bn" + | "ca" + | "cs" + | "da" + | "de" + | "el" + | "es" + | "fa" + | "fi" + | "fr" + | "gl" + | "hi" + | "hr" + | "hu" + | "id" + | "ig" + | "it" + | "ja" + | "ka" + | "ko" + | "lt" + | "ml" + | "mr" + | "ms" + | "nl" + | "nb" + | "pl" + | "pt" + | "pt-br" + | "ro" + | "ru" + | "se" + | "sk" + | "sl" + | "sr" + | "sw" + | "th" + | "tr" + | "uk" + | "vi" + | "zh" + | "zh-tw" + +export type Languages = { + [lang in Lang]: { language: string } +} + +export const defaultLanguage: Lang = "en" + +const languages: Languages = { + en: { + language: "English", + }, + ar: { + language: "العربية", + }, + az: { + language: "Azərbaycan", + }, + bg: { + language: "български", + }, + bn: { + language: "বাংলা", + }, + ca: { + language: "Català", + }, + cs: { + language: "Čeština", + }, + da: { + language: "Dansk", + }, + de: { + language: "Deutsch", + }, + el: { + language: "Ελληνικά", + }, + es: { + language: "Español", + }, + fa: { + language: "فارسی", + }, + fi: { + language: "Suomi", + }, + fr: { + language: "Français", + }, + gl: { + language: "Galego", + }, + hi: { + language: "हिन्दी", + }, + hr: { + language: "Hrvatski", + }, + hu: { + language: "Magyar", + }, + id: { + language: "Bahasa Indonesia", + }, + ig: { + language: "Ibo", + }, + it: { + language: "Italiano", + }, + ja: { + language: "日本語", + }, + ka: { + language: "ქართული", + }, + ko: { + language: "한국어", + }, + lt: { + language: "Lietuvis", + }, + ml: { + language: "മലയാളം", + }, + mr: { + language: "मराठी", + }, + ms: { + language: "Melayu", + }, + nl: { + language: "Nederlands", + }, + nb: { + language: "Norsk", + }, + pl: { + language: "Polski", + }, + pt: { + language: "Português", + }, + "pt-br": { + language: "Português", + }, + ro: { + language: "Română", + }, + ru: { + language: "Pусский", + }, + se: { + language: "Svenska", + }, + sk: { + language: "Slovenský", + }, + sl: { + language: "Slovenščina", + }, + sr: { + language: "Српски", + }, + sw: { + language: "Kiswahili", + }, + th: { + language: "ภาษาไทย", + }, + tr: { + language: "Türkçe", + }, + uk: { + language: "Українська", + }, + vi: { + language: "Tiếng Việt", + }, + zh: { + language: "简体中文", + }, + "zh-tw": { + language: "繁體中文", + }, +} + +const buildLangs = (process.env.GATSBY_BUILD_LANGS || "") + .split(",") + .filter(Boolean) + +// will take the same shape as `languages`. Only thing we are doing +// here is filtering the desired langs to be built +export const languageMetadata = Object.fromEntries( + Object.entries(languages).filter(([lang]) => { + // BUILD_LANGS === empty means to build all the langs + if (!buildLangs.length) { + return true + } + + return buildLangs.includes(lang) + }) +) + +export const supportedLanguages = Object.keys(languageMetadata) as Array + +export const ignoreLanguages = (Object.keys(languages) as Array).filter( + (lang) => !supportedLanguages.includes(lang) +) + +export default languages diff --git a/src/utils/mergeObjects.ts b/src/utils/mergeObjects.ts new file mode 100644 index 00000000000..d6871238d44 --- /dev/null +++ b/src/utils/mergeObjects.ts @@ -0,0 +1,12 @@ +// Wrapper on `Object.assign` to throw error if keys clash +const mergeObjects = (target: T, newObject: U): T & U => { + const targetKeys = Object.keys(target) + for (const key of Object.keys(newObject)) { + if (targetKeys.includes(key)) { + throw new Error(`target object already has key: ${key}`) + } + } + return Object.assign(target, newObject) +} + +export default mergeObjects diff --git a/src/utils/translations.js b/src/utils/translations.js deleted file mode 100644 index 2acf920d64f..00000000000 --- a/src/utils/translations.js +++ /dev/null @@ -1,88 +0,0 @@ -const allLanguages = require("../data/translations.json") - -const buildLangs = (process.env.GATSBY_BUILD_LANGS || "") - .split(",") - .filter(Boolean) - -// will take the same shape as `allLanguages`. Only thing we are doing -// here is filtering the desired langs to be built -const languageMetadata = Object.fromEntries( - Object.entries(allLanguages).filter(([lang]) => { - // BUILD_LANGS === empty means to build all the langs - if (!buildLangs.length) { - return true - } - - return buildLangs.includes(lang) - }) -) - -const supportedLanguages = Object.keys(languageMetadata) -const legacyHomepageLanguages = supportedLanguages.filter( - (lang) => languageMetadata[lang].useLegacyHomepage -) - -const consoleError = (message) => { - const { NODE_ENV } = process.env - if (NODE_ENV === "development") { - console.error(message) - } -} - -// Returns the en.json value -const getDefaultMessage = (key) => { - const defaultStrings = require("../intl/en.json") - const defaultMessage = defaultStrings[key] - if (defaultMessage === undefined) { - consoleError( - `No key "${key}" in en.json. Cannot provide a default message.` - ) - } - return defaultMessage || "" -} - -const isLangRightToLeft = (lang) => { - return lang === "ar" || lang === "fa" -} - -const translateMessageId = (id, intl) => { - if (!id) { - consoleError(`No id provided for translation.`) - return "" - } - if (!intl || !intl.formatMessage) { - consoleError(`Invalid/no intl provided for translation id ${id}`) - return "" - } - const translation = intl.formatMessage({ - id, - defaultMessage: getDefaultMessage(id), - }) - if (translation === id) { - consoleError( - `Intl ID string "${id}" has no match. Default message of "" returned.` - ) - return "" - } - return translation -} - -// Overwrites the default Persian numbering of the Farsi language to use Hindu-Arabic numerals (0-9) -// Context: https://github.com/ethereum/ethereum-org-website/pull/5490#pullrequestreview-892596553 -const getLocaleForNumberFormat = (locale) => { - if (locale === "fa") { - return "en" - } - - return locale -} - -// Must export using ES5 to import in gatsby-node.js -module.exports.allLanguages = allLanguages -module.exports.languageMetadata = languageMetadata -module.exports.supportedLanguages = supportedLanguages -module.exports.getDefaultMessage = getDefaultMessage -module.exports.isLangRightToLeft = isLangRightToLeft -module.exports.translateMessageId = translateMessageId -module.exports.legacyHomepageLanguages = legacyHomepageLanguages -module.exports.getLocaleForNumberFormat = getLocaleForNumberFormat diff --git a/src/utils/translations.ts b/src/utils/translations.ts new file mode 100644 index 00000000000..838ad76772f --- /dev/null +++ b/src/utils/translations.ts @@ -0,0 +1,59 @@ +import { IntlShape } from "gatsby-plugin-intl" + +import type { Lang } from "./languages" + +import defaultStrings from "../intl/en.json" + +const consoleError = (message: string): void => { + const { NODE_ENV } = process.env + if (NODE_ENV === "development") { + console.error(message) + } +} + +// Returns the en.json value +export const getDefaultMessage = (key: string): string => { + const defaultMessage = defaultStrings[key] + if (defaultMessage === undefined) { + consoleError( + `No key "${key}" in en.json. Cannot provide a default message.` + ) + } + return defaultMessage || "" +} + +export const isLangRightToLeft = (lang: Lang): boolean => { + return lang === "ar" || lang === "fa" +} + +export const translateMessageId = (id: string, intl: IntlShape): string => { + if (!id) { + consoleError(`No id provided for translation.`) + return "" + } + if (!intl || !intl.formatMessage) { + consoleError(`Invalid/no intl provided for translation id ${id}`) + return "" + } + const translation = intl.formatMessage({ + id, + defaultMessage: getDefaultMessage(id), + }) + if (translation === id) { + consoleError( + `Intl ID string "${id}" has no match. Default message of "" returned.` + ) + return "" + } + return translation +} + +// Overwrites the default Persian numbering of the Farsi language to use Hindu-Arabic numerals (0-9) +// Context: https://github.com/ethereum/ethereum-org-website/pull/5490#pullrequestreview-892596553 +export const getLocaleForNumberFormat = (locale: Lang) => { + if (locale === "fa") { + return "en" + } + + return locale +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000000..a0be1d7f5e7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["dom", "esnext"], + "jsx": "react", + "module": "esnext", + "moduleResolution": "node", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitAny": false, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["./src"] +} diff --git a/yarn.lock b/yarn.lock index 5026715bc16..861cd7e7fda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3303,6 +3303,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa" integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A== +"@types/node@^17.0.23": + version "17.0.23" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da" + integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== + "@types/node@^8.5.7": version "8.10.66" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.66.tgz#dd035d409df322acc83dff62a602f12a5783bbb3" @@ -3345,6 +3350,13 @@ dependencies: "@types/react" "*" +"@types/react-dom@^17.0.11": + version "17.0.15" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.15.tgz#f2c8efde11521a4b7991e076cb9c70ba3bb0d156" + integrity sha512-Tr9VU9DvNoHDWlmecmcsE5ZZiUkYx+nKBzum4Oxe1K0yJVyBlfbq7H3eXjxXqJczBKqPGq3EgfTru4MgKb9+Yw== + dependencies: + "@types/react" "^17" + "@types/react@*": version "17.0.37" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959" @@ -3354,6 +3366,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^17", "@types/react@^17.0.39": + version "17.0.44" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.44.tgz#c3714bd34dd551ab20b8015d9d0dbec812a51ec7" + integrity sha512-Ye0nlw09GeMp2Suh8qoOv0odfgCoowfM/9MG6WeRD60Gq9wS90bdkdRtYbRkNhXOpG4H+YXGvj4wOWhAC0LJ1g== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/responselike@*", "@types/responselike@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" @@ -15671,6 +15692,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript@^4.6.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c" + integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== + unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"